From b9421209d6c45aab2cd0bde6c80db1b02eb6af66 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Mon, 18 Sep 2023 23:32:43 +0800 Subject: [PATCH 01/10] ci: add ledger-merged-develop --- .github/workflows/ci-workflow.yml | 13 +- .github/workflows/guidelines-enforcer.yml | 22 + .github/workflows/swap-ci-workflow.yml | 16 + .gitignore | 1 + CHANGELOG.md | 25 + Makefile | 158 +-- bitcoin_client/CHANGELOG.md | 12 + bitcoin_client/ledger_bitcoin/__init__.py | 2 +- bitcoin_client/ledger_bitcoin/bip380/README | 4 + .../ledger_bitcoin/bip380/__init__.py | 1 + .../bip380/descriptors/__init__.py | 220 +++ .../bip380/descriptors/checksum.py | 71 + .../bip380/descriptors/errors.py | 5 + .../bip380/descriptors/parsing.py | 56 + .../bip380/descriptors/utils.py | 21 + bitcoin_client/ledger_bitcoin/bip380/key.py | 338 +++++ .../bip380/miniscript/__init__.py | 13 + .../bip380/miniscript/errors.py | 20 + .../bip380/miniscript/fragments.py | 1225 +++++++++++++++++ .../bip380/miniscript/parsing.py | 736 ++++++++++ .../bip380/miniscript/property.py | 83 ++ .../bip380/miniscript/satisfaction.py | 409 ++++++ .../ledger_bitcoin/bip380/utils/__init__.py | 0 .../ledger_bitcoin/bip380/utils/bignum.py | 64 + .../ledger_bitcoin/bip380/utils/hashes.py | 20 + .../bip380/utils/ripemd_fallback.py | 117 ++ .../ledger_bitcoin/bip380/utils/script.py | 473 +++++++ .../ledger_bitcoin/btchip/btchipHelpers.py | 4 +- bitcoin_client/ledger_bitcoin/client.py | 35 +- bitcoin_client/ledger_bitcoin/descriptor.py | 633 --------- bitcoin_client/ledger_bitcoin/py.typed | 0 bitcoin_client/ledger_bitcoin/segwit_addr.py | 137 ++ bitcoin_client/pyproject.toml | 2 + bitcoin_client/setup.cfg | 5 + bitcoin_client/tests/requirements.txt | 5 +- bitcoin_client_js/README.md | 32 +- bitcoin_client_js/package.json | 10 +- .../src/__tests__/appClient.test.ts | 146 +- .../src/__tests__/psbtv2.test.ts | 20 +- bitcoin_client_js/src/index.ts | 3 +- bitcoin_client_js/src/lib/appClient.ts | 193 ++- bitcoin_client_js/src/lib/bip32.ts | 4 +- bitcoin_client_js/src/lib/psbtv2.ts | 74 +- bitcoin_client_rs/Cargo.toml | 15 +- .../examples/ledger_hwi/Cargo.toml | 3 +- .../examples/ledger_hwi/src/main.rs | 36 +- bitcoin_client_rs/src/async_client.rs | 144 +- bitcoin_client_rs/src/client.rs | 150 +- bitcoin_client_rs/src/command.rs | 2 +- bitcoin_client_rs/src/error.rs | 3 + bitcoin_client_rs/src/interpreter.rs | 8 +- bitcoin_client_rs/src/lib.rs | 2 +- bitcoin_client_rs/src/merkle.rs | 4 +- bitcoin_client_rs/src/psbt.rs | 506 ++++++- bitcoin_client_rs/src/wallet.rs | 96 +- bitcoin_client_rs/tests/client.rs | 70 +- bitcoin_client_rs/tests/data/sign_psbt.json | 444 +++++- bitcoin_client_rs/tests/utils/mod.rs | 6 +- desktop-wallet/src/main/LedgerApi.ts | 1 + .../src/main/connectors/Speculos.ts | 2 +- doc/bitcoin.md | 15 +- doc/wallet.md | 39 +- glyphs/Bitcoin_64px.bmp | Bin 0 -> 674 bytes icons/stax_app_bitcoin.gif | Bin 0 -> 347 bytes src/boilerplate/dispatcher.c | 3 +- src/boilerplate/dispatcher.h | 2 +- src/boilerplate/io.c | 31 +- src/boilerplate/io.h | 2 + src/common/bip32.c | 55 - src/common/bip32.h | 49 - src/common/script.c | 107 +- src/common/wallet.c | 52 +- src/common/wallet.h | 16 +- src/constants.h | 5 + src/crypto.c | 362 ++--- src/crypto.h | 51 +- src/debug-helpers/debug.c | 11 +- src/handler/get_extended_pubkey.c | 54 +- src/handler/get_master_fingerprint.c | 4 +- src/handler/get_wallet_address.c | 102 +- src/handler/lib/get_preimage.c | 3 +- src/handler/lib/policy.c | 381 +++-- src/handler/lib/policy.h | 85 +- src/handler/lib/stream_preimage.c | 3 +- src/handler/register_wallet.c | 31 +- src/handler/sign_message.c | 9 +- src/handler/sign_psbt.c | 329 +++-- src/main.c | 37 +- src/swap/btchip_bcd.c | 1 - src/swap/handle_swap_sign_transaction.c | 12 + src/swap/handle_swap_sign_transaction.h | 2 + src/swap/swap_lib_calls.h | 72 +- src/ui/display.c | 616 ++------- src/ui/display.h | 125 +- src/ui/display_bagl.c | 475 +++++++ src/ui/display_nbgl.c | 532 +++++++ src/ui/display_utils.c | 2 +- src/ui/menu.c | 55 +- src/ui/menu.h | 12 +- src/ui/menu_bagl.c | 87 ++ src/ui/menu_nbgl.c | 60 + test_utils/requirements.txt | 2 +- tests/automations/register_wallet_accept.json | 20 +- tests/automations/register_wallet_reject.json | 6 +- tests/automations/sign_message_accept.json | 19 + tests/automations/sign_message_reject.json | 11 +- .../sign_with_default_wallet_accept.json | 24 +- ...ault_wallet_accept_nondefault_sighash.json | 18 +- ..._wallet_missing_nonwitnessutxo_accept.json | 24 +- .../automations/sign_with_wallet_accept.json | 24 +- ...gn_with_wallet_external_inputs_accept.json | 24 +- ..._wallet_missing_nonwitnessutxo_accept.json | 45 + tests/requirements.txt | 5 +- tests/skip_until_poda_test_e2e_miniscript.py | 61 +- tests/skip_until_poda_test_e2e_tapscripts.py | 15 +- tests/syscoin.conf | 2 + tests/test_dashboard.py | 3 + tests/test_e2e_multisig.py | 9 +- tests/test_get_extended_pubkey.py | 117 +- tests/test_get_wallet_address.py | 65 +- tests/test_get_wallet_address_v1.py | 104 +- tests/test_protocol.py | 33 + tests/test_register_wallet.py | 17 +- tests/test_sign_psbt.py | 197 ++- tests/test_sign_psbt_v1.py | 92 +- tests_mainnet/requirements.txt | 5 +- tests_mainnet/test_dashboard.py | 3 + unit-tests/CMakeLists.txt | 34 +- unit-tests/libs/crypto_mocks.c | 10 + unit-tests/libs/crypto_mocks.h | 7 + unit-tests/libs/sha-256.c | 224 +++ unit-tests/libs/sha-256.h | 105 ++ unit-tests/test_bip32.c | 80 +- unit-tests/test_script.c | 6 +- unit-tests/test_wallet.c | 31 + 135 files changed, 9595 insertions(+), 2656 deletions(-) create mode 100644 .github/workflows/guidelines-enforcer.yml create mode 100644 .github/workflows/swap-ci-workflow.yml create mode 100644 bitcoin_client/ledger_bitcoin/bip380/README create mode 100644 bitcoin_client/ledger_bitcoin/bip380/__init__.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/descriptors/__init__.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/descriptors/checksum.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/descriptors/errors.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/descriptors/parsing.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/descriptors/utils.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/key.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/__init__.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/errors.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/fragments.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/parsing.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/property.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/miniscript/satisfaction.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/utils/__init__.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/utils/bignum.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/utils/hashes.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/utils/ripemd_fallback.py create mode 100644 bitcoin_client/ledger_bitcoin/bip380/utils/script.py delete mode 100644 bitcoin_client/ledger_bitcoin/descriptor.py create mode 100644 bitcoin_client/ledger_bitcoin/py.typed create mode 100644 bitcoin_client/ledger_bitcoin/segwit_addr.py create mode 100644 glyphs/Bitcoin_64px.bmp create mode 100644 icons/stax_app_bitcoin.gif create mode 100644 src/ui/display_bagl.c create mode 100644 src/ui/display_nbgl.c create mode 100644 src/ui/menu_bagl.c create mode 100644 src/ui/menu_nbgl.c create mode 100644 tests/automations/sign_with_wallet_missing_nonwitnessutxo_accept.json create mode 100644 tests/test_protocol.py create mode 100644 unit-tests/libs/crypto_mocks.c create mode 100644 unit-tests/libs/crypto_mocks.h create mode 100644 unit-tests/libs/sha-256.c create mode 100644 unit-tests/libs/sha-256.h diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index aa902ac3..3c9c589c 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -6,6 +6,7 @@ on: branches: - master - develop + - ledger-merged-develop pull_request: branches: - master @@ -13,7 +14,7 @@ on: jobs: job_build: - name: Compilation for NanoS, X and S+ + name: Compilation for NanoS, X, S+, and Stax strategy: matrix: @@ -24,6 +25,8 @@ jobs: SDK: "$NANOX_SDK" - model: nanosp SDK: "$NANOSP_SDK" + - model: stax + SDK: "$STAX_SDK" runs-on: ubuntu-latest @@ -106,6 +109,7 @@ jobs: - model: nanos - model: nanox - model: nanosp + - model: stax needs: job_build runs-on: ubuntu-latest @@ -135,7 +139,7 @@ jobs: run: | cd tests pip install -r requirements.txt - PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --model=${{ matrix.model }} + PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --model=${{ matrix.model }} --timeout=500 job_test_mainnet: name: Tests on mainnet @@ -145,6 +149,7 @@ jobs: - model: nanos - model: nanox - model: nanosp + - model: stax needs: job_build runs-on: ubuntu-latest @@ -174,7 +179,7 @@ jobs: run: | cd tests_mainnet pip install -r requirements.txt - PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --model=${{ matrix.model }} + PYTHONPATH=$PYTHONPATH:/speculos pytest --headless --model=${{ matrix.model }} --timeout=300 # Legacy not applicable to Syscoin because no prior app # job_test_python_lib_legacyapp: @@ -259,4 +264,4 @@ jobs: - name: Run tests run: | cd bitcoin_client_rs/ - cargo test + cargo test --no-default-features --features="async" diff --git a/.github/workflows/guidelines-enforcer.yml b/.github/workflows/guidelines-enforcer.yml new file mode 100644 index 00000000..c154d6cf --- /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/swap-ci-workflow.yml b/.github/workflows/swap-ci-workflow.yml new file mode 100644 index 00000000..85225eca --- /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 933b01a2..e2943848 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ bin/ debug/ dep/ obj/ +build/ # Unit tests and code coverage unit-tests/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f35ee88..741b30a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Dates are in `dd-mm-yyyy` format. +## [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 diff --git a/Makefile b/Makefile index 8a90ae3e..0718019f 100644 --- a/Makefile +++ b/Makefile @@ -22,32 +22,44 @@ 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 = "" -APPVERSION_M = 1 +# Application allowed derivation curves. +CURVE_APP_LOAD_PARAMS = secp256k1 + +# Application allowed derivation paths. +PATH_APP_LOAD_PARAMS = "" +APP_LOAD_PARAMS += --path_slip21 "LEDGER-Wallet policy" + +# Application version +APPVERSION_M = 2 APPVERSION_N = 0 APPVERSION_P = 0 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" - APP_STACK_SIZE = 3072 +# Setting to allow building variant applications +VARIANT_PARAM = COIN +VARIANT_VALUES = syscoin_regtest syscoin + # simplify for tests ifndef COIN COIN=syscoin_regtest endif -# Flags: BOLOS_SETTINGS, GLOBAL_PIN, DERIVE_MASTER -APP_LOAD_FLAGS=--appFlags 0xa50 +######################################## +# Application custom permissions # +######################################## +HAVE_APPLICATION_FLAG_DERIVE_MASTER = 1 +HAVE_APPLICATION_FLAG_GLOBAL_PIN = 1 +HAVE_APPLICATION_FLAG_BOLOS_SETTINGS = 1 +HAVE_APPLICATION_FLAG_LIBRARY = 1 ifeq ($(COIN),syscoin_regtest) # Syscoin testnet, no legacy support DEFINES += BIP32_PUBKEY_VERSION=0x043587CF 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\" @@ -74,32 +86,34 @@ $(error Unsupported COIN - use syscoin_regtest, syscoin) endif endif -APP_LOAD_PARAMS += $(APP_LOAD_FLAGS) - -ifeq ($(TARGET_NAME),TARGET_NANOS) -ICONNAME=icons/nanos_app_$(COIN).gif -else -ICONNAME=icons/nanox_app_$(COIN).gif -endif - -all: default - -# TODO: double check if all those flags are still relevant/needed (was copied from legacy app-syscoin) - -DEFINES += APPNAME=\"$(APPNAME)\" -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 - -DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL="" +# Application icons following guidelines: +# https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon +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 + +######################################## +# Application communication interfaces # +######################################## +ENABLE_BLUETOOTH = 1 + +######################################## +# NBGL custom features # +######################################## +ENABLE_NBGL_QRCODE = 1 + +######################################## +# Features disablers # +######################################## +# Don't use standard app file to avoid conflicts for now +DISABLE_STANDARD_APP_FILES = 1 + +# 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 += UNUSED\(x\)=\(void\)x -DEFINES += APPVERSION=\"$(APPVERSION)\" - DEFINES += HAVE_BOLOS_APP_STACK_CANARY @@ -108,16 +122,6 @@ 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) @@ -130,77 +134,21 @@ CFLAGS += -include debug-helpers/debug.h # DEFINES += HAVE_PRINT_STACK_POINTER -ifndef DEBUG - DEBUG = 0 -endif - -ifeq ($(DEBUG),0) - DEFINES += PRINTF\(...\)= -else - ifeq ($(DEBUG),10) - $(warning Using semihosted PRINTF. Only run with speculos!) - DEFINES += HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF PRINTF=semihosted_printf - else - ifeq ($(TARGET_NAME),TARGET_NANOS) - DEFINES += HAVE_PRINTF PRINTF=screen_printf - else - DEFINES += HAVE_PRINTF PRINTF=mcu_usb_printf - endif - endif +ifeq ($(DEBUG),10) + $(warning Using semihosted PRINTF. Only run with speculos!) + DEFINES += HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF PRINTF=semihosted_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 -CFLAGS += -Oz -AS := $(GCCPATH)arm-none-eabi-gcc -LD := $(GCCPATH)arm-none-eabi-gcc -LDFLAGS += -O3 -Os -LDLIBS += -lm -lgcc -lc - -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 - -delete: - python3 -m ledgerblue.deleteApp $(COMMON_DELETE_PARAMS) - -include $(BOLOS_SDK)/Makefile.rules - -dep/%.d: %.c Makefile - -listvariants: - @echo VARIANTS COIN syscoin_regtest syscoin +# 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/bitcoin_client/CHANGELOG.md b/bitcoin_client/CHANGELOG.md index d477076c..baf9065c 100644 --- a/bitcoin_client/CHANGELOG.md +++ b/bitcoin_client/CHANGELOG.md @@ -7,6 +7,18 @@ 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 diff --git a/bitcoin_client/ledger_bitcoin/__init__.py b/bitcoin_client/ledger_bitcoin/__init__.py index 3c2ac741..f0a63c7c 100644 --- a/bitcoin_client/ledger_bitcoin/__init__.py +++ b/bitcoin_client/ledger_bitcoin/__init__.py @@ -7,7 +7,7 @@ from .wallet import AddressType, WalletPolicy, MultisigWallet, WalletType -__version__ = '0.2.0' +__version__ = '0.2.2' __all__ = [ "Client", diff --git a/bitcoin_client/ledger_bitcoin/bip380/README b/bitcoin_client/ledger_bitcoin/bip380/README new file mode 100644 index 00000000..3609525a --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/README @@ -0,0 +1,4 @@ +This folder is based on https://github.com/Eunovo/python-bip380/tree/4226b7f2b70211d696155f6fd39edc611761ed0b, in turn built on https://github.com/darosior/python-bip380/commit/d2f5d8f5b41cba189bd793c1081e9d61d2d160c1. + +The library is "not ready for any real world use", however we _only_ use it in order to generate addresses for descriptors containing miniscript, and compare the result with the address computed by the device. +This is a generic mitigation for any bug related to address generation on the device, like [this](https://donjon.ledger.com/lsb/019/). diff --git a/bitcoin_client/ledger_bitcoin/bip380/__init__.py b/bitcoin_client/ledger_bitcoin/bip380/__init__.py new file mode 100644 index 00000000..27fdca49 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.3" diff --git a/bitcoin_client/ledger_bitcoin/bip380/descriptors/__init__.py b/bitcoin_client/ledger_bitcoin/bip380/descriptors/__init__.py new file mode 100644 index 00000000..bc4eaac4 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/descriptors/__init__.py @@ -0,0 +1,220 @@ +from ...bip380.key import DescriptorKey +from ...bip380.miniscript import Node +from ...bip380.utils.hashes import sha256, hash160 +from ...bip380.utils.script import ( + CScript, + OP_1, + OP_DUP, + OP_HASH160, + OP_EQUALVERIFY, + OP_CHECKSIG, +) + +from .checksum import descsum_create +from .errors import DescriptorParsingError +from .parsing import descriptor_from_str +from .utils import taproot_tweak + + +class Descriptor: + """A Bitcoin Output Script Descriptor.""" + + def from_str(desc_str, strict=False): + """Parse a Bitcoin Output Script Descriptor from its string representation. + + :param strict: whether to require the presence of a checksum. + """ + desc = descriptor_from_str(desc_str, strict) + + # BIP389 prescribes that no two multipath key expressions in a single descriptor + # have different length. + multipath_len = None + for key in desc.keys: + if key.is_multipath(): + m_len = len(key.path.paths) + if multipath_len is None: + multipath_len = m_len + elif multipath_len != m_len: + raise DescriptorParsingError( + f"Descriptor contains multipath key expressions with varying length: '{desc_str}'." + ) + + return desc + + @property + def script_pubkey(self): + """Get the ScriptPubKey (output 'locking' Script) for this descriptor.""" + # To be implemented by derived classes + raise NotImplementedError + + @property + def script_sighash(self): + """Get the Script to be committed to by the signature hash of a spending transaction.""" + # To be implemented by derived classes + raise NotImplementedError + + @property + def keys(self): + """Get the list of all keys from this descriptor, in order of apparition.""" + # To be implemented by derived classes + raise NotImplementedError + + def derive(self, index): + """Derive the key at the given derivation index. + + A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened + wildcard". + """ + assert isinstance(index, int) + for key in self.keys: + key.derive(index) + + def satisfy(self, *args, **kwargs): + """Get the witness stack to spend from this descriptor. + + Various data may need to be passed as parameters to meet the locking + conditions set by the Script. + """ + # To be implemented by derived classes + raise NotImplementedError + + def copy(self): + """Get a copy of this descriptor.""" + # FIXME: do something nicer than roundtripping through string ser + return Descriptor.from_str(str(self)) + + def is_multipath(self): + """Whether this descriptor contains multipath key expression(s).""" + return any(k.is_multipath() for k in self.keys) + + def singlepath_descriptors(self): + """Get a list of descriptors that only contain keys that don't have multiple + derivation paths. + """ + singlepath_descs = [self.copy()] + + # First figure out the number of descriptors there will be + for key in self.keys: + if key.is_multipath(): + singlepath_descs += [ + self.copy() for _ in range(len(key.path.paths) - 1) + ] + break + + # Return early if there was no multipath key expression + if len(singlepath_descs) == 1: + return singlepath_descs + + # Then use one path for each + for i, desc in enumerate(singlepath_descs): + for key in desc.keys: + if key.is_multipath(): + assert len(key.path.paths) == len(singlepath_descs) + key.path.paths = key.path.paths[i: i + 1] + + assert all(not d.is_multipath() for d in singlepath_descs) + return singlepath_descs + + +# TODO: add methods to give access to all the Miniscript analysis +class WshDescriptor(Descriptor): + """A Segwit v0 P2WSH Output Script Descriptor.""" + + def __init__(self, witness_script): + assert isinstance(witness_script, Node) + self.witness_script = witness_script + + def __repr__(self): + return descsum_create(f"wsh({self.witness_script})") + + @property + def script_pubkey(self): + witness_program = sha256(self.witness_script.script) + return CScript([0, witness_program]) + + @property + def script_sighash(self): + return self.witness_script.script + + @property + def keys(self): + return self.witness_script.keys + + def satisfy(self, sat_material=None): + """Get the witness stack to spend from this descriptor. + + :param sat_material: a miniscript.satisfaction.SatisfactionMaterial with data + available to fulfill the conditions set by the Script. + """ + sat = self.witness_script.satisfy(sat_material) + if sat is not None: + return sat + [self.witness_script.script] + + +class WpkhDescriptor(Descriptor): + """A Segwit v0 P2WPKH Output Script Descriptor.""" + + def __init__(self, pubkey): + assert isinstance(pubkey, DescriptorKey) + self.pubkey = pubkey + + def __repr__(self): + return descsum_create(f"wpkh({self.pubkey})") + + @property + def script_pubkey(self): + witness_program = hash160(self.pubkey.bytes()) + return CScript([0, witness_program]) + + @property + def script_sighash(self): + key_hash = hash160(self.pubkey.bytes()) + return CScript([OP_DUP, OP_HASH160, key_hash, OP_EQUALVERIFY, OP_CHECKSIG]) + + @property + def keys(self): + return [self.pubkey] + + def satisfy(self, signature): + """Get the witness stack to spend from this descriptor. + + :param signature: a signature (in bytes) for the pubkey from the descriptor. + """ + assert isinstance(signature, bytes) + return [signature, self.pubkey.bytes()] + + +class TrDescriptor(Descriptor): + """A Pay-to-Taproot Output Script Descriptor.""" + + def __init__(self, internal_key): + assert isinstance(internal_key, DescriptorKey) and internal_key.x_only + self.internal_key = internal_key + + def __repr__(self): + return descsum_create(f"tr({self.internal_key})") + + def output_key(self): + # "If the spending conditions do not require a script path, the output key + # should commit to an unspendable script path" (see BIP341, BIP386) + return taproot_tweak(self.internal_key.bytes(), b"").format() + + @property + def script_pubkey(self): + return CScript([OP_1, self.output_key()]) + + @property + def keys(self): + return [self.internal_key] + + def satisfy(self, sat_material=None): + """Get the witness stack to spend from this descriptor. + + :param sat_material: a miniscript.satisfaction.SatisfactionMaterial with data + available to spend from the key path or any of the leaves. + """ + out_key = self.output_key() + if out_key in sat_material.signatures: + return [sat_material.signatures[out_key]] + + return diff --git a/bitcoin_client/ledger_bitcoin/bip380/descriptors/checksum.py b/bitcoin_client/ledger_bitcoin/bip380/descriptors/checksum.py new file mode 100644 index 00000000..9f3e0132 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/descriptors/checksum.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Utility functions related to output descriptors""" + +import re + +INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " +CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +GENERATOR = [0xF5DEE51989, 0xA9FDCA3312, 0x1BAB10E32D, 0x3706B1677A, 0x644D626FFD] + + +def descsum_polymod(symbols): + """Internal function that computes the descriptor checksum.""" + chk = 1 + for value in symbols: + top = chk >> 35 + chk = (chk & 0x7FFFFFFFF) << 5 ^ value + for i in range(5): + chk ^= GENERATOR[i] if ((top >> i) & 1) else 0 + return chk + + +def descsum_expand(s): + """Internal function that does the character to symbol expansion""" + groups = [] + symbols = [] + for c in s: + if not c in INPUT_CHARSET: + return None + v = INPUT_CHARSET.find(c) + symbols.append(v & 31) + groups.append(v >> 5) + if len(groups) == 3: + symbols.append(groups[0] * 9 + groups[1] * 3 + groups[2]) + groups = [] + if len(groups) == 1: + symbols.append(groups[0]) + elif len(groups) == 2: + symbols.append(groups[0] * 3 + groups[1]) + return symbols + + +def descsum_create(s): + """Add a checksum to a descriptor without""" + symbols = descsum_expand(s) + [0, 0, 0, 0, 0, 0, 0, 0] + checksum = descsum_polymod(symbols) ^ 1 + return ( + s + + "#" + + "".join(CHECKSUM_CHARSET[(checksum >> (5 * (7 - i))) & 31] for i in range(8)) + ) + + +def descsum_check(s): + """Verify that the checksum is correct in a descriptor""" + if s[-9] != "#": + return False + if not all(x in CHECKSUM_CHARSET for x in s[-8:]): + return False + symbols = descsum_expand(s[:-9]) + [CHECKSUM_CHARSET.find(x) for x in s[-8:]] + return descsum_polymod(symbols) == 1 + + +def drop_origins(s): + """Drop the key origins from a descriptor""" + desc = re.sub(r"\[.+?\]", "", s) + if "#" in s: + desc = desc[: desc.index("#")] + return descsum_create(desc) diff --git a/bitcoin_client/ledger_bitcoin/bip380/descriptors/errors.py b/bitcoin_client/ledger_bitcoin/bip380/descriptors/errors.py new file mode 100644 index 00000000..f7b58483 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/descriptors/errors.py @@ -0,0 +1,5 @@ +class DescriptorParsingError(ValueError): + """Error while parsing a Bitcoin Output Descriptor from its string representation""" + + def __init__(self, message): + self.message = message diff --git a/bitcoin_client/ledger_bitcoin/bip380/descriptors/parsing.py b/bitcoin_client/ledger_bitcoin/bip380/descriptors/parsing.py new file mode 100644 index 00000000..1d18bffd --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/descriptors/parsing.py @@ -0,0 +1,56 @@ +from ...bip380 import descriptors +from ...bip380.key import DescriptorKey, DescriptorKeyError +from ...bip380.miniscript import Node +from ...bip380.descriptors.checksum import descsum_check + +from .errors import DescriptorParsingError + + +def split_checksum(desc_str, strict=False): + """Removes and check the provided checksum. + If not told otherwise, this won't fail on a missing checksum. + + :param strict: whether to require the presence of the checksum. + """ + desc_split = desc_str.split("#") + if len(desc_split) != 2: + if strict: + raise DescriptorParsingError("Missing checksum") + return desc_split[0] + + descriptor, checksum = desc_split + if not descsum_check(desc_str): + raise DescriptorParsingError( + f"Checksum '{checksum}' is invalid for '{descriptor}'" + ) + + return descriptor + + +def descriptor_from_str(desc_str, strict=False): + """Parse a Bitcoin Output Script Descriptor from its string representation. + + :param strict: whether to require the presence of a checksum. + """ + desc_str = split_checksum(desc_str, strict=strict) + + if desc_str.startswith("wsh(") and desc_str.endswith(")"): + # TODO: decent errors in the Miniscript module to be able to catch them here. + ms = Node.from_str(desc_str[4:-1]) + return descriptors.WshDescriptor(ms) + + if desc_str.startswith("wpkh(") and desc_str.endswith(")"): + try: + pubkey = DescriptorKey(desc_str[5:-1]) + except DescriptorKeyError as e: + raise DescriptorParsingError(str(e)) + return descriptors.WpkhDescriptor(pubkey) + + if desc_str.startswith("tr(") and desc_str.endswith(")"): + try: + pubkey = DescriptorKey(desc_str[3:-1], x_only=True) + except DescriptorKeyError as e: + raise DescriptorParsingError(str(e)) + return descriptors.TrDescriptor(pubkey) + + raise DescriptorParsingError(f"Unknown descriptor fragment: {desc_str}") diff --git a/bitcoin_client/ledger_bitcoin/bip380/descriptors/utils.py b/bitcoin_client/ledger_bitcoin/bip380/descriptors/utils.py new file mode 100644 index 00000000..25dbfe94 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/descriptors/utils.py @@ -0,0 +1,21 @@ +"""Utilities for working with descriptors.""" +import coincurve +import hashlib + + +def tagged_hash(tag, data): + ss = hashlib.sha256(tag.encode("utf-8")).digest() + ss += ss + ss += data + return hashlib.sha256(ss).digest() + + +def taproot_tweak(pubkey_bytes, merkle_root): + assert isinstance(pubkey_bytes, bytes) and len(pubkey_bytes) == 32 + assert isinstance(merkle_root, bytes) + + t = tagged_hash("TapTweak", pubkey_bytes + merkle_root) + xonly_pubkey = coincurve.PublicKeyXOnly(pubkey_bytes) + xonly_pubkey.tweak_add(t) # TODO: error handling + + return xonly_pubkey diff --git a/bitcoin_client/ledger_bitcoin/bip380/key.py b/bitcoin_client/ledger_bitcoin/bip380/key.py new file mode 100644 index 00000000..3e05b61d --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/key.py @@ -0,0 +1,338 @@ +import coincurve +import copy + +from bip32 import BIP32, HARDENED_INDEX +from bip32.utils import _deriv_path_str_to_list +from .utils.hashes import hash160 +from enum import Enum, auto + + +def is_raw_key(obj): + return isinstance(obj, (coincurve.PublicKey, coincurve.PublicKeyXOnly)) + + +class DescriptorKeyError(Exception): + def __init__(self, message): + self.message = message + + +class DescriporKeyOrigin: + """The origin of a key in a descriptor. + + See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions. + """ + + def __init__(self, fingerprint, path): + assert isinstance(fingerprint, bytes) and isinstance(path, list) + + self.fingerprint = fingerprint + self.path = path + + def from_str(origin_str): + # Origing starts and ends with brackets + if not origin_str.startswith("[") or not origin_str.endswith("]"): + raise DescriptorKeyError(f"Insane origin: '{origin_str}'") + # At least 8 hex characters + brackets + if len(origin_str) < 10: + raise DescriptorKeyError(f"Insane origin: '{origin_str}'") + + # For the fingerprint, just read the 4 bytes. + try: + fingerprint = bytes.fromhex(origin_str[1:9]) + except ValueError: + raise DescriptorKeyError(f"Insane fingerprint in origin: '{origin_str}'") + # For the path, we (how bad) reuse an internal helper from python-bip32. + path = [] + if len(origin_str) > 10: + if origin_str[9] != "/": + raise DescriptorKeyError(f"Insane path in origin: '{origin_str}'") + # The helper operates on "m/10h/11/12'/13", so give it a "m". + dummy = "m" + try: + path = _deriv_path_str_to_list(dummy + origin_str[9:-1]) + except ValueError: + raise DescriptorKeyError(f"Insane path in origin: '{origin_str}'") + + return DescriporKeyOrigin(fingerprint, path) + + +class KeyPathKind(Enum): + FINAL = auto() + WILDCARD_UNHARDENED = auto() + WILDCARD_HARDENED = auto() + + def is_wildcard(self): + return self in [KeyPathKind.WILDCARD_HARDENED, KeyPathKind.WILDCARD_UNHARDENED] + + +def parse_index(index_str): + """Parse a derivation index, as contained in a derivation path.""" + assert isinstance(index_str, str) + + try: + # if HARDENED + if index_str[-1:] in ["'", "h", "H"]: + return int(index_str[:-1]) + HARDENED_INDEX + else: + return int(index_str) + except ValueError as e: + raise DescriptorKeyError(f"Invalid derivation index {index_str}: '{e}'") + + +class DescriptorKeyPath: + """The derivation path of a key in a descriptor. + + See https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions + as well as BIP389 for multipath expressions. + """ + + def __init__(self, paths, kind): + assert ( + isinstance(paths, list) + and isinstance(kind, KeyPathKind) + and len(paths) > 0 + and all(isinstance(p, list) for p in paths) + ) + + self.paths = paths + self.kind = kind + + def is_multipath(self): + """Whether this derivation path actually contains multiple of them.""" + return len(self.paths) > 1 + + def from_str(path_str): + if len(path_str) < 2: + raise DescriptorKeyError(f"Insane key path: '{path_str}'") + if path_str[0] != "/": + raise DescriptorKeyError(f"Insane key path: '{path_str}'") + + # Determine whether this key may be derived. + kind = KeyPathKind.FINAL + if len(path_str) > 2 and path_str[-3:] in ["/*'", "/*h", "/*H"]: + kind = KeyPathKind.WILDCARD_HARDENED + path_str = path_str[:-3] + elif len(path_str) > 1 and path_str[-2:] == "/*": + kind = KeyPathKind.WILDCARD_UNHARDENED + path_str = path_str[:-2] + + paths = [[]] + if len(path_str) == 0: + return DescriptorKeyPath(paths, kind) + + for index in path_str[1:].split("/"): + # If this is a multipath expression, of the form '' + if ( + index.startswith("<") + and index.endswith(">") + and ";" in index + and len(index) >= 5 + ): + # Can't have more than one multipath expression + if len(paths) > 1: + raise DescriptorKeyError( + f"May only have a single multipath step in derivation path: '{path_str}'" + ) + indexes = index[1:-1].split(";") + paths = [copy.copy(paths[0]) for _ in indexes] + for i, der_index in enumerate(indexes): + paths[i].append(parse_index(der_index)) + else: + # This is a "single index" expression. + for path in paths: + path.append(parse_index(index)) + return DescriptorKeyPath(paths, kind) + + +class DescriptorKey: + """A Bitcoin key to be used in Output Script Descriptors. + + May be an extended or raw public key. + """ + + def __init__(self, key, x_only=False): + # Information about the origin of this key. + self.origin = None + # If it is an xpub, a path toward a child key of that xpub. + self.path = None + # Whether to only create x-only public keys. + self.x_only = x_only + # Whether to serialize to string representation without the sign byte. + # This is necessary to roundtrip 33-bytes keys under Taproot context. + self.ser_x_only = x_only + + if isinstance(key, bytes): + if len(key) == 32: + key_cls = coincurve.PublicKeyXOnly + self.x_only = True + self.ser_x_only = True + elif len(key) == 33: + key_cls = coincurve.PublicKey + self.ser_x_only = False + else: + raise DescriptorKeyError( + "Only compressed and x-only keys are supported" + ) + try: + self.key = key_cls(key) + except ValueError as e: + raise DescriptorKeyError(f"Public key parsing error: '{str(e)}'") + + elif isinstance(key, BIP32): + self.key = key + + elif isinstance(key, str): + # Try parsing an optional origin prepended to the key + splitted_key = key.split("]", maxsplit=1) + if len(splitted_key) == 2: + origin, key = splitted_key + self.origin = DescriporKeyOrigin.from_str(origin + "]") + + # Is it a raw key? + if len(key) in (64, 66): + pk_cls = coincurve.PublicKey + if len(key) == 64: + pk_cls = coincurve.PublicKeyXOnly + self.x_only = True + self.ser_x_only = True + else: + self.ser_x_only = False + try: + self.key = pk_cls(bytes.fromhex(key)) + except ValueError as e: + raise DescriptorKeyError(f"Public key parsing error: '{str(e)}'") + # If not it must be an xpub. + else: + # There may be an optional path appended to the xpub. + splitted_key = key.split("/", maxsplit=1) + if len(splitted_key) == 2: + key, path = splitted_key + self.path = DescriptorKeyPath.from_str("/" + path) + + try: + self.key = BIP32.from_xpub(key) + except ValueError as e: + raise DescriptorKeyError(f"Xpub parsing error: '{str(e)}'") + + else: + raise DescriptorKeyError( + "Invalid parameter type: expecting bytes, hex str or BIP32 instance." + ) + + def __repr__(self): + key = "" + + def ser_index(key, der_index): + # If this a hardened step, deduce the threshold and mark it. + if der_index < HARDENED_INDEX: + return str(der_index) + else: + return f"{der_index - 2**31}'" + + def ser_paths(key, paths): + assert len(paths) > 0 + + for i, der_index in enumerate(paths[0]): + # If this is a multipath expression, write the multi-index step accordingly + if len(paths) > 1 and paths[1][i] != der_index: + key += "/<" + for j, path in enumerate(paths): + key += ser_index(key, path[i]) + if j < len(paths) - 1: + key += ";" + key += ">" + else: + key += "/" + ser_index(key, der_index) + + return key + + if self.origin is not None: + key += f"[{self.origin.fingerprint.hex()}" + key = ser_paths(key, [self.origin.path]) + key += "]" + + if isinstance(self.key, BIP32): + key += self.key.get_xpub() + else: + assert is_raw_key(self.key) + raw_key = self.key.format() + if len(raw_key) == 33 and self.ser_x_only: + raw_key = raw_key[1:] + key += raw_key.hex() + + if self.path is not None: + key = ser_paths(key, self.path.paths) + if self.path.kind == KeyPathKind.WILDCARD_UNHARDENED: + key += "/*" + elif self.path.kind == KeyPathKind.WILDCARD_HARDENED: + key += "/*'" + + return key + + def is_multipath(self): + """Whether this key contains more than one derivation path.""" + return self.path is not None and self.path.is_multipath() + + def derivation_path(self): + """Get the single derivation path for this key. + + Will raise if it has multiple, and return None if it doesn't have any. + """ + if self.path is None: + return None + if self.path.is_multipath(): + raise DescriptorKeyError( + f"Key has multiple derivation paths: {self.path.paths}" + ) + return self.path.paths[0] + + def bytes(self): + """Get this key as raw bytes. + + Will raise if this key contains multiple derivation paths. + """ + if is_raw_key(self.key): + raw = self.key.format() + if self.x_only and len(raw) == 33: + return raw[1:] + assert len(raw) == 32 or not self.x_only + return raw + else: + assert isinstance(self.key, BIP32) + path = self.derivation_path() + if path is None: + return self.key.pubkey + assert not self.path.kind.is_wildcard() # TODO: real errors + return self.key.get_pubkey_from_path(path) + + def derive(self, index): + """Derive the key at the given index. + + Will raise if this key contains multiple derivation paths. + A no-op if the key isn't a wildcard. Will start from 2**31 if the key is a "hardened + wildcard". + """ + assert isinstance(index, int) + if ( + self.path is None + or self.path.is_multipath() + or self.path.kind == KeyPathKind.FINAL + ): + return + assert isinstance(self.key, BIP32) + + if self.path.kind == KeyPathKind.WILDCARD_HARDENED: + index += 2 ** 31 + assert index < 2 ** 32 + + if self.origin is None: + fingerprint = hash160(self.key.pubkey)[:4] + self.origin = DescriporKeyOrigin(fingerprint, [index]) + else: + self.origin.path.append(index) + + # This can't fail now. + path = self.derivation_path() + # TODO(bip32): have a way to derive without roundtripping through string ser. + self.key = BIP32.from_xpub(self.key.get_xpub_from_path(path + [index])) + self.path = None diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/__init__.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/__init__.py new file mode 100644 index 00000000..b0de1f9c --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/__init__.py @@ -0,0 +1,13 @@ +""" +Miniscript +========== + +Miniscript is an extension to Bitcoin Output Script descriptors. It is a language for \ +writing (a subset of) Bitcoin Scripts in a structured way, enabling analysis, composition, \ +generic signing and more. + +For more information about Miniscript, see https://bitcoin.sipa.be/miniscript. +""" + +from .fragments import Node +from .satisfaction import SatisfactionMaterial diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/errors.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/errors.py new file mode 100644 index 00000000..7ccd98f4 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/errors.py @@ -0,0 +1,20 @@ +""" +All the exceptions raised when dealing with Miniscript. +""" + + +class MiniscriptMalformed(ValueError): + def __init__(self, message): + self.message = message + + +class MiniscriptNodeCreationError(ValueError): + def __init__(self, message): + self.message = message + + +class MiniscriptPropertyError(ValueError): + def __init__(self, message): + self.message = message + +# TODO: errors for type errors, parsing errors, etc.. diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/fragments.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/fragments.py new file mode 100644 index 00000000..d0e572ee --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/fragments.py @@ -0,0 +1,1225 @@ +""" +Miniscript AST elements. + +Each element correspond to a Bitcoin Script fragment, and has various type properties. +See the Miniscript website for the specification of the type system: https://bitcoin.sipa.be/miniscript/. +""" + +import copy +from ...bip380.miniscript import parsing + +from ...bip380.key import DescriptorKey +from ...bip380.utils.hashes import hash160 +from ...bip380.utils.script import ( + CScript, + OP_1, + OP_0, + OP_ADD, + OP_BOOLAND, + OP_BOOLOR, + OP_DUP, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_EQUALVERIFY, + OP_FROMALTSTACK, + OP_IFDUP, + OP_IF, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKMULTISIG, + OP_CHECKMULTISIGVERIFY, + OP_CHECKSEQUENCEVERIFY, + OP_CHECKSIG, + OP_CHECKSIGVERIFY, + OP_HASH160, + OP_HASH256, + OP_NOTIF, + OP_RIPEMD160, + OP_SHA256, + OP_SIZE, + OP_SWAP, + OP_TOALTSTACK, + OP_VERIFY, + OP_0NOTEQUAL, +) + +from .errors import MiniscriptNodeCreationError +from .property import Property +from .satisfaction import ExecutionInfo, Satisfaction + + +# Threshold for nLockTime: below this value it is interpreted as block number, +# otherwise as UNIX timestamp. +LOCKTIME_THRESHOLD = 500000000 # Tue Nov 5 00:53:20 1985 UTC + +# If CTxIn::nSequence encodes a relative lock-time and this flag +# is set, the relative lock-time has units of 512 seconds, +# otherwise it specifies blocks with a granularity of 1. +SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22 + + +class Node: + """A Miniscript fragment.""" + + # The fragment's type and properties + p = None + # List of all sub fragments + subs = [] + # A list of Script elements, a CScript is created all at once in the script() method. + _script = [] + # Whether any satisfaction for this fragment require a signature + needs_sig = None + # Whether any dissatisfaction for this fragment requires a signature + is_forced = None + # Whether this fragment has a unique unconditional satisfaction, and all conditional + # ones require a signature. + is_expressive = None + # Whether for any possible way to satisfy this fragment (may be none), a + # non-malleable satisfaction exists. + is_nonmalleable = None + # Whether this node or any of its subs contains an absolute heightlock + abs_heightlocks = None + # Whether this node or any of its subs contains a relative heightlock + rel_heightlocks = None + # Whether this node or any of its subs contains an absolute timelock + abs_timelocks = None + # Whether this node or any of its subs contains a relative timelock + rel_timelocks = None + # Whether this node does not contain a mix of timelock or heightlock of different types. + # That is, not (abs_heightlocks and rel_heightlocks or abs_timelocks and abs_timelocks) + no_timelock_mix = None + # Information about this Miniscript execution (satisfaction cost, etc..) + exec_info = None + + def __init__(self, *args, **kwargs): + # Needs to be implemented by derived classes. + raise NotImplementedError + + def from_str(ms_str): + """Parse a Miniscript fragment from its string representation.""" + assert isinstance(ms_str, str) + return parsing.miniscript_from_str(ms_str) + + def from_script(script, pkh_preimages={}): + """Decode a Miniscript fragment from its Script representation.""" + assert isinstance(script, CScript) + return parsing.miniscript_from_script(script, pkh_preimages) + + # TODO: have something like BuildScript from Core and get rid of the _script member. + @property + def script(self): + return CScript(self._script) + + @property + def keys(self): + """Get the list of all keys from this Miniscript, in order of apparition.""" + # Overriden by fragments that actually have keys. + return [key for sub in self.subs for key in sub.keys] + + def satisfy(self, sat_material): + """Get the witness of the smallest non-malleable satisfaction for this fragment, + if one exists. + + :param sat_material: a SatisfactionMaterial containing available data to satisfy + challenges. + """ + sat = self.satisfaction(sat_material) + if not sat.has_sig: + return None + return sat.witness + + def satisfaction(self, sat_material): + """Get the satisfaction for this fragment. + + :param sat_material: a SatisfactionMaterial containing available data to satisfy + challenges. + """ + # Needs to be implemented by derived classes. + raise NotImplementedError + + def dissatisfaction(self): + """Get the dissatisfaction for this fragment.""" + # Needs to be implemented by derived classes. + raise NotImplementedError + + +class Just0(Node): + def __init__(self): + + self._script = [OP_0] + + self.p = Property("Bzud") + self.needs_sig = False + self.is_forced = False + self.is_expressive = True + self.is_nonmalleable = True + self.abs_heightlocks = False + self.rel_heightlocks = False + self.abs_timelocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(0, 0, None, 0) + + def satisfaction(self, sat_material): + return Satisfaction.unavailable() + + def dissatisfaction(self): + return Satisfaction(witness=[]) + + def __repr__(self): + return "0" + + +class Just1(Node): + def __init__(self): + + self._script = [OP_1] + + self.p = Property("Bzu") + self.needs_sig = False + self.is_forced = True # No dissat + self.is_expressive = False # No dissat + self.is_nonmalleable = True # FIXME: how comes? Standardness rules? + self.abs_heightlocks = False + self.rel_heightlocks = False + self.abs_timelocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(0, 0, 0, None) + + def satisfaction(self, sat_material): + return Satisfaction(witness=[]) + + def dissatisfaction(self): + return Satisfaction.unavailable() + + def __repr__(self): + return "1" + + +class PkNode(Node): + """A virtual class for nodes containing a single public key. + + Should not be instanced directly, use Pk() or Pkh(). + """ + + def __init__(self, pubkey): + + if isinstance(pubkey, bytes) or isinstance(pubkey, str): + self.pubkey = DescriptorKey(pubkey) + elif isinstance(pubkey, DescriptorKey): + self.pubkey = pubkey + else: + raise MiniscriptNodeCreationError("Invalid public key") + + self.needs_sig = True # FIXME: think about having it in 'c:' instead + self.is_forced = False + self.is_expressive = True + self.is_nonmalleable = True + self.abs_heightlocks = False + self.rel_heightlocks = False + self.abs_timelocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + + @property + def keys(self): + return [self.pubkey] + + +class Pk(PkNode): + def __init__(self, pubkey): + PkNode.__init__(self, pubkey) + + self.p = Property("Konud") + self.exec_info = ExecutionInfo(0, 0, 0, 0) + + @property + def _script(self): + return [self.pubkey.bytes()] + + def satisfaction(self, sat_material): + sig = sat_material.signatures.get(self.pubkey.bytes()) + if sig is None: + return Satisfaction.unavailable() + return Satisfaction([sig], has_sig=True) + + def dissatisfaction(self): + return Satisfaction(witness=[b""]) + + def __repr__(self): + return f"pk_k({self.pubkey})" + + +class Pkh(PkNode): + # FIXME: should we support a hash here, like rust-bitcoin? I don't think it's safe. + def __init__(self, pubkey): + PkNode.__init__(self, pubkey) + + self.p = Property("Knud") + self.exec_info = ExecutionInfo(3, 0, 1, 1) + + @property + def _script(self): + return [OP_DUP, OP_HASH160, self.pk_hash(), OP_EQUALVERIFY] + + def satisfaction(self, sat_material): + sig = sat_material.signatures.get(self.pubkey.bytes()) + if sig is None: + return Satisfaction.unavailable() + return Satisfaction(witness=[sig, self.pubkey.bytes()], has_sig=True) + + def dissatisfaction(self): + return Satisfaction(witness=[b"", self.pubkey.bytes()]) + + def __repr__(self): + return f"pk_h({self.pubkey})" + + def pk_hash(self): + assert isinstance(self.pubkey, DescriptorKey) + return hash160(self.pubkey.bytes()) + + +class Older(Node): + def __init__(self, value): + assert value > 0 and value < 2 ** 31 + + self.value = value + self._script = [self.value, OP_CHECKSEQUENCEVERIFY] + + self.p = Property("Bz") + self.needs_sig = False + self.is_forced = True + self.is_expressive = False # No dissat + self.is_nonmalleable = True + self.rel_timelocks = bool(value & SEQUENCE_LOCKTIME_TYPE_FLAG) + self.rel_heightlocks = not self.rel_timelocks + self.abs_heightlocks = False + self.abs_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(1, 0, 0, None) + + def satisfaction(self, sat_material): + if sat_material.max_sequence < self.value: + return Satisfaction.unavailable() + return Satisfaction(witness=[]) + + def dissatisfaction(self): + return Satisfaction.unavailable() + + def __repr__(self): + return f"older({self.value})" + + +class After(Node): + def __init__(self, value): + assert value > 0 and value < 2 ** 31 + + self.value = value + self._script = [self.value, OP_CHECKLOCKTIMEVERIFY] + + self.p = Property("Bz") + self.needs_sig = False + self.is_forced = True + self.is_expressive = False # No dissat + self.is_nonmalleable = True + self.abs_heightlocks = value < LOCKTIME_THRESHOLD + self.abs_timelocks = not self.abs_heightlocks + self.rel_heightlocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(1, 0, 0, None) + + def satisfaction(self, sat_material): + if sat_material.max_lock_time < self.value: + return Satisfaction.unavailable() + return Satisfaction(witness=[]) + + def dissatisfaction(self): + return Satisfaction.unavailable() + + def __repr__(self): + return f"after({self.value})" + + +class HashNode(Node): + """A virtual class for fragments with hashlock semantics. + + Should not be instanced directly, use concrete fragments instead. + """ + + def __init__(self, digest, hash_op): + assert isinstance(digest, bytes) # TODO: real errors + + self.digest = digest + self._script = [OP_SIZE, 32, OP_EQUALVERIFY, hash_op, digest, OP_EQUAL] + + self.p = Property("Bonud") + self.needs_sig = False + self.is_forced = False + self.is_expressive = False + self.is_nonmalleable = True + self.abs_heightlocks = False + self.rel_heightlocks = False + self.abs_timelocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(4, 0, 1, None) + + def satisfaction(self, sat_material): + preimage = sat_material.preimages.get(self.digest) + if preimage is None: + return Satisfaction.unavailable() + return Satisfaction(witness=[preimage]) + + def dissatisfaction(self): + return Satisfaction.unavailable() + return Satisfaction(witness=[b""]) + + +class Sha256(HashNode): + def __init__(self, digest): + assert len(digest) == 32 # TODO: real errors + HashNode.__init__(self, digest, OP_SHA256) + + def __repr__(self): + return f"sha256({self.digest.hex()})" + + +class Hash256(HashNode): + def __init__(self, digest): + assert len(digest) == 32 # TODO: real errors + HashNode.__init__(self, digest, OP_HASH256) + + def __repr__(self): + return f"hash256({self.digest.hex()})" + + +class Ripemd160(HashNode): + def __init__(self, digest): + assert len(digest) == 20 # TODO: real errors + HashNode.__init__(self, digest, OP_RIPEMD160) + + def __repr__(self): + return f"ripemd160({self.digest.hex()})" + + +class Hash160(HashNode): + def __init__(self, digest): + assert len(digest) == 20 # TODO: real errors + HashNode.__init__(self, digest, OP_HASH160) + + def __repr__(self): + return f"hash160({self.digest.hex()})" + + +class Multi(Node): + def __init__(self, k, keys): + assert 1 <= k <= len(keys) + assert all(isinstance(k, DescriptorKey) for k in keys) + + self.k = k + self.pubkeys = keys + + self.p = Property("Bndu") + self.needs_sig = True + self.is_forced = False + self.is_expressive = True + self.is_nonmalleable = True + self.abs_heightlocks = False + self.rel_heightlocks = False + self.abs_timelocks = False + self.rel_timelocks = False + self.no_timelock_mix = True + self.exec_info = ExecutionInfo(1, len(keys), 1 + k, 1 + k) + + @property + def keys(self): + return self.pubkeys + + @property + def _script(self): + return [ + self.k, + *[k.bytes() for k in self.keys], + len(self.keys), + OP_CHECKMULTISIG, + ] + + def satisfaction(self, sat_material): + sigs = [] + for key in self.keys: + sig = sat_material.signatures.get(key.bytes()) + if sig is not None: + assert isinstance(sig, bytes) + sigs.append(sig) + if len(sigs) == self.k: + break + if len(sigs) < self.k: + return Satisfaction.unavailable() + return Satisfaction(witness=[b""] + sigs, has_sig=True) + + def dissatisfaction(self): + return Satisfaction(witness=[b""] * (self.k + 1)) + + def __repr__(self): + return f"multi({','.join([str(self.k)] + [str(k) for k in self.keys])})" + + +class AndV(Node): + def __init__(self, sub_x, sub_y): + assert sub_x.p.V + assert sub_y.p.has_any("BKV") + + self.subs = [sub_x, sub_y] + + self.p = Property( + sub_y.p.type() + + ("z" if sub_x.p.z and sub_y.p.z else "") + + ("o" if sub_x.p.z and sub_y.p.o or sub_x.p.o and sub_y.p.z else "") + + ("n" if sub_x.p.n or sub_x.p.z and sub_y.p.n else "") + + ("u" if sub_y.p.u else "") + ) + self.needs_sig = any(sub.needs_sig for sub in self.subs) + self.is_forced = any(sub.needs_sig for sub in self.subs) + self.is_expressive = False # Not 'd' + self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = not ( + self.abs_heightlocks + and self.abs_timelocks + or self.rel_heightlocks + and self.rel_timelocks + ) + + @property + def _script(self): + return sum((sub._script for sub in self.subs), start=[]) + + @property + def exec_info(self): + exec_info = ExecutionInfo.from_concat( + self.subs[0].exec_info, self.subs[1].exec_info + ) + exec_info.set_undissatisfiable() # it's V. + return exec_info + + def satisfaction(self, sat_material): + return Satisfaction.from_concat(sat_material, *self.subs) + + def dissatisfaction(self): + return Satisfaction.unavailable() # it's V. + + def __repr__(self): + return f"and_v({','.join(map(str, self.subs))})" + + +class AndB(Node): + def __init__(self, sub_x, sub_y): + assert sub_x.p.B and sub_y.p.W + + self.subs = [sub_x, sub_y] + + self.p = Property( + "Bu" + + ("z" if sub_x.p.z and sub_y.p.z else "") + + ("o" if sub_x.p.z and sub_y.p.o or sub_x.p.o and sub_y.p.z else "") + + ("n" if sub_x.p.n or sub_x.p.z and sub_y.p.n else "") + + ("d" if sub_x.p.d and sub_y.p.d else "") + + ("u" if sub_y.p.u else "") + ) + self.needs_sig = any(sub.needs_sig for sub in self.subs) + self.is_forced = ( + sub_x.is_forced + and sub_y.is_forced + or any(sub.is_forced and sub.needs_sig for sub in self.subs) + ) + self.is_expressive = all(sub.is_forced and sub.needs_sig for sub in self.subs) + self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = not ( + self.abs_heightlocks + and self.abs_timelocks + or self.rel_heightlocks + and self.rel_timelocks + ) + + @property + def _script(self): + return sum((sub._script for sub in self.subs), start=[]) + [OP_BOOLAND] + + @property + def exec_info(self): + return ExecutionInfo.from_concat( + self.subs[0].exec_info, self.subs[1].exec_info, ops_count=1 + ) + + def satisfaction(self, sat_material): + return Satisfaction.from_concat(sat_material, self.subs[0], self.subs[1]) + + def dissatisfaction(self): + return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() + + def __repr__(self): + return f"and_b({','.join(map(str, self.subs))})" + + +class OrB(Node): + def __init__(self, sub_x, sub_z): + assert sub_x.p.has_all("Bd") + assert sub_z.p.has_all("Wd") + + self.subs = [sub_x, sub_z] + + self.p = Property( + "Bdu" + + ("z" if sub_x.p.z and sub_z.p.z else "") + + ("o" if sub_x.p.z and sub_z.p.o or sub_x.p.o and sub_z.p.z else "") + ) + self.needs_sig = all(sub.needs_sig for sub in self.subs) + self.is_forced = False # Both subs are 'd' + self.is_expressive = all(sub.is_expressive for sub in self.subs) + self.is_nonmalleable = all( + sub.is_nonmalleable and sub.is_expressive for sub in self.subs + ) and any(sub.needs_sig for sub in self.subs) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) + + @property + def _script(self): + return sum((sub._script for sub in self.subs), start=[]) + [OP_BOOLOR] + + @property + def exec_info(self): + return ExecutionInfo.from_concat( + self.subs[0].exec_info, + self.subs[1].exec_info, + ops_count=1, + disjunction=True, + ) + + def satisfaction(self, sat_material): + return Satisfaction.from_concat( + sat_material, self.subs[0], self.subs[1], disjunction=True + ) + + def dissatisfaction(self): + return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() + + def __repr__(self): + return f"or_b({','.join(map(str, self.subs))})" + + +class OrC(Node): + def __init__(self, sub_x, sub_z): + assert sub_x.p.has_all("Bdu") and sub_z.p.V + + self.subs = [sub_x, sub_z] + + self.p = Property( + "V" + + ("z" if sub_x.p.z and sub_z.p.z else "") + + ("o" if sub_x.p.o and sub_z.p.z else "") + ) + self.needs_sig = all(sub.needs_sig for sub in self.subs) + self.is_forced = True # Because sub_z is 'V' + self.is_expressive = False # V + self.is_nonmalleable = ( + all(sub.is_nonmalleable for sub in self.subs) + and any(sub.needs_sig for sub in self.subs) + and sub_x.is_expressive + ) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) + + @property + def _script(self): + return self.subs[0]._script + [OP_NOTIF] + self.subs[1]._script + [OP_ENDIF] + + @property + def exec_info(self): + exec_info = ExecutionInfo.from_or_uneven( + self.subs[0].exec_info, self.subs[1].exec_info, ops_count=2 + ) + exec_info.set_undissatisfiable() # it's V. + return exec_info + + def satisfaction(self, sat_material): + return Satisfaction.from_or_uneven(sat_material, self.subs[0], self.subs[1]) + + def dissatisfaction(self): + return Satisfaction.unavailable() # it's V. + + def __repr__(self): + return f"or_c({','.join(map(str, self.subs))})" + + +class OrD(Node): + def __init__(self, sub_x, sub_z): + assert sub_x.p.has_all("Bdu") + assert sub_z.p.has_all("B") + + self.subs = [sub_x, sub_z] + + self.p = Property( + "B" + + ("z" if sub_x.p.z and sub_z.p.z else "") + + ("o" if sub_x.p.o and sub_z.p.z else "") + + ("d" if sub_z.p.d else "") + + ("u" if sub_z.p.u else "") + ) + self.needs_sig = all(sub.needs_sig for sub in self.subs) + self.is_forced = all(sub.is_forced for sub in self.subs) + self.is_expressive = all(sub.is_expressive for sub in self.subs) + self.is_nonmalleable = ( + all(sub.is_nonmalleable for sub in self.subs) + and any(sub.needs_sig for sub in self.subs) + and sub_x.is_expressive + ) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) + + @property + def _script(self): + return ( + self.subs[0]._script + + [OP_IFDUP, OP_NOTIF] + + self.subs[1]._script + + [OP_ENDIF] + ) + + @property + def exec_info(self): + return ExecutionInfo.from_or_uneven( + self.subs[0].exec_info, self.subs[1].exec_info, ops_count=3 + ) + + def satisfaction(self, sat_material): + return Satisfaction.from_or_uneven(sat_material, self.subs[0], self.subs[1]) + + def dissatisfaction(self): + return self.subs[1].dissatisfaction() + self.subs[0].dissatisfaction() + + def __repr__(self): + return f"or_d({','.join(map(str, self.subs))})" + + +class OrI(Node): + def __init__(self, sub_x, sub_z): + assert sub_x.p.type() == sub_z.p.type() and sub_x.p.has_any("BKV") + + self.subs = [sub_x, sub_z] + + self.p = Property( + sub_x.p.type() + + ("o" if sub_x.p.z and sub_z.p.z else "") + + ("d" if sub_x.p.d or sub_z.p.d else "") + + ("u" if sub_x.p.u and sub_z.p.u else "") + ) + self.needs_sig = all(sub.needs_sig for sub in self.subs) + self.is_forced = all(sub.is_forced for sub in self.subs) + self.is_expressive = ( + sub_x.is_expressive + and sub_z.is_forced + or sub_x.is_forced + and sub_z.is_expressive + ) + self.is_nonmalleable = all(sub.is_nonmalleable for sub in self.subs) and any( + sub.needs_sig for sub in self.subs + ) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) + + @property + def _script(self): + return ( + [OP_IF] + + self.subs[0]._script + + [OP_ELSE] + + self.subs[1]._script + + [OP_ENDIF] + ) + + @property + def exec_info(self): + return ExecutionInfo.from_or_even( + self.subs[0].exec_info, self.subs[1].exec_info, ops_count=3 + ) + + def satisfaction(self, sat_material): + return (self.subs[0].satisfaction(sat_material) + Satisfaction([b"\x01"])) | ( + self.subs[1].satisfaction(sat_material) + Satisfaction([b""]) + ) + + def dissatisfaction(self): + return (self.subs[0].dissatisfaction() + Satisfaction(witness=[b"\x01"])) | ( + self.subs[1].dissatisfaction() + Satisfaction(witness=[b""]) + ) + + def __repr__(self): + return f"or_i({','.join(map(str, self.subs))})" + + +class AndOr(Node): + def __init__(self, sub_x, sub_y, sub_z): + assert sub_x.p.has_all("Bdu") + assert sub_y.p.type() == sub_z.p.type() and sub_y.p.has_any("BKV") + + self.subs = [sub_x, sub_y, sub_z] + + self.p = Property( + sub_y.p.type() + + ("z" if sub_x.p.z and sub_y.p.z and sub_z.p.z else "") + + ( + "o" + if sub_x.p.z + and sub_y.p.o + and sub_z.p.o + or sub_x.p.o + and sub_y.p.z + and sub_z.p.z + else "" + ) + + ("d" if sub_z.p.d else "") + + ("u" if sub_y.p.u and sub_z.p.u else "") + ) + self.needs_sig = sub_x.needs_sig and (sub_y.needs_sig or sub_z.needs_sig) + self.is_forced = sub_z.is_forced and (sub_x.needs_sig or sub_y.is_forced) + self.is_expressive = ( + sub_x.is_expressive + and sub_z.is_expressive + and (sub_x.needs_sig or sub_y.is_forced) + ) + self.is_nonmalleable = ( + all(sub.is_nonmalleable for sub in self.subs) + and any(sub.needs_sig for sub in self.subs) + and sub_x.is_expressive + ) + self.abs_heightlocks = any(sub.abs_heightlocks for sub in self.subs) + self.rel_heightlocks = any(sub.rel_heightlocks for sub in self.subs) + self.abs_timelocks = any(sub.abs_timelocks for sub in self.subs) + self.rel_timelocks = any(sub.rel_timelocks for sub in self.subs) + # X and Y, or Z. So we have a mix if any contain a timelock mix, or + # there is a mix between X and Y. + self.no_timelock_mix = all(sub.no_timelock_mix for sub in self.subs) and not ( + any(sub.rel_timelocks for sub in [sub_x, sub_y]) + and any(sub.rel_heightlocks for sub in [sub_x, sub_y]) + or any(sub.abs_timelocks for sub in [sub_x, sub_y]) + and any(sub.abs_heightlocks for sub in [sub_x, sub_y]) + ) + + @property + def _script(self): + return ( + self.subs[0]._script + + [OP_NOTIF] + + self.subs[2]._script + + [OP_ELSE] + + self.subs[1]._script + + [OP_ENDIF] + ) + + @property + def exec_info(self): + return ExecutionInfo.from_andor_uneven( + self.subs[0].exec_info, + self.subs[1].exec_info, + self.subs[2].exec_info, + ops_count=3, + ) + + def satisfaction(self, sat_material): + # (A and B) or (!A and C) + return ( + self.subs[1].satisfaction(sat_material) + + self.subs[0].satisfaction(sat_material) + ) | (self.subs[2].satisfaction(sat_material) + self.subs[0].dissatisfaction()) + + def dissatisfaction(self): + # Dissatisfy X and Z + return self.subs[2].dissatisfaction() + self.subs[0].dissatisfaction() + + def __repr__(self): + return f"andor({','.join(map(str, self.subs))})" + + +class AndN(AndOr): + def __init__(self, sub_x, sub_y): + AndOr.__init__(self, sub_x, sub_y, Just0()) + + def __repr__(self): + return f"and_n({self.subs[0]},{self.subs[1]})" + + +class Thresh(Node): + def __init__(self, k, subs): + n = len(subs) + assert 1 <= k <= n + + self.k = k + self.subs = subs + + all_z = True + all_z_but_one_odu = False + all_e = True + all_m = True + s_count = 0 + # If k == 1, just check each child for k + if k > 1: + self.abs_heightlocks = subs[0].abs_heightlocks + self.rel_heightlocks = subs[0].rel_heightlocks + self.abs_timelocks = subs[0].abs_timelocks + self.rel_timelocks = subs[0].rel_timelocks + else: + self.no_timelock_mix = True + + assert subs[0].p.has_all("Bdu") + for sub in subs[1:]: + assert sub.p.has_all("Wdu") + if not sub.p.z: + if all_z_but_one_odu: + # Fails "all 'z' but one" + all_z_but_one_odu = False + if all_z and sub.p.has_all("odu"): + # They were all 'z' up to now. + all_z_but_one_odu = True + all_z = False + all_e = all_e and sub.is_expressive + all_m = all_m and sub.is_nonmalleable + if sub.needs_sig: + s_count += 1 + if k > 1: + self.abs_heightlocks |= sub.abs_heightlocks + self.rel_heightlocks |= sub.rel_heightlocks + self.abs_timelocks |= sub.abs_timelocks + self.rel_timelocks |= sub.rel_timelocks + else: + self.no_timelock_mix &= sub.no_timelock_mix + + self.p = Property( + "Bdu" + ("z" if all_z else "") + ("o" if all_z_but_one_odu else "") + ) + self.needs_sig = s_count >= n - k + self.is_forced = False # All subs need to be 'd' + self.is_expressive = all_e and s_count == n + self.is_nonmalleable = all_e and s_count >= n - k + if k > 1: + self.no_timelock_mix = not ( + self.abs_heightlocks + and self.abs_timelocks + or self.rel_heightlocks + and self.rel_timelocks + ) + + @property + def _script(self): + return ( + self.subs[0]._script + + sum(((sub._script + [OP_ADD]) for sub in self.subs[1:]), start=[]) + + [self.k, OP_EQUAL] + ) + + @property + def exec_info(self): + return ExecutionInfo.from_thresh(self.k, [sub.exec_info for sub in self.subs]) + + def satisfaction(self, sat_material): + return Satisfaction.from_thresh(sat_material, self.k, self.subs) + + def dissatisfaction(self): + return sum( + [sub.dissatisfaction() for sub in self.subs], start=Satisfaction(witness=[]) + ) + + def __repr__(self): + return f"thresh({self.k},{','.join(map(str, self.subs))})" + + +class WrapperNode(Node): + """A virtual base class for wrappers. + + Don't instanciate it directly, use concret wrapper fragments instead. + """ + + def __init__(self, sub): + self.subs = [sub] + + # Properties for most wrappers are directly inherited. When it's not, they + # are overriden in the fragment's __init__. + self.needs_sig = sub.needs_sig + self.is_forced = sub.is_forced + self.is_expressive = sub.is_expressive + self.is_nonmalleable = sub.is_nonmalleable + self.abs_heightlocks = sub.abs_heightlocks + self.rel_heightlocks = sub.rel_heightlocks + self.abs_timelocks = sub.abs_timelocks + self.rel_timelocks = sub.rel_timelocks + self.no_timelock_mix = not ( + self.abs_heightlocks + and self.abs_timelocks + or self.rel_heightlocks + and self.rel_timelocks + ) + + @property + def sub(self): + # Wrapper have a single sub + return self.subs[0] + + def satisfaction(self, sat_material): + # Most wrappers are satisfied this way, for special cases it's overriden. + return self.subs[0].satisfaction(sat_material) + + def dissatisfaction(self): + # Most wrappers are satisfied this way, for special cases it's overriden. + return self.subs[0].dissatisfaction() + + def skip_colon(self): + # We need to check this because of the pk() and pkh() aliases. + if isinstance(self.subs[0], WrapC) and isinstance( + self.subs[0].subs[0], (Pk, Pkh) + ): + return False + return isinstance(self.subs[0], WrapperNode) + + +class WrapA(WrapperNode): + def __init__(self, sub): + assert sub.p.B + WrapperNode.__init__(self, sub) + + self.p = Property("W" + "".join(c for c in "ud" if getattr(sub.p, c))) + + @property + def _script(self): + return [OP_TOALTSTACK] + self.sub._script + [OP_FROMALTSTACK] + + @property + def exec_info(self): + return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=2) + + def __repr__(self): + # Don't duplicate colons + if self.skip_colon(): + return f"a{self.subs[0]}" + return f"a:{self.subs[0]}" + + +class WrapS(WrapperNode): + def __init__(self, sub): + assert sub.p.has_all("Bo") + WrapperNode.__init__(self, sub) + + self.p = Property("W" + "".join(c for c in "ud" if getattr(sub.p, c))) + + @property + def _script(self): + return [OP_SWAP] + self.sub._script + + @property + def exec_info(self): + return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"s{self.subs[0]}" + return f"s:{self.subs[0]}" + + +class WrapC(WrapperNode): + def __init__(self, sub): + assert sub.p.K + WrapperNode.__init__(self, sub) + + # FIXME: shouldn't n and d be default props on the website? + self.p = Property("Bu" + "".join(c for c in "dno" if getattr(sub.p, c))) + + @property + def _script(self): + return self.sub._script + [OP_CHECKSIG] + + @property + def exec_info(self): + # FIXME: should need_sig be set to True here instead of in keys? + return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1, sat=1, dissat=1) + + def __repr__(self): + # Special case of aliases + if isinstance(self.subs[0], Pk): + return f"pk({self.subs[0].pubkey})" + if isinstance(self.subs[0], Pkh): + return f"pkh({self.subs[0].pubkey})" + # Avoid duplicating colons + if self.skip_colon(): + return f"c{self.subs[0]}" + return f"c:{self.subs[0]}" + + +class WrapT(AndV, WrapperNode): + def __init__(self, sub): + AndV.__init__(self, sub, Just1()) + + def is_wrapper(self): + return True + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"t{self.subs[0]}" + return f"t:{self.subs[0]}" + + +class WrapD(WrapperNode): + def __init__(self, sub): + assert sub.p.has_all("Vz") + WrapperNode.__init__(self, sub) + + self.p = Property("Bond") + self.is_forced = True # sub is V + self.is_expressive = True # sub is V, and we add a single dissat + + @property + def _script(self): + return [OP_DUP, OP_IF] + self.sub._script + [OP_ENDIF] + + @property + def exec_info(self): + return ExecutionInfo.from_wrap_dissat( + self.sub.exec_info, ops_count=3, sat=1, dissat=1 + ) + + def satisfaction(self, sat_material): + return Satisfaction(witness=[b"\x01"]) + self.subs[0].satisfaction(sat_material) + + def dissatisfaction(self): + return Satisfaction(witness=[b""]) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"d{self.subs[0]}" + return f"d:{self.subs[0]}" + + +class WrapV(WrapperNode): + def __init__(self, sub): + assert sub.p.B + WrapperNode.__init__(self, sub) + + self.p = Property("V" + "".join(c for c in "zon" if getattr(sub.p, c))) + self.is_forced = True # V + self.is_expressive = False # V + + @property + def _script(self): + if self.sub._script[-1] == OP_CHECKSIG: + return self.sub._script[:-1] + [OP_CHECKSIGVERIFY] + elif self.sub._script[-1] == OP_CHECKMULTISIG: + return self.sub._script[:-1] + [OP_CHECKMULTISIGVERIFY] + elif self.sub._script[-1] == OP_EQUAL: + return self.sub._script[:-1] + [OP_EQUALVERIFY] + return self.sub._script + [OP_VERIFY] + + @property + def exec_info(self): + verify_cost = int(self._script[-1] == OP_VERIFY) + return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=verify_cost) + + def dissatisfaction(self): + return Satisfaction.unavailable() # It's V. + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"v{self.subs[0]}" + return f"v:{self.subs[0]}" + + +class WrapJ(WrapperNode): + def __init__(self, sub): + assert sub.p.has_all("Bn") + WrapperNode.__init__(self, sub) + + self.p = Property("Bnd" + "".join(c for c in "ou" if getattr(sub.p, c))) + self.is_forced = False # d + self.is_expressive = sub.is_forced + + @property + def _script(self): + return [OP_SIZE, OP_0NOTEQUAL, OP_IF, *self.sub._script, OP_ENDIF] + + @property + def exec_info(self): + return ExecutionInfo.from_wrap_dissat(self.sub.exec_info, ops_count=4, dissat=1) + + def dissatisfaction(self): + return Satisfaction(witness=[b""]) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"j{self.subs[0]}" + return f"j:{self.subs[0]}" + + +class WrapN(WrapperNode): + def __init__(self, sub): + assert sub.p.B + WrapperNode.__init__(self, sub) + + self.p = Property("Bu" + "".join(c for c in "zond" if getattr(sub.p, c))) + + @property + def _script(self): + return [*self.sub._script, OP_0NOTEQUAL] + + @property + def exec_info(self): + return ExecutionInfo.from_wrap(self.sub.exec_info, ops_count=1) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"n{self.subs[0]}" + return f"n:{self.subs[0]}" + + +class WrapL(OrI, WrapperNode): + def __init__(self, sub): + OrI.__init__(self, Just0(), sub) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"l{self.subs[1]}" + return f"l:{self.subs[1]}" + + +class WrapU(OrI, WrapperNode): + def __init__(self, sub): + OrI.__init__(self, sub, Just0()) + + def __repr__(self): + # Avoid duplicating colons + if self.skip_colon(): + return f"u{self.subs[0]}" + return f"u:{self.subs[0]}" diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/parsing.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/parsing.py new file mode 100644 index 00000000..2058b7b6 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/parsing.py @@ -0,0 +1,736 @@ +""" +Utilities to parse Miniscript from string and Script representations. +""" + +from ...bip380.miniscript import fragments + +from ...bip380.key import DescriptorKey +from ...bip380.miniscript.errors import MiniscriptMalformed +from ...bip380.utils.script import ( + CScriptOp, + OP_ADD, + OP_BOOLAND, + OP_BOOLOR, + OP_CHECKSIGVERIFY, + OP_CHECKMULTISIGVERIFY, + OP_EQUALVERIFY, + OP_DUP, + OP_ELSE, + OP_ENDIF, + OP_EQUAL, + OP_FROMALTSTACK, + OP_IFDUP, + OP_IF, + OP_CHECKLOCKTIMEVERIFY, + OP_CHECKMULTISIG, + OP_CHECKSEQUENCEVERIFY, + OP_CHECKSIG, + OP_HASH160, + OP_HASH256, + OP_NOTIF, + OP_RIPEMD160, + OP_SHA256, + OP_SIZE, + OP_SWAP, + OP_TOALTSTACK, + OP_VERIFY, + OP_0NOTEQUAL, + ScriptNumError, + read_script_number, +) + + +def stack_item_to_int(item): + """ + Convert a stack item to an integer depending on its type. + May raise an exception if the item is bytes, otherwise return None if it + cannot perform the conversion. + """ + if isinstance(item, bytes): + return read_script_number(item) + + if isinstance(item, fragments.Node): + if isinstance(item, fragments.Just1): + return 1 + if isinstance(item, fragments.Just0): + return 0 + + if isinstance(item, int): + return item + + return None + + +def decompose_script(script): + """Create a list of Script element from a CScript, decomposing the compact + -VERIFY opcodes into the non-VERIFY OP and an OP_VERIFY. + """ + elems = [] + for elem in script: + if elem == OP_CHECKSIGVERIFY: + elems += [OP_CHECKSIG, OP_VERIFY] + elif elem == OP_CHECKMULTISIGVERIFY: + elems += [OP_CHECKMULTISIG, OP_VERIFY] + elif elem == OP_EQUALVERIFY: + elems += [OP_EQUAL, OP_VERIFY] + else: + elems.append(elem) + return elems + + +def parse_term_single_elem(expr_list, idx): + """ + Try to parse a terminal node from the element of {expr_list} at {idx}. + """ + # Match against pk_k(key). + if ( + isinstance(expr_list[idx], bytes) + and len(expr_list[idx]) == 33 + and expr_list[idx][0] in [2, 3] + ): + expr_list[idx] = fragments.Pk(expr_list[idx]) + + # Match against JUST_1 and JUST_0. + if expr_list[idx] == 1: + expr_list[idx] = fragments.Just1() + if expr_list[idx] == b"": + expr_list[idx] = fragments.Just0() + + +def parse_term_2_elems(expr_list, idx): + """ + Try to parse a terminal node from two elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + elem_a = expr_list[idx] + elem_b = expr_list[idx + 1] + + # Only older() and after() as term with 2 stack items + if not isinstance(elem_b, CScriptOp): + return + try: + n = stack_item_to_int(elem_a) + if n is None: + return + except ScriptNumError: + return + + if n <= 0 or n >= 2 ** 31: + return + + if elem_b == OP_CHECKSEQUENCEVERIFY: + node = fragments.Older(n) + expr_list[idx: idx + 2] = [node] + return expr_list + + if elem_b == OP_CHECKLOCKTIMEVERIFY: + node = fragments.After(n) + expr_list[idx: idx + 2] = [node] + return expr_list + + +def parse_term_5_elems(expr_list, idx, pkh_preimages={}): + """ + Try to parse a terminal node from five elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + # The only 3 items node is pk_h + if expr_list[idx: idx + 2] != [OP_DUP, OP_HASH160]: + return + if not isinstance(expr_list[idx + 2], bytes): + return + if len(expr_list[idx + 2]) != 20: + return + if expr_list[idx + 3: idx + 5] != [OP_EQUAL, OP_VERIFY]: + return + + key_hash = expr_list[idx + 2] + key = pkh_preimages.get(key_hash) + assert key is not None # TODO: have a real error here + node = fragments.Pkh(key) + expr_list[idx: idx + 5] = [node] + return expr_list + + +def parse_term_7_elems(expr_list, idx): + """ + Try to parse a terminal node from seven elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + # Note how all the hashes are 7 elems because the VERIFY was decomposed + # Match against sha256. + if ( + expr_list[idx: idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_SHA256] + and isinstance(expr_list[idx + 5], bytes) + and len(expr_list[idx + 5]) == 32 + and expr_list[idx + 6] == OP_EQUAL + ): + node = fragments.Sha256(expr_list[idx + 5]) + expr_list[idx: idx + 7] = [node] + return expr_list + + # Match against hash256. + if ( + expr_list[idx: idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_HASH256] + and isinstance(expr_list[idx + 5], bytes) + and len(expr_list[idx + 5]) == 32 + and expr_list[idx + 6] == OP_EQUAL + ): + node = fragments.Hash256(expr_list[idx + 5]) + expr_list[idx: idx + 7] = [node] + return expr_list + + # Match against ripemd160. + if ( + expr_list[idx: idx + 5] + == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_RIPEMD160] + and isinstance(expr_list[idx + 5], bytes) + and len(expr_list[idx + 5]) == 20 + and expr_list[idx + 6] == OP_EQUAL + ): + node = fragments.Ripemd160(expr_list[idx + 5]) + expr_list[idx: idx + 7] = [node] + return expr_list + + # Match against hash160. + if ( + expr_list[idx: idx + 5] == [OP_SIZE, b"\x20", OP_EQUAL, OP_VERIFY, OP_HASH160] + and isinstance(expr_list[idx + 5], bytes) + and len(expr_list[idx + 5]) == 20 + and expr_list[idx + 6] == OP_EQUAL + ): + node = fragments.Hash160(expr_list[idx + 5]) + expr_list[idx: idx + 7] = [node] + return expr_list + + +def parse_nonterm_2_elems(expr_list, idx): + """ + Try to parse a non-terminal node from two elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + elem_a = expr_list[idx] + elem_b = expr_list[idx + 1] + + if isinstance(elem_a, fragments.Node): + # Match against and_v. + if isinstance(elem_b, fragments.Node) and elem_a.p.V and elem_b.p.has_any("BKV"): + # Is it a special case of t: wrapper? + if isinstance(elem_b, fragments.Just1): + node = fragments.WrapT(elem_a) + else: + node = fragments.AndV(elem_a, elem_b) + expr_list[idx: idx + 2] = [node] + return expr_list + + # Match against c wrapper. + if elem_b == OP_CHECKSIG and elem_a.p.K: + node = fragments.WrapC(elem_a) + expr_list[idx: idx + 2] = [node] + return expr_list + + # Match against v wrapper. + if elem_b == OP_VERIFY and elem_a.p.B: + node = fragments.WrapV(elem_a) + expr_list[idx: idx + 2] = [node] + return expr_list + + # Match against n wrapper. + if elem_b == OP_0NOTEQUAL and elem_a.p.B: + node = fragments.WrapN(elem_a) + expr_list[idx: idx + 2] = [node] + return expr_list + + # Match against s wrapper. + if isinstance(elem_b, fragments.Node) and elem_a == OP_SWAP and elem_b.p.has_all("Bo"): + node = fragments.WrapS(elem_b) + expr_list[idx: idx + 2] = [node] + return expr_list + + +def parse_nonterm_3_elems(expr_list, idx): + """ + Try to parse a non-terminal node from *at least* three elements of + {expr_list}, starting from {idx}. + Return the new expression list on success, None if there was no match. + """ + elem_a = expr_list[idx] + elem_b = expr_list[idx + 1] + elem_c = expr_list[idx + 2] + + if isinstance(elem_a, fragments.Node) and isinstance(elem_b, fragments.Node): + # Match against and_b. + if elem_c == OP_BOOLAND and elem_a.p.B and elem_b.p.W: + node = fragments.AndB(elem_a, elem_b) + expr_list[idx: idx + 3] = [node] + return expr_list + + # Match against or_b. + if elem_c == OP_BOOLOR and elem_a.p.has_all("Bd") and elem_b.p.has_all("Wd"): + node = fragments.OrB(elem_a, elem_b) + expr_list[idx: idx + 3] = [node] + return expr_list + + # Match against a wrapper. + if ( + elem_a == OP_TOALTSTACK + and isinstance(elem_b, fragments.Node) + and elem_b.p.B + and elem_c == OP_FROMALTSTACK + ): + node = fragments.WrapA(elem_b) + expr_list[idx: idx + 3] = [node] + return expr_list + + # FIXME: multi is a terminal! + # Match against a multi. + try: + k = stack_item_to_int(expr_list[idx]) + except ScriptNumError: + return + if k is None: + return + # ()* CHECKMULTISIG + if k > len(expr_list[idx + 1:]) - 2: + return + # Get the keys + keys = [] + i = idx + 1 + while idx < len(expr_list) - 2: + if not isinstance(expr_list[i], fragments.Pk): + break + keys.append(expr_list[i].pubkey) + i += 1 + if expr_list[i + 1] == OP_CHECKMULTISIG: + if k > len(keys): + return + try: + m = stack_item_to_int(expr_list[i]) + except ScriptNumError: + return + if m is None or m != len(keys): + return + node = fragments.Multi(k, keys) + expr_list[idx: i + 2] = [node] + return expr_list + + +def parse_nonterm_4_elems(expr_list, idx): + """ + Try to parse a non-terminal node from at least four elements of {expr_list}, + starting from {idx}. + Return the new expression list on success, None if there was no match. + """ + (it_a, it_b, it_c, it_d) = expr_list[idx: idx + 4] + + # Match against thresh. It's of the form [X] ([X] ADD)* k EQUAL + if isinstance(it_a, fragments.Node) and it_a.p.has_all("Bdu"): + subs = [it_a] + # The first matches, now do all the ([X] ADD)s and return + # if a pair is of the form (k, EQUAL). + for i in range(idx + 1, len(expr_list) - 1, 2): + if ( + isinstance(expr_list[i], fragments.Node) + and expr_list[i].p.has_all("Wdu") + and expr_list[i + 1] == OP_ADD + ): + subs.append(expr_list[i]) + continue + elif expr_list[i + 1] == OP_EQUAL: + try: + k = stack_item_to_int(expr_list[i]) + if len(subs) >= k >= 1: + node = fragments.Thresh(k, subs) + expr_list[idx: i + 1 + 1] = [node] + return expr_list + except ScriptNumError: + break + else: + break + + # Match against or_c. + if ( + isinstance(it_a, fragments.Node) + and it_a.p.has_all("Bdu") + and it_b == OP_NOTIF + and isinstance(it_c, fragments.Node) + and it_c.p.V + and it_d == OP_ENDIF + ): + node = fragments.OrC(it_a, it_c) + expr_list[idx: idx + 4] = [node] + return expr_list + + # Match against d wrapper. + if ( + [it_a, it_b] == [OP_DUP, OP_IF] + and isinstance(it_c, fragments.Node) + and it_c.p.has_all("Vz") + and it_d == OP_ENDIF + ): + node = fragments.WrapD(it_c) + expr_list[idx: idx + 4] = [node] + return expr_list + + +def parse_nonterm_5_elems(expr_list, idx): + """ + Try to parse a non-terminal node from five elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + (it_a, it_b, it_c, it_d, it_e) = expr_list[idx: idx + 5] + + # Match against or_d. + if ( + isinstance(it_a, fragments.Node) + and it_a.p.has_all("Bdu") + and [it_b, it_c] == [OP_IFDUP, OP_NOTIF] + and isinstance(it_d, fragments.Node) + and it_d.p.B + and it_e == OP_ENDIF + ): + node = fragments.OrD(it_a, it_d) + expr_list[idx: idx + 5] = [node] + return expr_list + + # Match against or_i. + if ( + it_a == OP_IF + and isinstance(it_b, fragments.Node) + and it_b.p.has_any("BKV") + and it_c == OP_ELSE + and isinstance(it_d, fragments.Node) + and it_d.p.has_any("BKV") + and it_e == OP_ENDIF + ): + if isinstance(it_b, fragments.Just0): + node = fragments.WrapL(it_d) + elif isinstance(it_d, fragments.Just0): + node = fragments.WrapU(it_b) + else: + node = fragments.OrI(it_b, it_d) + expr_list[idx: idx + 5] = [node] + return expr_list + + # Match against j wrapper. + if ( + [it_a, it_b, it_c] == [OP_SIZE, OP_0NOTEQUAL, OP_IF] + and isinstance(it_d, fragments.Node) + and it_e == OP_ENDIF + ): + node = fragments.WrapJ(expr_list[idx + 3]) + expr_list[idx: idx + 5] = [node] + return expr_list + + +def parse_nonterm_6_elems(expr_list, idx): + """ + Try to parse a non-terminal node from six elements of {expr_list}, starting + from {idx}. + Return the new expression list on success, None if there was no match. + """ + (it_a, it_b, it_c, it_d, it_e, it_f) = expr_list[idx: idx + 6] + + # Match against andor. + if ( + isinstance(it_a, fragments.Node) + and it_a.p.has_all("Bdu") + and it_b == OP_NOTIF + and isinstance(it_c, fragments.Node) + and it_c.p.has_any("BKV") + and it_d == OP_ELSE + and isinstance(it_e, fragments.Node) + and it_e.p.has_any("BKV") + and it_f == OP_ENDIF + ): + if isinstance(it_c, fragments.Just0): + node = fragments.AndN(it_a, it_e) + else: + node = fragments.AndOr(it_a, it_e, it_c) + expr_list[idx: idx + 6] = [node] + return expr_list + + +def parse_expr_list(expr_list): + """Parse a node from a list of Script elements.""" + # Every recursive call must progress the AST construction, + # until it is complete (single root node remains). + expr_list_len = len(expr_list) + + # Root node reached. + if expr_list_len == 1 and isinstance(expr_list[0], fragments.Node): + return expr_list[0] + + # Step through each list index and match against templates. + idx = expr_list_len - 1 + while idx >= 0: + if expr_list_len - idx >= 2: + new_expr_list = parse_nonterm_2_elems(expr_list, idx) + if new_expr_list is not None: + return parse_expr_list(new_expr_list) + + if expr_list_len - idx >= 3: + new_expr_list = parse_nonterm_3_elems(expr_list, idx) + if new_expr_list is not None: + return parse_expr_list(new_expr_list) + + if expr_list_len - idx >= 4: + new_expr_list = parse_nonterm_4_elems(expr_list, idx) + if new_expr_list is not None: + return parse_expr_list(new_expr_list) + + if expr_list_len - idx >= 5: + new_expr_list = parse_nonterm_5_elems(expr_list, idx) + if new_expr_list is not None: + return parse_expr_list(new_expr_list) + + if expr_list_len - idx >= 6: + new_expr_list = parse_nonterm_6_elems(expr_list, idx) + if new_expr_list is not None: + return parse_expr_list(new_expr_list) + + # Right-to-left parsing. + # Step one position left. + idx -= 1 + + # No match found. + raise MiniscriptMalformed(f"{expr_list}") + + +def miniscript_from_script(script, pkh_preimages={}): + """Construct miniscript node from script. + + :param script: The Bitcoin Script to decode. + :param pkh_preimage: A mapping from keyhash to key to decode pk_h() fragments. + """ + expr_list = decompose_script(script) + expr_list_len = len(expr_list) + + # We first parse terminal expressions. + idx = 0 + while idx < expr_list_len: + parse_term_single_elem(expr_list, idx) + + if expr_list_len - idx >= 2: + new_expr_list = parse_term_2_elems(expr_list, idx) + if new_expr_list is not None: + expr_list = new_expr_list + expr_list_len = len(expr_list) + + if expr_list_len - idx >= 5: + new_expr_list = parse_term_5_elems(expr_list, idx, pkh_preimages) + if new_expr_list is not None: + expr_list = new_expr_list + expr_list_len = len(expr_list) + + if expr_list_len - idx >= 7: + new_expr_list = parse_term_7_elems(expr_list, idx) + if new_expr_list is not None: + expr_list = new_expr_list + expr_list_len = len(expr_list) + + idx += 1 + + # fragments.And then recursively parse non-terminal ones. + return parse_expr_list(expr_list) + + +def split_params(string): + """Read a list of values before the next ')'. Split the result by comma.""" + i = string.find(")") + assert i >= 0 + + params, remaining = string[:i], string[i:] + if len(remaining) > 0: + return params.split(","), remaining[1:] + else: + return params.split(","), "" + + +def parse_many(string): + """Read a list of nodes before the next ')'.""" + subs = [] + remaining = string + while True: + sub, remaining = parse_one(remaining) + subs.append(sub) + if remaining[0] == ")": + return subs, remaining[1:] + assert remaining[0] == "," # TODO: real errors + remaining = remaining[1:] + + +def parse_one_num(string): + """Read an integer before the next comma.""" + i = string.find(",") + assert i >= 0 + + return int(string[:i]), string[i + 1:] + + +def parse_one(string): + """Read a node and its subs recursively from a string. + Returns the node and the part of the string not consumed. + """ + + # We special case fragments.Just1 and fragments.Just0 since they are the only one which don't + # have a function syntax. + if string[0] == "0": + return fragments.Just0(), string[1:] + if string[0] == "1": + return fragments.Just1(), string[1:] + + # Now, find the separator for all functions. + for i, char in enumerate(string): + if char in ["(", ":"]: + break + # For wrappers, we may have many of them. + if char == ":" and i > 1: + tag, remaining = string[0], string[1:] + else: + tag, remaining = string[:i], string[i + 1:] + + # fragments.Wrappers + if char == ":": + sub, remaining = parse_one(remaining) + if tag == "a": + return fragments.WrapA(sub), remaining + + if tag == "s": + return fragments.WrapS(sub), remaining + + if tag == "c": + return fragments.WrapC(sub), remaining + + if tag == "t": + return fragments.WrapT(sub), remaining + + if tag == "d": + return fragments.WrapD(sub), remaining + + if tag == "v": + return fragments.WrapV(sub), remaining + + if tag == "j": + return fragments.WrapJ(sub), remaining + + if tag == "n": + return fragments.WrapN(sub), remaining + + if tag == "l": + return fragments.WrapL(sub), remaining + + if tag == "u": + return fragments.WrapU(sub), remaining + + assert False, (tag, sub, remaining) # TODO: real errors + + # Terminal elements other than 0 and 1 + if tag in [ + "pk", + "pkh", + "pk_k", + "pk_h", + "sha256", + "hash256", + "ripemd160", + "hash160", + "older", + "after", + "multi", + ]: + params, remaining = split_params(remaining) + + if tag == "0": + return fragments.Just0(), remaining + + if tag == "1": + return fragments.Just1(), remaining + + if tag == "pk": + return fragments.WrapC(fragments.Pk(params[0])), remaining + + if tag == "pk_k": + return fragments.Pk(params[0]), remaining + + if tag == "pkh": + return fragments.WrapC(fragments.Pkh(params[0])), remaining + + if tag == "pk_h": + return fragments.Pkh(params[0]), remaining + + if tag == "older": + value = int(params[0]) + return fragments.Older(value), remaining + + if tag == "after": + value = int(params[0]) + return fragments.After(value), remaining + + if tag in ["sha256", "hash256", "ripemd160", "hash160"]: + digest = bytes.fromhex(params[0]) + if tag == "sha256": + return fragments.Sha256(digest), remaining + if tag == "hash256": + return fragments.Hash256(digest), remaining + if tag == "ripemd160": + return fragments.Ripemd160(digest), remaining + return fragments.Hash160(digest), remaining + + if tag == "multi": + k = int(params.pop(0)) + key_n = [] + for param in params: + key_obj = DescriptorKey(param) + key_n.append(key_obj) + return fragments.Multi(k, key_n), remaining + + assert False, (tag, params, remaining) + + # Non-terminal elements (connectives) + # We special case fragments.Thresh, as its first sub is an integer. + if tag == "thresh": + k, remaining = parse_one_num(remaining) + # TODO: real errors in place of unpacking + subs, remaining = parse_many(remaining) + + if tag == "and_v": + return fragments.AndV(*subs), remaining + + if tag == "and_b": + return fragments.AndB(*subs), remaining + + if tag == "and_n": + return fragments.AndN(*subs), remaining + + if tag == "or_b": + return fragments.OrB(*subs), remaining + + if tag == "or_c": + return fragments.OrC(*subs), remaining + + if tag == "or_d": + return fragments.OrD(*subs), remaining + + if tag == "or_i": + return fragments.OrI(*subs), remaining + + if tag == "andor": + return fragments.AndOr(*subs), remaining + + if tag == "thresh": + return fragments.Thresh(k, subs), remaining + + assert False, (tag, subs, remaining) # TODO + + +def miniscript_from_str(ms_str): + """Construct miniscript node from string representation""" + node, remaining = parse_one(ms_str) + assert remaining == "" + return node diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/property.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/property.py new file mode 100644 index 00000000..5cff50b7 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/property.py @@ -0,0 +1,83 @@ +# Copyright (c) 2020 The Bitcoin Core developers +# Copyright (c) 2021 Antoine Poinsot +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +from .errors import MiniscriptPropertyError + + +# TODO: implement __eq__ +class Property: + """Miniscript expression property""" + + # "B": Base type + # "V": Verify type + # "K": Key type + # "W": Wrapped type + # "z": Zero-arg property + # "o": One-arg property + # "n": Nonzero arg property + # "d": Dissatisfiable property + # "u": Unit property + types = "BVKW" + props = "zondu" + + def __init__(self, property_str=""): + """Create a property, optionally from a str of property and types""" + allowed = self.types + self.props + invalid = set(property_str).difference(set(allowed)) + + if invalid: + raise MiniscriptPropertyError( + f"Invalid property/type character(s) '{''.join(invalid)}'" + f" (allowed: '{allowed}')" + ) + + for literal in allowed: + setattr(self, literal, literal in property_str) + + self.check_valid() + + def __repr__(self): + """Generate string representation of property""" + return "".join([c for c in self.types + self.props if getattr(self, c)]) + + def has_all(self, properties): + """Given a str of types and properties, return whether we have all of them""" + return all([getattr(self, pt) for pt in properties]) + + def has_any(self, properties): + """Given a str of types and properties, return whether we have at least one of them""" + return any([getattr(self, pt) for pt in properties]) + + def check_valid(self): + """Raises a MiniscriptPropertyError if the types/properties conflict""" + # Can only be of a single type. + if len(self.type()) > 1: + raise MiniscriptPropertyError(f"A Miniscript fragment can only be of a single type, got '{self.type()}'") + + # Check for conflicts in type & properties. + checks = [ + # (type/property, must_be, must_not_be) + ("K", "u", ""), + ("V", "", "du"), + ("z", "", "o"), + ("n", "", "z"), + ] + conflicts = [] + + for (attr, must_be, must_not_be) in checks: + if not getattr(self, attr): + continue + if not self.has_all(must_be): + conflicts.append(f"{attr} must be {must_be}") + if self.has_any(must_not_be): + conflicts.append(f"{attr} must not be {must_not_be}") + if conflicts: + raise MiniscriptPropertyError(f"Conflicting types and properties: {', '.join(conflicts)}") + + def type(self): + return "".join(filter(lambda x: x in self.types, str(self))) + + def properties(self): + return "".join(filter(lambda x: x in self.props, str(self))) diff --git a/bitcoin_client/ledger_bitcoin/bip380/miniscript/satisfaction.py b/bitcoin_client/ledger_bitcoin/bip380/miniscript/satisfaction.py new file mode 100644 index 00000000..67e87806 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/miniscript/satisfaction.py @@ -0,0 +1,409 @@ +""" +Miniscript satisfaction. + +This module contains logic for "signing for" a Miniscript (constructing a valid witness +that meets the conditions set by the Script) and analysis of such satisfaction(s) (eg the +maximum cost in a given resource). +This is currently focused on non-malleable satisfaction. We take shortcuts to not care about +non-canonical (dis)satisfactions. +""" + + +def add_optional(a, b): + """Add two numbers that may be None together.""" + if a is None or b is None: + return None + return a + b + + +def max_optional(a, b): + """Return the maximum of two numbers that may be None.""" + if a is None: + return b + if b is None: + return a + return max(a, b) + + +class SatisfactionMaterial: + """Data that may be needed in order to satisfy a Minsicript fragment.""" + + def __init__( + self, preimages={}, signatures={}, max_sequence=2 ** 32, max_lock_time=2 ** 32 + ): + """ + :param preimages: Mapping from a hash (as bytes), to its 32-bytes preimage. + :param signatures: Mapping from a public key (as bytes), to a signature for this key. + :param max_sequence: The maximum relative timelock possible (coin age). + :param max_lock_time: The maximum absolute timelock possible (block height). + """ + self.preimages = preimages + self.signatures = signatures + self.max_sequence = max_sequence + self.max_lock_time = max_lock_time + + def clear(self): + self.preimages.clear() + self.signatures.clear() + self.max_sequence = 0 + self.max_lock_time = 0 + + def __repr__(self): + return ( + f"SatisfactionMaterial(preimages: {self.preimages}, signatures: " + f"{self.signatures}, max_sequence: {self.max_sequence}, max_lock_time: " + f"{self.max_lock_time}" + ) + + +class Satisfaction: + """All information about a satisfaction.""" + + def __init__(self, witness, has_sig=False): + assert isinstance(witness, list) or witness is None + self.witness = witness + self.has_sig = has_sig + # TODO: we probably need to take into account non-canon sats, as the algorithm + # described on the website mandates it: + # > Iterate over all the valid satisfactions/dissatisfactions in the table above + # > (including the non-canonical ones), + + def __add__(self, other): + """Concatenate two satisfactions together.""" + witness = add_optional(self.witness, other.witness) + has_sig = self.has_sig or other.has_sig + return Satisfaction(witness, has_sig) + + def __or__(self, other): + """Choose between two (dis)satisfactions.""" + assert isinstance(other, Satisfaction) + + # If one isn't available, return the other one. + if self.witness is None: + return other + if other.witness is None: + return self + + # > If among all valid solutions (including DONTUSE ones) more than one does not + # > have the HASSIG marker, return DONTUSE, as this is malleable because of reason + # > 1. + # TODO + # if not (self.has_sig or other.has_sig): + # return Satisfaction.unavailable() + + # > If instead exactly one does not have the HASSIG marker, return that solution + # > because of reason 2. + if self.has_sig and not other.has_sig: + return other + if not self.has_sig and other.has_sig: + return self + + # > Otherwise, all not-DONTUSE options are valid, so return the smallest one (in + # > terms of witness size). + if self.size() > other.size(): + return other + + # > If all valid solutions have the HASSIG marker, but all of them are DONTUSE, return DONTUSE-HASSIG. + # TODO + + return self + + def unavailable(): + return Satisfaction(witness=None) + + def is_unavailable(self): + return self.witness is None + + def size(self): + return len(self.witness) + sum(len(elem) for elem in self.witness) + + def from_concat(sat_material, sub_a, sub_b, disjunction=False): + """Get the satisfaction for a Miniscript whose Script corresponds to a + concatenation of two subscripts A and B. + + :param sub_a: The sub-fragment A. + :param sub_b: The sub-fragment B. + :param disjunction: Whether this fragment has an 'or()' semantic. + """ + if disjunction: + return (sub_b.dissatisfaction() + sub_a.satisfaction(sat_material)) | ( + sub_b.satisfaction(sat_material) + sub_a.dissatisfaction() + ) + return sub_b.satisfaction(sat_material) + sub_a.satisfaction(sat_material) + + def from_or_uneven(sat_material, sub_a, sub_b): + """Get the satisfaction for a Miniscript which unconditionally executes a first + sub A and only executes B if A was dissatisfied. + + :param sub_a: The sub-fragment A. + :param sub_b: The sub-fragment B. + """ + return sub_a.satisfaction(sat_material) | ( + sub_b.satisfaction(sat_material) + sub_a.dissatisfaction() + ) + + def from_thresh(sat_material, k, subs): + """Get the satisfaction for a Miniscript which satisfies k of the given subs, + and dissatisfies all the others. + + :param sat_material: The material to satisfy the challenges. + :param k: The number of subs that need to be satisfied. + :param subs: The list of all subs of the threshold. + """ + # Pick the k sub-fragments to satisfy, prefering (in order): + # 1. Fragments that don't require a signature to be satisfied + # 2. Fragments whose satisfaction's size is smaller + # Record the unavailable (in either way) ones as we go. + arbitrage, unsatisfiable, undissatisfiable = [], [], [] + for sub in subs: + sat, dissat = sub.satisfaction(sat_material), sub.dissatisfaction() + if sat.witness is None: + unsatisfiable.append(sub) + elif dissat.witness is None: + undissatisfiable.append(sub) + else: + arbitrage.append( + (int(sat.has_sig), len(sat.witness) - len(dissat.witness), sub) + ) + + # If not enough (dis)satisfactions are available, fail. + if len(unsatisfiable) > len(subs) - k or len(undissatisfiable) > k: + return Satisfaction.unavailable() + + # Otherwise, satisfy the k most optimal ones. + arbitrage = sorted(arbitrage, key=lambda x: x[:2]) + optimal_sat = undissatisfiable + [a[2] for a in arbitrage] + unsatisfiable + to_satisfy = set(optimal_sat[:k]) + return sum( + [ + sub.satisfaction(sat_material) + if sub in to_satisfy + else sub.dissatisfaction() + for sub in subs[::-1] + ], + start=Satisfaction(witness=[]), + ) + + +class ExecutionInfo: + """Information about the execution of a Miniscript.""" + + def __init__(self, stat_ops, _dyn_ops, sat_size, dissat_size): + # The *maximum* number of *always* executed non-PUSH Script OPs to satisfy this + # Miniscript fragment non-malleably. + self._static_ops_count = stat_ops + # The maximum possible number of counted-as-executed-by-interpreter OPs if this + # fragment is executed. + # It is only >0 for an executed multi() branch. That is, for a CHECKMULTISIG that + # is not part of an unexecuted branch of an IF .. ENDIF. + self._dyn_ops_count = _dyn_ops + # The *maximum* number of stack elements to satisfy this Miniscript fragment + # non-malleably. + self.sat_elems = sat_size + # The *maximum* number of stack elements to dissatisfy this Miniscript fragment + # non-malleably. + self.dissat_elems = dissat_size + + @property + def ops_count(self): + """ + The worst-case number of OPs that would be considered executed by the Script + interpreter. + Note it is considered alone and not necessarily coherent with the other maxima. + """ + return self._static_ops_count + self._dyn_ops_count + + def is_dissatisfiable(self): + """Whether the Miniscript is *non-malleably* dissatisfiable.""" + return self.dissat_elems is not None + + def set_undissatisfiable(self): + """Set the Miniscript as being impossible to dissatisfy.""" + self.dissat_elems = None + + def from_concat(sub_a, sub_b, ops_count=0, disjunction=False): + """Compute the execution info from a Miniscript whose Script corresponds to + a concatenation of two subscript A and B. + + :param sub_a: The execution information of the subscript A. + :param sub_b: The execution information of the subscript B. + :param ops_count: The added number of static OPs added on top. + :param disjunction: Whether this fragment has an 'or()' semantic. + """ + # Number of static OPs is simple, they are all executed. + static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count + # Same for the dynamic ones, there is no conditional branch here. + dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count + # If this is an 'or', only one needs to be satisfied. Pick the most expensive + # satisfaction/dissatisfaction pair. + # If not, both need to be anyways. + if disjunction: + first = add_optional(sub_a.sat_elems, sub_b.dissat_elems) + second = add_optional(sub_a.dissat_elems, sub_b.sat_elems) + sat_elems = max_optional(first, second) + else: + sat_elems = add_optional(sub_a.sat_elems, sub_b.sat_elems) + # In any case dissatisfying the fragment requires dissatisfying both concatenated + # subs. + dissat_elems = add_optional(sub_a.dissat_elems, sub_b.dissat_elems) + + return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) + + def from_or_uneven(sub_a, sub_b, ops_count=0): + """Compute the execution info from a Miniscript which always executes A and only + executes B depending on the outcome of A's execution. + + :param sub_a: The execution information of the subscript A. + :param sub_b: The execution information of the subscript B. + :param ops_count: The added number of static OPs added on top. + """ + # Number of static OPs is simple, they are all executed. + static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count + # If the first sub is non-malleably dissatisfiable, the worst case is executing + # both. Otherwise it is necessarily satisfying only the first one. + if sub_a.is_dissatisfiable(): + dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count + else: + dyn_ops = sub_a._dyn_ops_count + # Either we satisfy A, or satisfy B (and thereby dissatisfy A). Pick the most + # expensive. + first = sub_a.sat_elems + second = add_optional(sub_a.dissat_elems, sub_b.sat_elems) + sat_elems = max_optional(first, second) + # We only take canonical dissatisfactions into account. + dissat_elems = add_optional(sub_a.dissat_elems, sub_b.dissat_elems) + + return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) + + def from_or_even(sub_a, sub_b, ops_count): + """Compute the execution info from a Miniscript which executes either A or B, but + never both. + + :param sub_a: The execution information of the subscript A. + :param sub_b: The execution information of the subscript B. + :param ops_count: The added number of static OPs added on top. + """ + # Number of static OPs is simple, they are all executed. + static_ops = sub_a._static_ops_count + sub_b._static_ops_count + ops_count + # Only one of the branch is executed, pick the most expensive one. + dyn_ops = max(sub_a._dyn_ops_count, sub_b._dyn_ops_count) + # Same. Also, we add a stack element used to tell which branch to take. + sat_elems = add_optional(max_optional(sub_a.sat_elems, sub_b.sat_elems), 1) + # Same here. + dissat_elems = add_optional( + max_optional(sub_a.dissat_elems, sub_b.dissat_elems), 1 + ) + + return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) + + def from_andor_uneven(sub_a, sub_b, sub_c, ops_count=0): + """Compute the execution info from a Miniscript which always executes A, and then + executes B if A returned True else executes C. Semantic: or(and(A,B), C). + + :param sub_a: The execution information of the subscript A. + :param sub_b: The execution information of the subscript B. + :param sub_b: The execution information of the subscript C. + :param ops_count: The added number of static OPs added on top. + """ + # Number of static OPs is simple, they are all executed. + static_ops = ( + sum(sub._static_ops_count for sub in [sub_a, sub_b, sub_c]) + ops_count + ) + # If the first sub is non-malleably dissatisfiable, the worst case is executing + # it and the most expensive between B and C. + # If it isn't the worst case is then necessarily to execute A and B. + if sub_a.is_dissatisfiable(): + dyn_ops = sub_a._dyn_ops_count + max( + sub_b._dyn_ops_count, sub_c._dyn_ops_count + ) + else: + # If the first isn't non-malleably dissatisfiable, the worst case is + # satisfying it (and necessarily satisfying the second one too) + dyn_ops = sub_a._dyn_ops_count + sub_b._dyn_ops_count + # Same for the number of stack elements (implicit from None here). + first = add_optional(sub_a.sat_elems, sub_b.sat_elems) + second = add_optional(sub_a.dissat_elems, sub_c.sat_elems) + sat_elems = max_optional(first, second) + # The only canonical dissatisfaction is dissatisfying A and C. + dissat_elems = add_optional(sub_a.dissat_elems, sub_c.dissat_elems) + + return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) + + # TODO: i think it'd be possible to not have this be special-cased to 'thresh()' + def from_thresh(k, subs): + """Compute the execution info from a Miniscript 'thresh()' fragment. Specialized + to this specifc fragment for now. + + :param k: The actual threshold of the 'thresh()' fragment. + :param subs: All the possible sub scripts. + """ + # All the OPs from the subs + n-1 * OP_ADD + 1 * OP_EQUAL + static_ops = sum(sub._static_ops_count for sub in subs) + len(subs) + # dyn_ops = sum(sorted([sub._dyn_ops_count for sub in subs], reverse=True)[:k]) + # All subs are executed, there is no OP_IF branch. + dyn_ops = sum([sub._dyn_ops_count for sub in subs]) + + # In order to estimate the worst case we simulate to satisfy the k subs whose + # sat/dissat ratio is the largest, and dissatisfy the others. + # We do so by iterating through all the subs, recording their sat-dissat "score" + # and those that either cannot be satisfied or dissatisfied. + arbitrage, unsatisfiable, undissatisfiable = [], [], [] + for sub in subs: + if sub.sat_elems is None: + unsatisfiable.append(sub) + elif sub.dissat_elems is None: + undissatisfiable.append(sub) + else: + arbitrage.append((sub.sat_elems - sub.dissat_elems, sub)) + # Of course, if too many can't be (dis)satisfied, we have a problem. + # Otherwise, simulate satisfying first the subs that must be (no dissatisfaction) + # then the most expensive ones, and then dissatisfy all the others. + if len(unsatisfiable) > len(subs) - k or len(undissatisfiable) > k: + sat_elems = None + else: + arbitrage = sorted(arbitrage, key=lambda x: x[0], reverse=True) + worst_sat = undissatisfiable + [a[1] for a in arbitrage] + unsatisfiable + sat_elems = sum( + [sub.sat_elems for sub in worst_sat[:k]] + + [sub.dissat_elems for sub in worst_sat[k:]] + ) + if len(undissatisfiable) > 0: + dissat_elems = None + else: + dissat_elems = sum([sub.dissat_elems for sub in subs]) + + return ExecutionInfo(static_ops, dyn_ops, sat_elems, dissat_elems) + + def from_wrap(sub, ops_count, dyn=0, sat=0, dissat=0): + """Compute the execution info from a Miniscript which always executes a subscript + but adds some logic around. + + :param sub: The execution information of the single subscript. + :param ops_count: The added number of static OPs added on top. + :param dyn: The added number of dynamic OPs added on top. + :param sat: The added number of satisfaction stack elements added on top. + :param dissat: The added number of dissatisfcation stack elements added on top. + """ + return ExecutionInfo( + sub._static_ops_count + ops_count, + sub._dyn_ops_count + dyn, + add_optional(sub.sat_elems, sat), + add_optional(sub.dissat_elems, dissat), + ) + + def from_wrap_dissat(sub, ops_count, dyn=0, sat=0, dissat=0): + """Compute the execution info from a Miniscript which always executes a subscript + but adds some logic around. + + :param sub: The execution information of the single subscript. + :param ops_count: The added number of static OPs added on top. + :param dyn: The added number of dynamic OPs added on top. + :param sat: The added number of satisfaction stack elements added on top. + :param dissat: The added number of dissatisfcation stack elements added on top. + """ + return ExecutionInfo( + sub._static_ops_count + ops_count, + sub._dyn_ops_count + dyn, + add_optional(sub.sat_elems, sat), + dissat, + ) diff --git a/bitcoin_client/ledger_bitcoin/bip380/utils/__init__.py b/bitcoin_client/ledger_bitcoin/bip380/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bitcoin_client/ledger_bitcoin/bip380/utils/bignum.py b/bitcoin_client/ledger_bitcoin/bip380/utils/bignum.py new file mode 100644 index 00000000..13849391 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/utils/bignum.py @@ -0,0 +1,64 @@ +# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2021 Antoine Poinsot +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Big number routines. + +This file is taken from the Bitcoin Core test framework. It was previously +copied from python-bitcoinlib. +""" + +import struct + + +# generic big endian MPI format + + +def bn_bytes(v, have_ext=False): + ext = 0 + if have_ext: + ext = 1 + return ((v.bit_length() + 7) // 8) + ext + + +def bn2bin(v): + s = bytearray() + i = bn_bytes(v) + while i > 0: + s.append((v >> ((i - 1) * 8)) & 0xFF) + i -= 1 + return s + + +def bn2mpi(v): + have_ext = False + if v.bit_length() > 0: + have_ext = (v.bit_length() & 0x07) == 0 + + neg = False + if v < 0: + neg = True + v = -v + + s = struct.pack(b">I", bn_bytes(v, have_ext)) + ext = bytearray() + if have_ext: + ext.append(0) + v_bin = bn2bin(v) + if neg: + if have_ext: + ext[0] |= 0x80 + else: + v_bin[0] |= 0x80 + return s + ext + v_bin + + +# bitcoin-specific little endian format, with implicit size +def mpi2vch(s): + r = s[4:] # strip size + r = r[::-1] # reverse string, converting BE->LE + return r + + +def bn2vch(v): + return bytes(mpi2vch(bn2mpi(v))) diff --git a/bitcoin_client/ledger_bitcoin/bip380/utils/hashes.py b/bitcoin_client/ledger_bitcoin/bip380/utils/hashes.py new file mode 100644 index 00000000..1124dc57 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/utils/hashes.py @@ -0,0 +1,20 @@ +""" +Common Bitcoin hashes. +""" + +import hashlib +from .ripemd_fallback import ripemd160_fallback + + +def sha256(data): + """{data} must be bytes, returns sha256(data)""" + assert isinstance(data, bytes) + return hashlib.sha256(data).digest() + + +def hash160(data): + """{data} must be bytes, returns ripemd160(sha256(data))""" + assert isinstance(data, bytes) + if 'ripemd160' in hashlib.algorithms_available: + return hashlib.new("ripemd160", sha256(data)).digest() + return ripemd160_fallback(sha256(data)) diff --git a/bitcoin_client/ledger_bitcoin/bip380/utils/ripemd_fallback.py b/bitcoin_client/ledger_bitcoin/bip380/utils/ripemd_fallback.py new file mode 100644 index 00000000..a4043de9 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/utils/ripemd_fallback.py @@ -0,0 +1,117 @@ +# 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 + +# fmt: off + +""" +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_fallback(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) \ No newline at end of file diff --git a/bitcoin_client/ledger_bitcoin/bip380/utils/script.py b/bitcoin_client/ledger_bitcoin/bip380/utils/script.py new file mode 100644 index 00000000..9ff0e703 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/bip380/utils/script.py @@ -0,0 +1,473 @@ +# Copyright (c) 2015-2020 The Bitcoin Core developers +# Copyright (c) 2021 Antoine Poinsot +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Script utilities + +This file was taken from Bitcoin Core test framework, and was previously +modified from python-bitcoinlib. +""" +import struct + +from .bignum import bn2vch + + +OPCODE_NAMES = {} + + +class CScriptOp(int): + """A single script opcode""" + + __slots__ = () + + @staticmethod + def encode_op_pushdata(d): + """Encode a PUSHDATA op, returning bytes""" + if len(d) < 0x4C: + return b"" + bytes([len(d)]) + d # OP_PUSHDATA + elif len(d) <= 0xFF: + return b"\x4c" + bytes([len(d)]) + d # OP_PUSHDATA1 + elif len(d) <= 0xFFFF: + return b"\x4d" + struct.pack(b" 4: + raise ScriptNumError("Too large push") + + if size == 0: + return 0 + + # We always check for minimal encoding + if (data[size - 1] & 0x7f) == 0: + if size == 1 or (data[size - 2] & 0x80) == 0: + raise ScriptNumError("Non minimal encoding") + + res = int.from_bytes(data, byteorder="little") + + # Remove the sign bit if set, and negate the result + if data[size - 1] & 0x80: + return -(res & ~(0x80 << (size - 1))) + return res + + +class CScriptInvalidError(Exception): + """Base class for CScript exceptions""" + + pass + + +class CScriptTruncatedPushDataError(CScriptInvalidError): + """Invalid pushdata due to truncation""" + + def __init__(self, msg, data): + self.data = data + super(CScriptTruncatedPushDataError, self).__init__(msg) + + +# This is used, eg, for blockchain heights in coinbase scripts (bip34) +class CScriptNum: + __slots__ = ("value",) + + def __init__(self, d=0): + self.value = d + + @staticmethod + def encode(obj): + r = bytearray(0) + if obj.value == 0: + return bytes(r) + neg = obj.value < 0 + absvalue = -obj.value if neg else obj.value + while absvalue: + r.append(absvalue & 0xFF) + absvalue >>= 8 + if r[-1] & 0x80: + r.append(0x80 if neg else 0) + elif neg: + r[-1] |= 0x80 + return bytes([len(r)]) + r + + @staticmethod + def decode(vch): + result = 0 + # We assume valid push_size and minimal encoding + value = vch[1:] + if len(value) == 0: + return result + for i, byte in enumerate(value): + result |= int(byte) << 8 * i + if value[-1] >= 0x80: + # Mask for all but the highest result bit + num_mask = (2 ** (len(value) * 8) - 1) >> 1 + result &= num_mask + result *= -1 + return result + + +class CScript(bytes): + """Serialized script + + A bytes subclass, so you can use this directly whenever bytes are accepted. + Note that this means that indexing does *not* work - you'll get an index by + byte rather than opcode. This format was chosen for efficiency so that the + general case would not require creating a lot of little CScriptOP objects. + + iter(script) however does iterate by opcode. + """ + + __slots__ = () + + @classmethod + def __coerce_instance(cls, other): + # Coerce other into bytes + if isinstance(other, CScriptOp): + other = bytes([other]) + elif isinstance(other, CScriptNum): + if other.value == 0: + other = bytes([CScriptOp(OP_0)]) + else: + other = CScriptNum.encode(other) + elif isinstance(other, int): + if 0 <= other <= 16: + other = bytes([CScriptOp.encode_op_n(other)]) + elif other == -1: + other = bytes([OP_1NEGATE]) + else: + other = CScriptOp.encode_op_pushdata(bn2vch(other)) + elif isinstance(other, (bytes, bytearray)): + other = CScriptOp.encode_op_pushdata(other) + return other + + def __add__(self, other): + # Do the coercion outside of the try block so that errors in it are + # noticed. + other = self.__coerce_instance(other) + + try: + # bytes.__add__ always returns bytes instances unfortunately + return CScript(super(CScript, self).__add__(other)) + except TypeError: + raise TypeError("Can not add a %r instance to a CScript" % other.__class__) + + def join(self, iterable): + # join makes no sense for a CScript() + raise NotImplementedError + + def __new__(cls, value=b""): + if isinstance(value, bytes) or isinstance(value, bytearray): + return super(CScript, cls).__new__(cls, value) + else: + + def coerce_iterable(iterable): + for instance in iterable: + yield cls.__coerce_instance(instance) + + # Annoyingly on both python2 and python3 bytes.join() always + # returns a bytes instance even when subclassed. + return super(CScript, cls).__new__(cls, b"".join(coerce_iterable(value))) + + def raw_iter(self): + """Raw iteration + + Yields tuples of (opcode, data, sop_idx) so that the different possible + PUSHDATA encodings can be accurately distinguished, as well as + determining the exact opcode byte indexes. (sop_idx) + """ + i = 0 + while i < len(self): + sop_idx = i + opcode = self[i] + i += 1 + + if opcode > OP_PUSHDATA4: + yield (opcode, None, sop_idx) + else: + datasize = None + pushdata_type = None + if opcode < OP_PUSHDATA1: + pushdata_type = "PUSHDATA(%d)" % opcode + datasize = opcode + + elif opcode == OP_PUSHDATA1: + pushdata_type = "PUSHDATA1" + if i >= len(self): + raise CScriptInvalidError("PUSHDATA1: missing data length") + datasize = self[i] + i += 1 + + elif opcode == OP_PUSHDATA2: + pushdata_type = "PUSHDATA2" + if i + 1 >= len(self): + raise CScriptInvalidError("PUSHDATA2: missing data length") + datasize = self[i] + (self[i + 1] << 8) + i += 2 + + elif opcode == OP_PUSHDATA4: + pushdata_type = "PUSHDATA4" + if i + 3 >= len(self): + raise CScriptInvalidError("PUSHDATA4: missing data length") + datasize = ( + self[i] + + (self[i + 1] << 8) + + (self[i + 2] << 16) + + (self[i + 3] << 24) + ) + i += 4 + + else: + assert False # shouldn't happen + + data = bytes(self[i: i + datasize]) + + # Check for truncation + if len(data) < datasize: + raise CScriptTruncatedPushDataError( + "%s: truncated data" % pushdata_type, data + ) + + i += datasize + + yield (opcode, data, sop_idx) + + def __iter__(self): + """'Cooked' iteration + + Returns either a CScriptOP instance, an integer, or bytes, as + appropriate. + + See raw_iter() if you need to distinguish the different possible + PUSHDATA encodings. + """ + for (opcode, data, sop_idx) in self.raw_iter(): + if data is not None: + yield data + else: + opcode = CScriptOp(opcode) + + if opcode.is_small_int(): + yield opcode.decode_op_n() + else: + yield CScriptOp(opcode) + + def __repr__(self): + def _repr(o): + if isinstance(o, bytes): + return "x('%s')" % o.hex() + else: + return repr(o) + + ops = [] + i = iter(self) + while True: + op = None + try: + op = _repr(next(i)) + except CScriptTruncatedPushDataError as err: + op = "%s..." % (_repr(err.data), err) + break + except CScriptInvalidError as err: + op = "" % err + break + except StopIteration: + break + finally: + if op is not None: + ops.append(op) + + return "CScript([%s])" % ", ".join(ops) + + def GetSigOpCount(self, fAccurate): + """Get the SigOp count. + + fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details. + + Note that this is consensus-critical. + """ + n = 0 + lastOpcode = OP_INVALIDOPCODE + for (opcode, data, sop_idx) in self.raw_iter(): + if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY): + n += 1 + elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): + if fAccurate and (OP_1 <= lastOpcode <= OP_16): + n += opcode.decode_op_n() + else: + n += 20 + lastOpcode = opcode + return n diff --git a/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py b/bitcoin_client/ledger_bitcoin/btchip/btchipHelpers.py index ba5b66c5..bb74cd56 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 f2066919..156b5890 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -3,7 +3,7 @@ import base64 from io import BytesIO, BufferedReader -from bitcoin_client.ledger_bitcoin.errors import UnknownDeviceError +from .bip380.descriptors import Descriptor from .command_builder import BitcoinCommandBuilder, BitcoinInsType from .common import Chain, read_uint, read_varint @@ -11,9 +11,11 @@ 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 WalletPolicy, WalletType from .psbt import PSBT, normalize_psbt +from . import segwit_addr from ._serialize import deser_string @@ -109,6 +111,13 @@ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]: wallet_id = response[0:32] wallet_hmac = response[32:64] + if self._should_validate_address(wallet): + # 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_segwit_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( @@ -143,9 +152,18 @@ def get_wallet_address( if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS) - return response.decode() + result = response.decode() + + if self._should_validate_address(wallet): + # sanity check: for miniscripts, derive the address independently with python-bip380 + + if result != self._derive_segwit_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") + + return result def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]: + psbt = normalize_psbt(psbt) if psbt.version != 2: @@ -253,6 +271,19 @@ def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: return base64.b64encode(response).decode('utf-8') + def _should_validate_address(self, wallet: WalletPolicy) -> bool: + # TODO: extend to taproot miniscripts once supported + return wallet.descriptor_template.startswith("wsh(") and not wallet.descriptor_template.startswith("wsh(sortedmulti(") + + def _derive_segwit_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> bool: + desc = Descriptor.from_str(wallet.get_descriptor(change)) + desc.derive(address_index) + spk = desc.script_pubkey + if spk[0:2] != b'\x00\x20' or len(spk) != 34: + raise RuntimeError("Invalid scriptPubKey") + hrp = "bc" if self.chain == Chain.MAIN else "tb" + return segwit_addr.encode(hrp, 0, spk[2:]) + def createClient(comm_client: Optional[TransportClient] = None, chain: Chain = Chain.MAIN, debug: bool = False) -> Union[LegacyClient, NewClient]: if comm_client is None: diff --git a/bitcoin_client/ledger_bitcoin/descriptor.py b/bitcoin_client/ledger_bitcoin/descriptor.py deleted file mode 100644 index ef85e8f5..00000000 --- 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/py.typed b/bitcoin_client/ledger_bitcoin/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/bitcoin_client/ledger_bitcoin/segwit_addr.py b/bitcoin_client/ledger_bitcoin/segwit_addr.py new file mode 100644 index 00000000..ef417477 --- /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/pyproject.toml b/bitcoin_client/pyproject.toml index f7473453..89455162 100644 --- a/bitcoin_client/pyproject.toml +++ b/bitcoin_client/pyproject.toml @@ -1,5 +1,7 @@ [build-system] requires = [ + "bip32~=3.0", + "coincurve~=18.0", "typing-extensions>=3.7", "ledgercomm>=1.1.0", "setuptools>=42", diff --git a/bitcoin_client/setup.cfg b/bitcoin_client/setup.cfg index 235d722b..21656c88 100644 --- a/bitcoin_client/setup.cfg +++ b/bitcoin_client/setup.cfg @@ -18,10 +18,15 @@ classifiers = packages = find: python_requires = >=3.7 install_requires= + bip32~=3.0, + coincurve~=18.0, 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 58945f42..23a57de0 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_js/README.md b/bitcoin_client_js/README.md index cdc9ddbe..efd4ab53 100644 --- a/bitcoin_client_js/README.md +++ b/bitcoin_client_js/README.md @@ -6,11 +6,17 @@ TypeScript client for Ledger Bitcoin application. Supports versions 2.1.0 and ab 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 @@ -26,10 +32,10 @@ The following example showcases all the main methods of the `Client`'s interface More examples can be found in the [test suite](src/__tests__/appClient.test.ts). -Testing the `signPsbt` method requires a valid PSBTv2, and provide the corresponding wallet policy; it is skipped by default in the following example. +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. @@ -89,23 +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, pubkey, signature], where: + // result will be a list of triples [i, partialSig], where: // - i is the input index - // - pubkey is either a 33-byte compressed pubkey, or a 32-byte x-only pubkey - // - signature is the signature for the corresponding input/pubkey; the signature is concatenated with - // the 1-byte sighash-type (except if the sighash type is SIGHASH_DEFAULT in taproot signing). + // - 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:"); @@ -117,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 912c1bc2..2f7e2334 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.0.2", "description": "Ledger Hardware Wallet Syscoin 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,9 +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", + "bitcoinjs-lib": "^6.1.3", "bs58check": "2.1.2", "tiny-secp256k1": "^2.1.2" }, @@ -72,4 +74,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 2dba3247..fcf69441 100644 --- a/bitcoin_client_js/src/__tests__/appClient.test.ts +++ b/bitcoin_client_js/src/__tests__/appClient.test.ts @@ -128,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(); @@ -255,6 +260,51 @@ describe("test AppClient", () => { 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", @@ -262,6 +312,7 @@ describe("test AppClient", () => { [ "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF", "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK", + "tpubDCoDDpHR1MYXcFrarTcwBufQvWPXSSZpGxjnhRaW612TMxs5TWDEPdbYRHtQdZ9z1UqtKGQKVQ4FqejzbFSdvQvJsD75yrgh7thVoFho6jE", ] ); @@ -296,25 +347,110 @@ describe("test AppClient", () => { expect(result.length).toEqual(2); expect(result[0][0]).toEqual(0); - expect(result[0][1]).toEqual(Buffer.from( + expect(result[0][1].pubkey).toEqual(Buffer.from( "03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3", "hex" )); - expect(result[0][2]).toEqual(Buffer.from( + expect(result[0][1].signature).toEqual(Buffer.from( "304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01", "hex" )); expect(result[1][0]).toEqual(1); - expect(result[1][1]).toEqual(Buffer.from( + expect(result[1][1].pubkey).toEqual(Buffer.from( "0271b5b779ad870838587797bcf6f0c7aec5abe76a709d724f48d2e26cf874f0a0", "hex" )); - expect(result[1][2]).toEqual(Buffer.from( + 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 () => { @@ -327,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__/psbtv2.test.ts b/bitcoin_client_js/src/__tests__/psbtv2.test.ts index 088ddfda..3aa2b103 100644 --- a/bitcoin_client_js/src/__tests__/psbtv2.test.ts +++ b/bitcoin_client_js/src/__tests__/psbtv2.test.ts @@ -1,7 +1,7 @@ import { PsbtV2 } from "../lib/psbtv2"; describe("PsbtV2", () => { - it("deserializes a psbt and reserializes it unchanged", async () => { + 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" @@ -12,4 +12,22 @@ describe("PsbtV2", () => { 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 929a2758..01a2be10 100644 --- a/bitcoin_client_js/src/index.ts +++ b/bitcoin_client_js/src/index.ts @@ -1,4 +1,4 @@ -import AppClient from './lib/appClient'; +import AppClient, { PartialSignature } from './lib/appClient'; import { DefaultDescriptorTemplate, DefaultWalletPolicy, @@ -11,6 +11,7 @@ export { PsbtV2, DefaultDescriptorTemplate, DefaultWalletPolicy, + PartialSignature, WalletPolicy }; diff --git a/bitcoin_client_js/src/lib/appClient.ts b/bitcoin_client_js/src/lib/appClient.ts index 617703fa..bb369779 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'; @@ -11,7 +15,7 @@ import { createVarint, parseVarint } from './varint'; const CLA_BTC = 0xe1; const CLA_FRAMEWORK = 0xf8; -const CURRENT_PROTOCOL_VERSION = 1; // from supported from version 2.1.0 of the app +const CURRENT_PROTOCOL_VERSION = 1; // supported from version 2.1.0 of the app enum BitcoinIns { GET_PUBKEY = 0x00, @@ -26,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 @@ -70,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 @@ -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]; } /** @@ -181,29 +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 an array of of tuples with 3 elements containing: + * @returns an array of of tuples with 2 elements containing: * - the index of the input being signed; - * - a Buffer with either a 33-byte compressed pubkey or a 32-byte x-only pubkey whose corresponding secret key was used to sign; - * - a Buffer with the corresponding signature. + * - an instance of PartialSignature */ async signPsbt( - psbt: PsbtV2, + psbt: PsbtV2 | string | Buffer, walletPolicy: WalletPolicy, walletHMAC: Buffer | null, progressCallback?: () => void - ): Promise<[number, Buffer, Buffer][]> { + ): Promise<[number, PartialSignature][]> { + + if (typeof psbt === 'string') { + psbt = Buffer.from(psbt, "base64"); + } + + if (Buffer.isBuffer(psbt)) { + const psbtObj = new PsbtV2() + psbtObj.deserialize(psbt); + psbt = psbtObj; + } + const merkelizedPsbt = new MerkelizedPsbt(psbt); if (walletHMAC != null && walletHMAC.length != 32) { @@ -248,16 +340,18 @@ export class AppClient { const yielded = clientInterpreter.getYielded(); - const ret: [number, Buffer, Buffer][] = []; + const ret: [number, PartialSignature][] = []; for (const inputAndSig of yielded) { // inputAndSig contains: // const [inputIndex, inputIndexLen] = parseVarint(inputAndSig, 0); - const pubkeyLen = inputAndSig[inputIndexLen]; - const pubkey = inputAndSig.subarray(inputIndexLen + 1, inputIndexLen + 1 + pubkeyLen); - const signature = inputAndSig.subarray(inputIndexLen + 1 + pubkeyLen) + const pubkeyAugmLen = inputAndSig[inputIndexLen]; + const pubkeyAugm = inputAndSig.subarray(inputIndexLen + 1, inputIndexLen + 1 + pubkeyAugmLen); + const signature = inputAndSig.subarray(inputIndexLen + 1 + pubkeyAugmLen) + + const partialSig = makePartialSignature(pubkeyAugm, signature); - ret.push([Number(inputIndex), pubkey, signature]); + ret.push([Number(inputIndex), partialSig]); } return ret; } @@ -312,6 +406,77 @@ export class AppClient { return result.toString('base64'); } + + /* Performs any additional check on the generated address before returning it.*/ + private async validateAddress( + address: string, + walletPolicy: WalletPolicy, + change: number, + addressIndex: number + ) { + if (change !== 0 && change !== 1) + throw new Error('Change can only be 0 or 1'); + const isChange: boolean = change === 1; + if (addressIndex < 0 || !Number.isInteger(addressIndex)) + throw new Error('Invalid address index'); + const appAndVer = await this.getAppAndVersion(); + let network; + if (appAndVer.name === 'Bitcoin Test') { + network = networks.testnet; + } else if (appAndVer.name === 'Bitcoin') { + network = networks.bitcoin; + } else { + throw new Error( + `Invalid network: ${appAndVer.name}. Expected 'Bitcoin Test' or 'Bitcoin'.` + ); + } + let expression = walletPolicy.descriptorTemplate; + // Replace change: + expression = expression.replace(/\/\*\*/g, `/<0;1>/*`); + const regExpMN = new RegExp(`/<(\\d+);(\\d+)>`, 'g'); + let matchMN; + while ((matchMN = regExpMN.exec(expression)) !== null) { + const [M, N] = [parseInt(matchMN[1], 10), parseInt(matchMN[2], 10)]; + expression = expression.replace(`/<${M};${N}>`, `/${isChange ? N : M}`); + } + // Replace index: + expression = expression.replace(/\/\*/g, `/${addressIndex}`); + // Replace origin in reverse order to prevent + // misreplacements, e.g., @10 being mistaken for @1 and leaving a 0. + for (let i = walletPolicy.keys.length - 1; i >= 0; i--) + expression = expression.replace( + new RegExp(`@${i}`, 'g'), + walletPolicy.keys[i] + ); + let thirdPartyValidationApplicable = true; + let thirdPartyGeneratedAddress: string; + try { + thirdPartyGeneratedAddress = new Descriptor({ + expression, + network + }).getAddress(); + } catch (err) { + // Note: @bitcoinerlab/descriptors@1.0.x does not support Tapscript yet. + // These are the supported descriptors: + // - pkh(KEY) + // - wpkh(KEY) + // - sh(wpkh(KEY)) + // - sh(SCRIPT) + // - wsh(SCRIPT) + // - sh(wsh(SCRIPT)), where + // SCRIPT is any of the (non-tapscript) fragments in: https://bitcoin.sipa.be/miniscript/ + // + // Other expressions are not supported and third party validation would not be applicable: + thirdPartyValidationApplicable = false; + } + if ( + thirdPartyValidationApplicable && + address !== thirdPartyGeneratedAddress + ) + throw new Error( + `Third party address validation mismatch: ${address} != ${thirdPartyGeneratedAddress}` + ); + } } export default AppClient; diff --git a/bitcoin_client_js/src/lib/bip32.ts b/bitcoin_client_js/src/lib/bip32.ts index 0275bb17..091cf66f 100644 --- a/bitcoin_client_js/src/lib/bip32.ts +++ b/bitcoin_client_js/src/lib/bip32.ts @@ -32,7 +32,7 @@ export function pathStringToArray(path: string): readonly number[] { } export function pubkeyFromXpub(xpub: string): Buffer { - const xpubBuf = bs58check.decode(xpub); + const xpubBuf = Buffer.from(bs58check.decode(xpub)); return xpubBuf.slice(xpubBuf.length - 33); } @@ -41,7 +41,7 @@ export function getXpubComponents(xpub: string): { readonly pubkey: Buffer; readonly version: number; } { - const xpubBuf: Buffer = bs58check.decode(xpub); + const xpubBuf = Buffer.from(bs58check.decode(xpub)); return { chaincode: xpubBuf.slice(13, 13 + 32), pubkey: xpubBuf.slice(xpubBuf.length - 33), diff --git a/bitcoin_client_js/src/lib/psbtv2.ts b/bitcoin_client_js/src/lib/psbtv2.ts index eaed937a..c4bf84be 100644 --- a/bitcoin_client_js/src/lib/psbtv2.ts +++ b/bitcoin_client_js/src/lib/psbtv2.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import bjs from 'bitcoinjs-lib'; +import * as bjs from 'bitcoinjs-lib'; import { BufferReader, @@ -11,6 +11,8 @@ import { import { sanitizeBigintToNumber } from './varint'; export enum psbtGlobal { + UNSIGNED_TX = 0x00, + XPUB = 0x01, TX_VERSION = 0x02, FALLBACK_LOCKTIME = 0x03, INPUT_COUNT = 0x04, @@ -36,6 +38,7 @@ export enum psbtIn { } export enum psbtOut { REDEEM_SCRIPT = 0x00, + WITNESS_SCRIPT = 0x01, BIP_32_DERIVATION = 0x02, AMOUNT = 0x03, SCRIPT = 0x04, @@ -367,14 +370,74 @@ export class PsbtV2 { throw new Error('Invalid magic bytes'); } while (this.readKeyPair(this.globalMap, buf)); - for (let i = 0; i < this.getGlobalInputCount(); i++) { + + let psbtVersion: number; + try { + psbtVersion = this.getGlobalPsbtVersion(); + } catch { + psbtVersion = 0; + } + + if (psbtVersion !== 0 && psbtVersion !== 2) throw new Error("Only PSBTs of version 0 or 2 are supported"); + + let nInputs: number; + let nOutputs: number; + if (psbtVersion == 0) { + // if PSBTv0, we parse the PSBT_GLOBAL_UNSIGNED_TX field + const txRaw = this.getGlobal(psbtGlobal.UNSIGNED_TX); + const tx = bjs.Transaction.fromBuffer(txRaw); + nInputs = tx.ins.length; + nOutputs = tx.outs.length + } else { + // if PSBTv2, we already have the counts + nInputs = this.getGlobalInputCount(); + nOutputs = this.getGlobalOutputCount(); + } + + for (let i = 0; i < nInputs; i++) { this.inputMaps[i] = new Map(); while (this.readKeyPair(this.inputMaps[i], buf)); } - for (let i = 0; i < this.getGlobalOutputCount(); i++) { + for (let i = 0; i < nOutputs; i++) { this.outputMaps[i] = new Map(); while (this.readKeyPair(this.outputMaps[i], buf)); } + + this.normalizeToV2(); + } + normalizeToV2() { + // if the psbt is a PsbtV0, convert it to PsbtV2 instead. + // throw an error for any version other than 0 or 2, + const psbtVersion = this.getGlobalOptional(psbtGlobal.VERSION)?.readInt32LE(0); + if (psbtVersion === 2) return; + else if (psbtVersion !== undefined) { + throw new Error('Invalid or unsupported value for PSBT_GLOBAL_VERSION'); + } + + // Convert PsbtV0 to PsbtV2 by parsing the PSBT_GLOBAL_UNSIGNED_TX field + // and filling in the corresponding fields. + const txRaw = this.getGlobal(psbtGlobal.UNSIGNED_TX); + const tx = bjs.Transaction.fromBuffer(txRaw); + + this.setGlobalPsbtVersion(2); + this.setGlobalTxVersion(tx.version); + this.setGlobalFallbackLocktime(tx.locktime); + this.setGlobalInputCount(tx.ins.length); + this.setGlobalOutputCount(tx.outs.length); + + for (let i = 0; i < tx.ins.length; i++) { + this.setInputPreviousTxId(i, tx.ins[i].hash); + this.setInputOutputIndex(i, tx.ins[i].index); + this.setInputSequence(i, tx.ins[i].sequence); + } + + for (let i = 0; i < tx.outs.length; i++) { + this.setOutputAmount(i, tx.outs[i].value); + this.setOutputScript(i, tx.outs[i].script); + } + + // PSBT_GLOBAL_UNSIGNED_TX must be removed in a valid PSBTv2 + this.globalMap.delete(psbtGlobal.UNSIGNED_TX.toString(16).padStart(2, '0')); } /** * Imports a BitcoinJS (bitcoinjs-lib) Psbt object. @@ -467,6 +530,7 @@ export class PsbtV2 { const keyData = buf.readSlice(keyLen - 1); const value = buf.readVarSlice(); set(map, keyType, keyData, value); + return true; } private getKeyDatas( @@ -654,8 +718,8 @@ function createKey(buf: Buffer): Key { return new Key(buf.readUInt8(0), buf.slice(1)); } function serializeMap(buf: BufferWriter, map: ReadonlyMap) { - for (const key of map.keys()) { - const value = map.get(key)!; + // serialize in lexicographical order of keys + for (let [key, value] of [...map].sort(([k1], [k2]) => k1.localeCompare(k2))) { const keyPair = new KeyPair(createKey(Buffer.from(key, 'hex')), value); keyPair.serialize(buf); } diff --git a/bitcoin_client_rs/Cargo.toml b/bitcoin_client_rs/Cargo.toml index 4d37f86e..78658c07 100644 --- a/bitcoin_client_rs/Cargo.toml +++ b/bitcoin_client_rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ledger_bitcoin_client" -version = "0.1.2" +version = "0.3.2" authors = ["Edouard Paris "] edition = "2018" description = "Ledger Bitcoin application client" @@ -9,18 +9,27 @@ license = "Apache-2.0" documentation = "https://docs.rs/ledger_bitcoin_client/" [features] -default = ["async"] +default = ["async", "paranoid_client"] async = ["async-trait"] +# The paranoid_client feature makes sure that the client independently derives wallet +# policy addresses using rust-miniscript, returning an error if they do not match. +# It is strongly recommended to not disable this feature, unless the same check is +# performed elsewhere. +# Read more at https://donjon.ledger.com/lsb/019/ +paranoid_client = ["miniscript"] + [dependencies] async-trait = { version = "0.1", optional = true } -bitcoin = { version = "0.29.1", default-features = false, features = ["no-std"] } +bitcoin = { version = "0.30", default-features = false, features = ["no-std"] } +miniscript = { version = "10.0", optional = true, default-features = false, features = ["no-std"] } [workspace] members = ["examples/ledger_hwi"] # Dependencies used for tests and examples only. [dev-dependencies] +hex = "0.4.3" tokio = { version = "1.21", features = ["macros", "rt", "rt-multi-thread"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/bitcoin_client_rs/examples/ledger_hwi/Cargo.toml b/bitcoin_client_rs/examples/ledger_hwi/Cargo.toml index 6c193e93..1fe45aa0 100644 --- a/bitcoin_client_rs/examples/ledger_hwi/Cargo.toml +++ b/bitcoin_client_rs/examples/ledger_hwi/Cargo.toml @@ -8,7 +8,8 @@ edition = "2018" clap = { version = "4.0.18", features = ["derive"] } ledger_bitcoin_client = { path = "../.." } async-trait = { version = "0.1"} -bitcoin = { version = "0.29.1", default-features = false, features = ["no-std"] } +bitcoin = { version = "0.30", default-features = false, features = ["no-std"] } +hex = "0.4" base64 = "0.13.0" ledger-apdu = "0.10" ledger-transport-hid = "0.10" diff --git a/bitcoin_client_rs/examples/ledger_hwi/src/main.rs b/bitcoin_client_rs/examples/ledger_hwi/src/main.rs index 5326b4af..0b53ed52 100644 --- a/bitcoin_client_rs/examples/ledger_hwi/src/main.rs +++ b/bitcoin_client_rs/examples/ledger_hwi/src/main.rs @@ -2,11 +2,7 @@ use std::error::Error; use std::str::FromStr; use std::sync::Arc; -use bitcoin::{ - consensus::encode::deserialize, - hashes::hex::{FromHex, ToHex}, - util::{bip32, psbt::Psbt}, -}; +use bitcoin::{bip32, hashes::hex::FromHex, psbt::Psbt}; use hidapi::HidApi; use ledger_transport_hid::TransportNativeHID; @@ -14,6 +10,7 @@ use regex::Regex; use ledger_bitcoin_client::{ async_client::{BitcoinClient, Transport}, + psbt::PartialSignature, wallet::{Version, WalletPolicy, WalletPubKey}, }; @@ -86,7 +83,7 @@ async fn main() { "name: {}\nversion: {}\nflags: {}", name, version, - flags.to_hex() + hex::encode(flags) ); } Some(Commands::GetFingerprint) => { @@ -151,7 +148,7 @@ async fn register_wallet( .register_wallet(&wallet) .await .map_err(|e| format!("{:#?}", e))?; - println!("{}", hmac.to_hex()); + println!("{}", hex::encode(hmac)); Ok(()) } @@ -162,7 +159,7 @@ async fn sign( policy: &str, hmac: Option<&str>, ) -> Result<(), Box> { - let psbt: Psbt = deserialize(&base64::decode(&psbt)?).map_err(|e| format!("{:#?}", e))?; + let psbt = Psbt::deserialize(&base64::decode(&psbt)?).map_err(|e| format!("{:#?}", e))?; let (descriptor_template, keys) = extract_keys_and_template(policy)?; let wallet = WalletPolicy::new(name.to_string(), Version::V2, descriptor_template, keys); let hmac = if let Some(s) = hmac { @@ -178,8 +175,23 @@ async fn sign( .await .map_err(|e| format!("{:#?}", e))?; - for (index, key, sig) in res { - println!("index: {}, key: {}, sig: {}", index, key, sig); + for (index, sig) in res { + match sig { + PartialSignature::Sig(key, sig) => { + println!("index: {}, key: {}, sig: {}", index, key, sig); + } + PartialSignature::TapScriptSig(key, tapleaf_hash, sig) => { + println!( + "index: {}, key: {}, tapleaf_hash: {}, sig: {}", + index, + key, + tapleaf_hash + .map(|h| hex::encode(h)) + .unwrap_or("none".to_string()), + hex::encode(sig.to_vec()) + ); + } + } } Ok(()) } @@ -190,7 +202,9 @@ fn extract_keys_and_template(policy: &str) -> Result<(String, Vec) let mut pubkeys: Vec = Vec::new(); for (index, capture) in re.find_iter(policy).enumerate() { let pubkey = WalletPubKey::from_str(capture.as_str()).map_err(|e| format!("{}", e))?; - pubkeys.push(pubkey); + if !pubkeys.contains(&pubkey) { + pubkeys.push(pubkey); + } descriptor_template = descriptor_template.replace(capture.as_str(), &format!("@{}", index)); } if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once("#") { diff --git a/bitcoin_client_rs/src/async_client.rs b/bitcoin_client_rs/src/async_client.rs index c4102515..0f90ff3e 100644 --- a/bitcoin_client_rs/src/async_client.rs +++ b/bitcoin_client_rs/src/async_client.rs @@ -4,16 +4,16 @@ use core::str::FromStr; use async_trait::async_trait; use bitcoin::{ + address, + bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, consensus::encode::{deserialize_partial, VarInt}, + psbt::PartiallySignedTransaction as Psbt, secp256k1::ecdsa::Signature, - util::{ - bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, - ecdsa::EcdsaSig, - psbt::PartiallySignedTransaction as Psbt, - }, - PublicKey, }; +#[cfg(feature = "paranoid_client")] +use miniscript::{Descriptor, DescriptorPublicKey}; + use crate::{ apdu::{APDUCommand, StatusWord}, command, @@ -68,6 +68,37 @@ impl BitcoinClient { } } + // Verifies that the address that the application returns matches the one independently + // computed on the client + #[cfg(feature = "paranoid_client")] + async fn check_address( + &self, + wallet: &WalletPolicy, + change: bool, + address_index: u32, + expected_address: &bitcoin::Address, + ) -> Result<(), BitcoinClientError> { + let desc_str = wallet + .get_descriptor(change) + .map_err(|_| BitcoinClientError::ClientError("Failed to get descriptor".to_string()))?; + let descriptor = Descriptor::::from_str(&desc_str).map_err(|_| { + BitcoinClientError::ClientError("Failed to parse descriptor".to_string()) + })?; + + if descriptor + .at_derivation_index(address_index) + .map_err(|_| { + BitcoinClientError::ClientError("Failed to derive descriptor".to_string()) + })? + .script_pubkey() + != expected_address.payload.script_pubkey() + { + return Err(BitcoinClientError::InvalidResponse("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new".to_string())); + } + + Ok(()) + } + /// Returns the currently running app's name, version and state flags pub async fn get_version( &self, @@ -77,7 +108,7 @@ impl BitcoinClient { if data.is_empty() || data[0] != 0x01 { return Err(BitcoinClientError::UnexpectedResult { command: cmd.ins, - data: data.clone(), + data, }); } @@ -110,9 +141,18 @@ impl BitcoinClient { &self, ) -> Result> { let cmd = command::get_master_fingerprint(); - self.make_request(&cmd, None) - .await - .map(|data| Fingerprint::from(data.as_slice())) + self.make_request(&cmd, None).await.and_then(|data| { + if data.len() < 4 { + Err(BitcoinClientError::UnexpectedResult { + command: cmd.ins, + data, + }) + } else { + let mut fg = [0x00; 4]; + fg.copy_from_slice(&data[0..4]); + Ok(Fingerprint::from(fg)) + } + }) } /// Retrieve the bip32 extended pubkey derived with the given path @@ -145,7 +185,8 @@ impl BitcoinClient { intpr.add_known_list(&keys); //necessary for version 1 of the protocol (introduced in version 2.1.0) intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec()); - self.make_request(&cmd, Some(&mut intpr)) + let (id, hmac) = self + .make_request(&cmd, Some(&mut intpr)) .await .and_then(|data| { if data.len() < 64 { @@ -156,11 +197,22 @@ impl BitcoinClient { } else { let mut id = [0x00; 32]; id.copy_from_slice(&data[0..32]); - let mut hash = [0x00; 32]; - hash.copy_from_slice(&data[32..64]); - Ok((id, hash)) + let mut hmac = [0x00; 32]; + hmac.copy_from_slice(&data[32..64]); + Ok((id, hmac)) } - }) + })?; + + #[cfg(feature = "paranoid_client")] + { + let device_addr = self + .get_wallet_address(wallet, Some(&hmac), false, 0, false) + .await?; + + self.check_address(wallet, false, 0, &device_addr).await?; + } + + Ok((id, hmac)) } /// For a given wallet that was already registered on the device (or a standard wallet that does not need registration), @@ -172,7 +224,7 @@ impl BitcoinClient { change: bool, address_index: u32, display: bool, - ) -> Result> { + ) -> Result, BitcoinClientError> { let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -180,7 +232,8 @@ impl BitcoinClient { // necessary for version 1 of the protocol (introduced in version 2.1.0) intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec()); let cmd = command::get_wallet_address(wallet, wallet_hmac, change, address_index, display); - self.make_request(&cmd, Some(&mut intpr)) + let address = self + .make_request(&cmd, Some(&mut intpr)) .await .and_then(|data| { bitcoin::Address::from_str(&String::from_utf8_lossy(&data)).map_err(|_| { @@ -189,7 +242,15 @@ impl BitcoinClient { data, } }) - }) + })?; + + #[cfg(feature = "paranoid_client")] + { + self.check_address(wallet, change, address_index, &address) + .await?; + } + + Ok(address) } /// Signs a PSBT using a registered wallet (or a standard wallet that does not need registration). @@ -200,7 +261,7 @@ impl BitcoinClient { psbt: &Psbt, wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, - ) -> Result, BitcoinClientError> { + ) -> Result, BitcoinClientError> { let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -210,7 +271,7 @@ impl BitcoinClient { let global_map: Vec<(Vec, Vec)> = get_v2_global_pairs(psbt) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&global_map); let global_mapping_commitment = get_merkleized_map_commitment(&global_map); @@ -224,7 +285,7 @@ impl BitcoinClient { .ok_or(BitcoinClientError::InvalidPsbt)?; let input_map: Vec<(Vec, Vec)> = get_v2_input_pairs(input, txin) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&input_map); input_commitments.push(get_merkleized_map_commitment(&input_map)); @@ -240,7 +301,7 @@ impl BitcoinClient { .ok_or(BitcoinClientError::InvalidPsbt)?; let output_map: Vec<(Vec, Vec)> = get_v2_output_pairs(output, txout) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&output_map); output_commitments.push(get_merkleized_map_commitment(&output_map)); @@ -272,40 +333,21 @@ impl BitcoinClient { let mut signatures = Vec::new(); for result in results { - let (input_index, i1): (VarInt, usize) = + let (input_index, i): (VarInt, usize) = deserialize_partial(&result).map_err(|_| BitcoinClientError::UnexpectedResult { command: cmd.ins, data: result.clone(), })?; - let key_byte = result.get(i1).ok_or(BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - })?; - let key_len = u8::from_le_bytes([*key_byte]) as usize; - - if i1 + 1 + key_len > result.len() { - return Err(BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - }); - } - - let key = PublicKey::from_slice(&result[i1 + 1..i1 + 1 + key_len]).map_err(|_| { - BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - } - })?; - - let sig = EcdsaSig::from_slice(&result[i1 + 1 + key_len..]).map_err(|_| { - BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - } - })?; - - signatures.push((input_index.0 as usize, key, sig)); + signatures.push(( + input_index.0 as usize, + PartialSignature::from_slice(&result[i..]).map_err(|_| { + BitcoinClientError::UnexpectedResult { + command: cmd.ins, + data: result.clone(), + } + })?, + )); } Ok(signatures) diff --git a/bitcoin_client_rs/src/client.rs b/bitcoin_client_rs/src/client.rs index 9e5b9829..eb41f132 100644 --- a/bitcoin_client_rs/src/client.rs +++ b/bitcoin_client_rs/src/client.rs @@ -2,16 +2,16 @@ use core::fmt::Debug; use core::str::FromStr; use bitcoin::{ + address, + bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, consensus::encode::{deserialize_partial, VarInt}, - secp256k1::ecdsa::Signature, - util::{ - bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, - ecdsa::EcdsaSig, - psbt::PartiallySignedTransaction as Psbt, - }, - PublicKey, + psbt::PartiallySignedTransaction as Psbt, + secp256k1::ecdsa, }; +#[cfg(feature = "paranoid_client")] +use miniscript::{Descriptor, DescriptorPublicKey}; + use crate::{ apdu::{APDUCommand, StatusWord}, command, @@ -63,6 +63,37 @@ impl BitcoinClient { } } + // Verifies that the address that the application returns matches the one independently + // computed on the client + #[cfg(feature = "paranoid_client")] + fn check_address( + &self, + wallet: &WalletPolicy, + change: bool, + address_index: u32, + expected_address: &bitcoin::Address, + ) -> Result<(), BitcoinClientError> { + let desc_str = wallet + .get_descriptor(change) + .map_err(|_| BitcoinClientError::ClientError("Failed to get descriptor".to_string()))?; + let descriptor = Descriptor::::from_str(&desc_str).map_err(|_| { + BitcoinClientError::ClientError("Failed to parse descriptor".to_string()) + })?; + + if descriptor + .at_derivation_index(address_index) + .map_err(|_| { + BitcoinClientError::ClientError("Failed to derive descriptor".to_string()) + })? + .script_pubkey() + != expected_address.payload.script_pubkey() + { + return Err(BitcoinClientError::InvalidResponse("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new".to_string())); + } + + Ok(()) + } + /// Returns the currently running app's name, version and state flags pub fn get_version(&self) -> Result<(String, String, Vec), BitcoinClientError> { let cmd = command::get_version(); @@ -70,7 +101,7 @@ impl BitcoinClient { if data.is_empty() || data[0] != 0x01 { return Err(BitcoinClientError::UnexpectedResult { command: cmd.ins, - data: data.clone(), + data, }); } @@ -101,8 +132,18 @@ impl BitcoinClient { /// Retrieve the master fingerprint. pub fn get_master_fingerprint(&self) -> Result> { let cmd = command::get_master_fingerprint(); - self.make_request(&cmd, None) - .map(|data| Fingerprint::from(data.as_slice())) + self.make_request(&cmd, None).and_then(|data| { + if data.len() < 4 { + Err(BitcoinClientError::UnexpectedResult { + command: cmd.ins, + data, + }) + } else { + let mut fg = [0x00; 4]; + fg.copy_from_slice(&data[0..4]); + Ok(Fingerprint::from(fg)) + } + }) } /// Retrieve the bip32 extended pubkey derived with the given path @@ -136,7 +177,7 @@ impl BitcoinClient { intpr.add_known_list(&keys); // necessary for version 1 of the protocol (introduced in version 2.1.0) intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec()); - self.make_request(&cmd, Some(&mut intpr)).and_then(|data| { + let (id, hmac) = self.make_request(&cmd, Some(&mut intpr)).and_then(|data| { if data.len() < 64 { Err(BitcoinClientError::UnexpectedResult { command: cmd.ins, @@ -145,11 +186,19 @@ impl BitcoinClient { } else { let mut id = [0x00; 32]; id.copy_from_slice(&data[0..32]); - let mut hash = [0x00; 32]; - hash.copy_from_slice(&data[32..64]); - Ok((id, hash)) + let mut hmac = [0x00; 32]; + hmac.copy_from_slice(&data[32..64]); + Ok((id, hmac)) } - }) + })?; + + #[cfg(feature = "paranoid_client")] + { + let device_addr = self.get_wallet_address(wallet, Some(&hmac), false, 0, false)?; + self.check_address(wallet, false, 0, &device_addr)?; + } + + Ok((id, hmac)) } /// For a given wallet that was already registered on the device (or a standard wallet that does not need registration), @@ -161,7 +210,7 @@ impl BitcoinClient { change: bool, address_index: u32, display: bool, - ) -> Result> { + ) -> Result, BitcoinClientError> { let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -169,14 +218,20 @@ impl BitcoinClient { // necessary for version 1 of the protocol (introduced in version 2.1.0) intpr.add_known_preimage(wallet.descriptor_template.as_bytes().to_vec()); let cmd = command::get_wallet_address(wallet, wallet_hmac, change, address_index, display); - self.make_request(&cmd, Some(&mut intpr)).and_then(|data| { - bitcoin::Address::from_str(&String::from_utf8_lossy(&data)).map_err(|_| { - BitcoinClientError::UnexpectedResult { + let address = self.make_request(&cmd, Some(&mut intpr)).and_then(|data| { + bitcoin::Address::::from_str(&String::from_utf8_lossy(&data)) + .map_err(|_| BitcoinClientError::UnexpectedResult { command: cmd.ins, data, - } - }) - }) + }) + })?; + + #[cfg(feature = "paranoid_client")] + { + self.check_address(wallet, change, address_index, &address)?; + } + + Ok(address) } /// Signs a PSBT using a registered wallet (or a standard wallet that does not need registration). @@ -187,7 +242,7 @@ impl BitcoinClient { psbt: &Psbt, wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, - ) -> Result, BitcoinClientError> { + ) -> Result, BitcoinClientError> { let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -197,7 +252,7 @@ impl BitcoinClient { let global_map: Vec<(Vec, Vec)> = get_v2_global_pairs(psbt) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&global_map); let global_mapping_commitment = get_merkleized_map_commitment(&global_map); @@ -211,7 +266,7 @@ impl BitcoinClient { .ok_or(BitcoinClientError::InvalidPsbt)?; let input_map: Vec<(Vec, Vec)> = get_v2_input_pairs(input, txin) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&input_map); input_commitments.push(get_merkleized_map_commitment(&input_map)); @@ -227,7 +282,7 @@ impl BitcoinClient { .ok_or(BitcoinClientError::InvalidPsbt)?; let output_map: Vec<(Vec, Vec)> = get_v2_output_pairs(output, txout) .into_iter() - .map(deserialize_pairs) + .map(deserialize_pair) .collect(); intpr.add_known_mapping(&output_map); output_commitments.push(get_merkleized_map_commitment(&output_map)); @@ -259,40 +314,21 @@ impl BitcoinClient { let mut signatures = Vec::new(); for result in results { - let (input_index, i1): (VarInt, usize) = + let (input_index, i): (VarInt, usize) = deserialize_partial(&result).map_err(|_| BitcoinClientError::UnexpectedResult { command: cmd.ins, data: result.clone(), })?; - let key_byte = result.get(i1).ok_or(BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - })?; - let key_len = u8::from_le_bytes([*key_byte]) as usize; - - if i1 + 1 + key_len > result.len() { - return Err(BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - }); - } - - let key = PublicKey::from_slice(&result[i1 + 1..i1 + 1 + key_len]).map_err(|_| { - BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - } - })?; - - let sig = EcdsaSig::from_slice(&result[i1 + 1 + key_len..]).map_err(|_| { - BitcoinClientError::UnexpectedResult { - command: cmd.ins, - data: result.clone(), - } - })?; - - signatures.push((input_index.0 as usize, key, sig)); + signatures.push(( + input_index.0 as usize, + PartialSignature::from_slice(&result[i..]).map_err(|_| { + BitcoinClientError::UnexpectedResult { + command: cmd.ins, + data: result.clone(), + } + })?, + )); } Ok(signatures) @@ -304,7 +340,7 @@ impl BitcoinClient { &self, message: &[u8], path: &DerivationPath, - ) -> Result<(u8, Signature), BitcoinClientError> { + ) -> Result<(u8, ecdsa::Signature), BitcoinClientError> { let chunks: Vec<&[u8]> = message.chunks(64).collect(); let mut intpr = ClientCommandInterpreter::new(); let message_commitment_root = intpr.add_known_list(&chunks); @@ -312,7 +348,7 @@ impl BitcoinClient { self.make_request(&cmd, Some(&mut intpr)).and_then(|data| { Ok(( data[0], - Signature::from_compact(&data[1..]).map_err(|_| { + ecdsa::Signature::from_compact(&data[1..]).map_err(|_| { BitcoinClientError::UnexpectedResult { command: cmd.ins, data: data.to_vec(), diff --git a/bitcoin_client_rs/src/command.rs b/bitcoin_client_rs/src/command.rs index dcacaad6..6d54c794 100644 --- a/bitcoin_client_rs/src/command.rs +++ b/bitcoin_client_rs/src/command.rs @@ -1,8 +1,8 @@ /// APDU commands for the Bitcoin application. /// use bitcoin::{ + bip32::{ChildNumber, DerivationPath}, consensus::encode::{self, VarInt}, - util::bip32::{ChildNumber, DerivationPath}, }; use core::default::Default; diff --git a/bitcoin_client_rs/src/error.rs b/bitcoin_client_rs/src/error.rs index e9c5c437..fce642c6 100644 --- a/bitcoin_client_rs/src/error.rs +++ b/bitcoin_client_rs/src/error.rs @@ -4,11 +4,14 @@ use crate::{apdu::StatusWord, interpreter::InterpreterError}; #[derive(Debug)] pub enum BitcoinClientError { + ClientError(String), InvalidPsbt, Transport(T), Interpreter(InterpreterError), Device { command: u8, status: StatusWord }, UnexpectedResult { command: u8, data: Vec }, + InvalidResponse(String), + UnsupportedAppVersion, } impl From for BitcoinClientError { diff --git a/bitcoin_client_rs/src/interpreter.rs b/bitcoin_client_rs/src/interpreter.rs index d3d16d6b..3bf01456 100644 --- a/bitcoin_client_rs/src/interpreter.rs +++ b/bitcoin_client_rs/src/interpreter.rs @@ -43,7 +43,7 @@ impl ClientCommandInterpreter { pub fn add_known_preimage(&mut self, element: Vec) { let mut engine = sha256::Hash::engine(); engine.input(&element); - let hash = sha256::Hash::from_engine(engine).into_inner(); + let hash = sha256::Hash::from_engine(engine).to_byte_array(); self.known_preimages.push((hash, element)); } @@ -62,7 +62,7 @@ impl ClientCommandInterpreter { preimage.extend_from_slice(element.as_ref()); let mut engine = sha256::Hash::engine(); engine.input(&preimage); - let hash = sha256::Hash::from_engine(engine).into_inner(); + let hash = sha256::Hash::from_engine(engine).to_byte_array(); self.known_preimages.push((hash, preimage)); leaves.push(hash); } @@ -288,13 +288,13 @@ pub fn get_merkleized_map_commitment(mapping: &[(Vec, Vec)]) -> Vec preimage.extend_from_slice(key); let mut engine = sha256::Hash::engine(); engine.input(&preimage); - keys_hashes.push(sha256::Hash::from_engine(engine).into_inner()); + keys_hashes.push(sha256::Hash::from_engine(engine).to_byte_array()); let mut preimage = vec![0x00]; preimage.extend_from_slice(value); let mut engine = sha256::Hash::engine(); engine.input(&preimage); - values_hashes.push(sha256::Hash::from_engine(engine).into_inner()); + values_hashes.push(sha256::Hash::from_engine(engine).to_byte_array()); } let mut commitment = encode::serialize(&VarInt(sorted.len() as u64)); diff --git a/bitcoin_client_rs/src/lib.rs b/bitcoin_client_rs/src/lib.rs index 8c7fbd42..dc751c4c 100644 --- a/bitcoin_client_rs/src/lib.rs +++ b/bitcoin_client_rs/src/lib.rs @@ -1,11 +1,11 @@ mod command; mod interpreter; mod merkle; -mod psbt; pub mod apdu; pub mod client; pub mod error; +pub mod psbt; pub mod wallet; #[cfg(feature = "async")] diff --git a/bitcoin_client_rs/src/merkle.rs b/bitcoin_client_rs/src/merkle.rs index 3bc52438..a82766d2 100644 --- a/bitcoin_client_rs/src/merkle.rs +++ b/bitcoin_client_rs/src/merkle.rs @@ -84,7 +84,7 @@ impl Tree { let mut engine = sha256::Hash::engine(); engine.input(input.as_slice()); - let value = sha256::Hash::from_engine(engine).into_inner(); + let value = sha256::Hash::from_engine(engine).to_byte_array(); Tree::Node { height: lchild.height() + 1, left: Box::new(lchild), @@ -215,7 +215,7 @@ mod tests { let mut engine = sha256::Hash::engine(); engine.input(input.as_slice()); - let value = sha256::Hash::from_engine(engine).into_inner(); + let value = sha256::Hash::from_engine(engine).to_byte_array(); assert_eq!(tree.get_leaf_proof(2), Some(vec![value.to_vec()])); let _tree = MerkleTree::new(leaves.to_vec()); diff --git a/bitcoin_client_rs/src/psbt.rs b/bitcoin_client_rs/src/psbt.rs index d82a393d..eb82fe97 100644 --- a/bitcoin_client_rs/src/psbt.rs +++ b/bitcoin_client_rs/src/psbt.rs @@ -6,30 +6,39 @@ use bitcoin::{ blockdata::transaction::{TxIn, TxOut}, consensus::encode::{deserialize, serialize, VarInt}, - util::psbt::{raw, Input, Output, Psbt}, + ecdsa, + hashes::Hash, + key::Error as KeyError, + psbt::{raw, Input, Output, Psbt}, + secp256k1::{self, XOnlyPublicKey}, + taproot, + taproot::TapLeafHash, + PublicKey, }; +use serialize::Serialize; + #[rustfmt::skip] macro_rules! impl_psbt_get_pair { ($rv:ident.push($slf:ident.$unkeyed_name:ident, $unkeyed_typeval:ident)) => { if let Some(ref $unkeyed_name) = $slf.$unkeyed_name { - $rv.push(bitcoin::util::psbt::raw::Pair { - key: bitcoin::util::psbt::raw::Key { + $rv.push(bitcoin::psbt::raw::Pair { + key: bitcoin::psbt::raw::Key { type_value: $unkeyed_typeval, key: vec![], }, - value: bitcoin::util::psbt::serialize::Serialize::serialize($unkeyed_name), + value: Serialize::serialize($unkeyed_name), }); } }; ($rv:ident.push_map($slf:ident.$keyed_name:ident, $keyed_typeval:ident)) => { for (key, val) in &$slf.$keyed_name { - $rv.push(bitcoin::util::psbt::raw::Pair { - key: bitcoin::util::psbt::raw::Key { + $rv.push(bitcoin::psbt::raw::Pair { + key: bitcoin::psbt::raw::Key { type_value: $keyed_typeval, - key: bitcoin::util::psbt::serialize::Serialize::serialize(key), + key: Serialize::serialize(key), }, - value: bitcoin::util::psbt::serialize::Serialize::serialize(val), + value: Serialize::serialize(val), }); } }; @@ -66,7 +75,7 @@ pub fn get_v2_global_pairs(psbt: &Psbt) -> Vec { ret.extend(fingerprint.as_bytes()); derivation .into_iter() - .for_each(|n| ret.extend(&u32::from(*n).to_le_bytes())); + .for_each(|n| ret.extend(u32::from(*n).to_le_bytes())); ret }, }); @@ -374,6 +383,481 @@ pub fn get_v2_output_pairs(output: &Output, txout: &TxOut) -> Vec { rv } -pub fn deserialize_pairs(pair: raw::Pair) -> (Vec, Vec) { - (deserialize(&serialize(&pair.key)).unwrap(), pair.value) +pub fn deserialize_pair(pair: raw::Pair) -> (Vec, Vec) { + ( + deserialize(&Serialize::serialize(&pair.key)).unwrap(), + pair.value, + ) +} + +pub enum PartialSignature { + /// signature stored in pbst.partial_sigs + Sig(PublicKey, ecdsa::Signature), + /// signature stored in pbst.tap_script_sigs + TapScriptSig(XOnlyPublicKey, Option, taproot::Signature), +} + +impl PartialSignature { + pub fn from_slice(slice: &[u8]) -> Result { + let key_augment_byte = slice + .first() + .ok_or(PartialSignatureError::BadKeyAugmentLength)?; + let key_augment_len = u8::from_le_bytes([*key_augment_byte]) as usize; + + if key_augment_len >= slice.len() { + Err(PartialSignatureError::BadKeyAugmentLength) + } else if key_augment_len == 64 { + let key = XOnlyPublicKey::from_slice(&slice[1..33]) + .map_err(PartialSignatureError::XOnlyPubKey)?; + let tap_leaf_hash = + TapLeafHash::from_slice(&slice[33..65]).map_err(PartialSignatureError::TapLeaf)?; + let sig = taproot::Signature::from_slice(&slice[65..]) + .map_err(PartialSignatureError::TaprootSig)?; + Ok(Self::TapScriptSig(key, Some(tap_leaf_hash), sig)) + } else if key_augment_len == 32 { + let key = XOnlyPublicKey::from_slice(&slice[1..33]) + .map_err(PartialSignatureError::XOnlyPubKey)?; + let sig = taproot::Signature::from_slice(&slice[65..]) + .map_err(PartialSignatureError::TaprootSig)?; + Ok(Self::TapScriptSig(key, None, sig)) + } else { + let key = PublicKey::from_slice(&slice[1..key_augment_len + 1]) + .map_err(PartialSignatureError::PubKey)?; + let sig = ecdsa::Signature::from_slice(&slice[key_augment_len + 1..]) + .map_err(PartialSignatureError::EcdsaSig)?; + Ok(Self::Sig(key, sig)) + } + } +} + +pub enum PartialSignatureError { + BadKeyAugmentLength, + XOnlyPubKey(secp256k1::Error), + PubKey(KeyError), + EcdsaSig(ecdsa::Error), + TaprootSig(taproot::Error), + TapLeaf(bitcoin::hashes::Error), +} + +mod serialize { + use core::convert::{TryFrom, TryInto}; + + use bitcoin::{ + bip32::{ChildNumber, Fingerprint, KeySource}, + blockdata::{ + script::ScriptBuf, + transaction::{Transaction, TxOut}, + witness::Witness, + }, + consensus::encode::{self, deserialize_partial, serialize, Decodable, Encodable}, + ecdsa, + hashes::{hash160, ripemd160, sha256, sha256d, Hash}, + key::PublicKey, + psbt::{Error, PsbtSighashType}, + secp256k1::{self, XOnlyPublicKey}, + taproot, + taproot::{ControlBlock, LeafVersion, TapLeafHash, TapNodeHash, TapTree, TaprootBuilder}, + VarInt, + }; + + macro_rules! impl_psbt_de_serialize { + ($thing:ty) => { + impl_psbt_serialize!($thing); + impl_psbt_deserialize!($thing); + }; + } + + macro_rules! impl_psbt_deserialize { + ($thing:ty) => { + impl Deserialize for $thing { + fn deserialize(bytes: &[u8]) -> Result { + bitcoin::consensus::deserialize(&bytes[..]) + .map_err(|e| bitcoin::psbt::Error::from(e)) + } + } + }; + } + + macro_rules! impl_psbt_serialize { + ($thing:ty) => { + impl Serialize for $thing { + fn serialize(&self) -> Vec { + bitcoin::consensus::serialize(self) + } + } + }; + } + + // macros for serde of hashes + macro_rules! impl_psbt_hash_de_serialize { + ($hash_type:ty) => { + impl_psbt_hash_serialize!($hash_type); + impl_psbt_hash_deserialize!($hash_type); + }; + } + + macro_rules! impl_psbt_hash_deserialize { + ($hash_type:ty) => { + impl $crate::psbt::serialize::Deserialize for $hash_type { + fn deserialize(bytes: &[u8]) -> Result { + <$hash_type>::from_slice(&bytes[..]).map_err(|e| bitcoin::psbt::Error::from(e)) + } + } + }; + } + + macro_rules! impl_psbt_hash_serialize { + ($hash_type:ty) => { + impl $crate::psbt::serialize::Serialize for $hash_type { + fn serialize(&self) -> Vec { + self.as_byte_array().to_vec() + } + } + }; + } + + /// A trait for serializing a value as raw data for insertion into PSBT + /// key-value maps. + pub(crate) trait Serialize { + /// Serialize a value as raw data. + fn serialize(&self) -> Vec; + } + + /// A trait for deserializing a value from raw data in PSBT key-value maps. + pub(crate) trait Deserialize: Sized { + /// Deserialize a value from raw data. + fn deserialize(bytes: &[u8]) -> Result; + } + + impl_psbt_de_serialize!(Transaction); + impl_psbt_de_serialize!(TxOut); + impl_psbt_de_serialize!(Witness); + impl_psbt_hash_de_serialize!(ripemd160::Hash); + impl_psbt_hash_de_serialize!(sha256::Hash); + impl_psbt_hash_de_serialize!(TapLeafHash); + impl_psbt_hash_de_serialize!(TapNodeHash); + impl_psbt_hash_de_serialize!(hash160::Hash); + impl_psbt_hash_de_serialize!(sha256d::Hash); + + // taproot + impl_psbt_de_serialize!(Vec); + + impl Serialize for bitcoin::psbt::raw::Key { + fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + VarInt((self.key.len() + 1) as u64) + .consensus_encode(&mut buf) + .expect("in-memory writers don't error"); + + self.type_value + .consensus_encode(&mut buf) + .expect("in-memory writers don't error"); + + for key in &self.key { + key.consensus_encode(&mut buf) + .expect("in-memory writers don't error"); + } + + buf + } + } + + impl Serialize for ScriptBuf { + fn serialize(&self) -> Vec { + self.to_bytes() + } + } + + impl Deserialize for ScriptBuf { + fn deserialize(bytes: &[u8]) -> Result { + Ok(Self::from(bytes.to_vec())) + } + } + + impl Serialize for PublicKey { + fn serialize(&self) -> Vec { + let mut buf = Vec::new(); + self.write_into(&mut buf).expect("vecs don't error"); + buf + } + } + + impl Deserialize for PublicKey { + fn deserialize(bytes: &[u8]) -> Result { + PublicKey::from_slice(bytes).map_err(Error::InvalidPublicKey) + } + } + + impl Serialize for secp256k1::PublicKey { + fn serialize(&self) -> Vec { + self.serialize().to_vec() + } + } + + impl Deserialize for secp256k1::PublicKey { + fn deserialize(bytes: &[u8]) -> Result { + secp256k1::PublicKey::from_slice(bytes).map_err(Error::InvalidSecp256k1PublicKey) + } + } + + impl Serialize for ecdsa::Signature { + fn serialize(&self) -> Vec { + self.to_vec() + } + } + + impl Deserialize for ecdsa::Signature { + fn deserialize(bytes: &[u8]) -> Result { + // NB: Since BIP-174 says "the signature as would be pushed to the stack from + // a scriptSig or witness" we should ideally use a consensus deserialization and do + // not error on a non-standard values. However, + // + // 1) the current implementation of from_u32_consensus(`flag`) does not preserve + // the sighash byte `flag` mapping all unknown values to EcdsaSighashType::All or + // EcdsaSighashType::AllPlusAnyOneCanPay. Therefore, break the invariant + // EcdsaSig::from_slice(&sl[..]).to_vec = sl. + // + // 2) This would cause to have invalid signatures because the sighash message + // also has a field sighash_u32 (See BIP141). For example, when signing with non-standard + // 0x05, the sighash message would have the last field as 0x05u32 while, the verification + // would use check the signature assuming sighash_u32 as `0x01`. + ecdsa::Signature::from_slice(bytes).map_err(|e| match e { + ecdsa::Error::EmptySignature => Error::InvalidEcdsaSignature(e), + ecdsa::Error::NonStandardSighashType(flag) => Error::NonStandardSighashType(flag), + ecdsa::Error::Secp256k1(..) => Error::InvalidEcdsaSignature(e), + ecdsa::Error::HexEncoding(..) => { + unreachable!("Decoding from slice, not hex") + } + _ => Error::InvalidEcdsaSignature(e), + }) + } + } + + impl Serialize for KeySource { + fn serialize(&self) -> Vec { + let mut rv: Vec = Vec::with_capacity(key_source_len(self)); + + rv.append(&mut self.0.to_bytes().to_vec()); + + for cnum in self.1.into_iter() { + rv.append(&mut serialize(&u32::from(*cnum))) + } + + rv + } + } + + impl Deserialize for KeySource { + fn deserialize(bytes: &[u8]) -> Result { + if bytes.len() < 4 { + return Err(Error::ConsensusEncoding( + bitcoin::consensus::encode::Error::ParseFailed( + "Not enough bytes for key source", + ), + )); + } + + let fprint: Fingerprint = bytes[0..4].try_into().expect("4 is the fingerprint length"); + let mut dpath: Vec = Default::default(); + + let mut d = &bytes[4..]; + while !d.is_empty() { + match u32::consensus_decode(&mut d) { + Ok(index) => dpath.push(index.into()), + Err(e) => return Err(e)?, + } + } + + Ok((fprint, dpath.into())) + } + } + + // partial sigs + impl Serialize for Vec { + fn serialize(&self) -> Vec { + self.clone() + } + } + + impl Deserialize for Vec { + fn deserialize(bytes: &[u8]) -> Result { + Ok(bytes.to_vec()) + } + } + + impl Serialize for PsbtSighashType { + fn serialize(&self) -> Vec { + serialize(&self.to_u32()) + } + } + + impl Deserialize for PsbtSighashType { + fn deserialize(bytes: &[u8]) -> Result { + let raw: u32 = encode::deserialize(bytes)?; + Ok(PsbtSighashType::from_u32(raw)) + } + } + + // Taproot related ser/deser + impl Serialize for XOnlyPublicKey { + fn serialize(&self) -> Vec { + XOnlyPublicKey::serialize(self).to_vec() + } + } + + impl Deserialize for XOnlyPublicKey { + fn deserialize(bytes: &[u8]) -> Result { + XOnlyPublicKey::from_slice(bytes).map_err(|_| Error::InvalidXOnlyPublicKey) + } + } + + impl Serialize for taproot::Signature { + fn serialize(&self) -> Vec { + self.to_vec() + } + } + + impl Deserialize for taproot::Signature { + fn deserialize(bytes: &[u8]) -> Result { + taproot::Signature::from_slice(bytes).map_err(Error::InvalidTaprootSignature) + } + } + + impl Serialize for (XOnlyPublicKey, TapLeafHash) { + fn serialize(&self) -> Vec { + let ser_pk = self.0.serialize(); + let mut buf = Vec::with_capacity(ser_pk.len() + self.1.as_byte_array().len()); + buf.extend(ser_pk); + buf.extend(self.1.as_byte_array()); + buf + } + } + + impl Deserialize for (XOnlyPublicKey, TapLeafHash) { + fn deserialize(bytes: &[u8]) -> Result { + if bytes.len() < 32 { + return Err(Error::ConsensusEncoding( + bitcoin::consensus::encode::Error::ParseFailed( + "Not enough bytes for public key and tapleaf hash", + ), + )); + } + let a: XOnlyPublicKey = Deserialize::deserialize(&bytes[..32])?; + let b: TapLeafHash = Deserialize::deserialize(&bytes[32..])?; + Ok((a, b)) + } + } + + impl Serialize for ControlBlock { + fn serialize(&self) -> Vec { + ControlBlock::serialize(self) + } + } + + impl Deserialize for ControlBlock { + fn deserialize(bytes: &[u8]) -> Result { + Self::decode(bytes).map_err(|_| Error::InvalidControlBlock) + } + } + + // Versioned ScriptBuf + impl Serialize for (ScriptBuf, LeafVersion) { + fn serialize(&self) -> Vec { + let mut buf = Vec::with_capacity(self.0.len() + 1); + buf.extend(self.0.as_bytes()); + buf.push(self.1.to_consensus()); + buf + } + } + + impl Deserialize for (ScriptBuf, LeafVersion) { + fn deserialize(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(Error::ConsensusEncoding( + bitcoin::consensus::encode::Error::ParseFailed( + "Not enough bytes for script buf and leaf version", + ), + )); + } + // The last byte is LeafVersion. + let script = ScriptBuf::deserialize(&bytes[..bytes.len() - 1])?; + let leaf_ver = LeafVersion::from_consensus(bytes[bytes.len() - 1]) + .map_err(|_| Error::InvalidLeafVersion)?; + Ok((script, leaf_ver)) + } + } + + impl Serialize for (Vec, KeySource) { + fn serialize(&self) -> Vec { + let mut buf = Vec::with_capacity(32 * self.0.len() + key_source_len(&self.1)); + self.0 + .consensus_encode(&mut buf) + .expect("Vecs don't error allocation"); + // TODO: Add support for writing into a writer for key-source + buf.extend(self.1.serialize()); + buf + } + } + + impl Deserialize for (Vec, KeySource) { + fn deserialize(bytes: &[u8]) -> Result { + let (leafhash_vec, consumed) = deserialize_partial::>(bytes)?; + let key_source = KeySource::deserialize(&bytes[consumed..])?; + Ok((leafhash_vec, key_source)) + } + } + + impl Serialize for TapTree { + fn serialize(&self) -> Vec { + let capacity = self + .script_leaves() + .map(|l| { + l.script().len() + VarInt(l.script().len() as u64).len() // script version + + 1 // merkle branch + + 1 // leaf version + }) + .sum::(); + let mut buf = Vec::with_capacity(capacity); + for leaf_info in self.script_leaves() { + // # Cast Safety: + // + // TaprootMerkleBranch can only have len atmost 128(TAPROOT_CONTROL_MAX_NODE_COUNT). + // safe to cast from usize to u8 + buf.push(leaf_info.merkle_branch().len() as u8); + buf.push(leaf_info.version().to_consensus()); + leaf_info + .script() + .consensus_encode(&mut buf) + .expect("Vecs dont err"); + } + buf + } + } + + impl Deserialize for TapTree { + fn deserialize(bytes: &[u8]) -> Result { + let mut builder = TaprootBuilder::new(); + let mut bytes_iter = bytes.iter(); + while let Some(depth) = bytes_iter.next() { + let version = bytes_iter + .next() + .ok_or(Error::Taproot("Invalid Taproot Builder"))?; + let (script, consumed) = deserialize_partial::(bytes_iter.as_slice())?; + if consumed > 0 { + bytes_iter.nth(consumed - 1); + } + let leaf_version = + LeafVersion::from_consensus(*version).map_err(|_| Error::InvalidLeafVersion)?; + builder = builder + .add_leaf_with_ver(*depth, script, leaf_version) + .map_err(|_| Error::Taproot("Tree not in DFS order"))?; + } + TapTree::try_from(builder).map_err(Error::TapTree) + } + } + + // Helper function to compute key source len + fn key_source_len(key_source: &KeySource) -> usize { + 4 + 4 * (key_source.1).as_ref().len() + } } diff --git a/bitcoin_client_rs/src/wallet.rs b/bitcoin_client_rs/src/wallet.rs index 36bbb021..6d51fecc 100644 --- a/bitcoin_client_rs/src/wallet.rs +++ b/bitcoin_client_rs/src/wallet.rs @@ -3,9 +3,9 @@ use core::iter::IntoIterator; use core::str::FromStr; use bitcoin::{ + bip32::{DerivationPath, Error, ExtendedPubKey, Fingerprint, KeySource}, consensus::encode::{self, VarInt}, hashes::{sha256, Hash, HashEngine}, - util::bip32::{DerivationPath, Error, ExtendedPubKey, Fingerprint, KeySource}, }; use crate::merkle::MerkleTree; @@ -113,7 +113,7 @@ impl WalletPolicy { if self.version == Version::V2 { let mut engine = sha256::Hash::engine(); engine.input(self.descriptor_template.as_bytes()); - let hash = sha256::Hash::from_engine(engine).into_inner(); + let hash = sha256::Hash::from_engine(engine).to_byte_array(); res.extend_from_slice(&hash); } else { res.extend_from_slice(self.descriptor_template.as_bytes()); @@ -130,7 +130,7 @@ impl WalletPolicy { preimage.extend_from_slice(key.to_string().as_bytes()); let mut engine = sha256::Hash::engine(); engine.input(&preimage); - sha256::Hash::from_engine(engine).into_inner() + sha256::Hash::from_engine(engine).to_byte_array() }) .collect(), ) @@ -140,10 +140,35 @@ impl WalletPolicy { res } + pub fn get_descriptor(&self, change: bool) -> Result { + let mut desc = self.descriptor_template.clone(); + + for (i, key) in self.keys.iter().enumerate().rev() { + desc = desc.replace(&format!("@{}", i), &key.to_string()); + } + + desc = desc.replace("/**", &format!("/{}/{}", if change { 1 } else { 0 }, "*")); + + // For every "/" expression, replace with M if not change, or with N if change + while let Some(start) = desc.find("/<") { + if let Some(end) = desc.find(">") { + let nums: Vec<&str> = desc[start + 2..end].split(";").collect(); + if nums.len() == 2 { + let replacement = if change { nums[1] } else { nums[0] }; + desc = format!("{}{}{}", &desc[..start + 1], replacement, &desc[end + 1..]); + } else { + return Err(WalletError::InvalidPolicy); + } + } + } + + Ok(desc) + } + pub fn id(&self) -> [u8; 32] { let mut engine = sha256::Hash::engine(); engine.input(&self.serialize()); - sha256::Hash::from_engine(engine).into_inner() + sha256::Hash::from_engine(engine).to_byte_array() } } @@ -151,8 +176,10 @@ impl WalletPolicy { pub enum WalletError { InvalidThreshold, UnsupportedAddressType, + InvalidPolicy, } +#[derive(PartialEq, Eq)] pub struct WalletPubKey { pub inner: ExtendedPubKey, pub source: Option, @@ -230,7 +257,7 @@ impl FromStr for WalletPubKey { } impl core::fmt::Display for WalletPubKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { if self.source.is_none() { write!(f, "{}", self.inner) } else { @@ -255,7 +282,7 @@ impl core::fmt::Display for WalletPubKey { #[cfg(test)] mod tests { use super::*; - use bitcoin::hashes::hex::ToHex; + use bitcoin::hashes::hex::FromHex; use core::str::FromStr; const MASTER_KEY_EXAMPLE: &str = "[5c9e228d]tpubDEGquuorgFNb8bjh5kNZQMPtABJzoWwNm78FUmeoPkfRtoPF7JLrtoZeT3J3ybq1HmC3Rn1Q8wFQ8J5usanzups5rj7PJoQLNyvq8QbJruW/**"; @@ -305,6 +332,61 @@ mod tests { WalletPubKey::from_str("[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK").unwrap(), ], ); - assert_eq!(wallet.serialize().as_slice().to_hex(), "020c436f6c642073746f726167651fb56c3d5542fa09b3956834a9ff6a1df5c36a38e5b02c63c54b41a9a04403b82602516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb"); + assert_eq!(wallet.serialize().as_slice(), Vec::::from_hex("020c436f6c642073746f726167651fb56c3d5542fa09b3956834a9ff6a1df5c36a38e5b02c63c54b41a9a04403b82602516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb").unwrap()); + } + + #[test] + fn test_get_descriptor() { + let wallet = WalletPolicy::new( + "Cold storage".to_string(), + Version::V2, + "wsh(sortedmulti(2,@0/**,@1/<12;3>/*))".to_string(), + vec![ + WalletPubKey::from_str("[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF").unwrap(), + WalletPubKey::from_str("[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK").unwrap(), + ], + ); + + assert_eq!(wallet.get_descriptor(false).unwrap(), "wsh(sortedmulti(2,[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/0/*,[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK/12/*))"); + assert_eq!(wallet.get_descriptor(true).unwrap(), "wsh(sortedmulti(2,[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF/1/*,[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK/3/*))"); + + let wallet = WalletPolicy::new( + "Cold storage".to_string(), + Version::V2, + "wsh(or_d(pk(@0/<0;1>/*),and_v(v:pkh(@1/<0;1>/*),older(65535))))".to_string(), + vec![ + WalletPubKey::from_str("[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N").unwrap(), + WalletPubKey::from_str("[053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp").unwrap(), + ], + ); + assert_eq!(wallet.get_descriptor(false).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/0/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/0/*),older(65535))))"); + + assert_eq!(wallet.get_descriptor(true).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/1/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/1/*),older(65535))))"); + + let wallet = WalletPolicy::new( + "Cold storage".to_string(), + Version::V2, + "wsh(or_d(pk(@0/<0;1>/*),and_v(v:pkh(@1/**),older(65535))))".to_string(), + vec![ + WalletPubKey::from_str("[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N").unwrap(), + WalletPubKey::from_str("[053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp").unwrap(), + ], + ); + assert_eq!(wallet.get_descriptor(false).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/0/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/0/*),older(65535))))"); + + assert_eq!(wallet.get_descriptor(true).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/1/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/1/*),older(65535))))"); + + let wallet = WalletPolicy::new( + "Cold storage".to_string(), + Version::V2, + "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(65535))))".to_string(), + vec![ + WalletPubKey::from_str("[ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N").unwrap(), + WalletPubKey::from_str("[053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp").unwrap(), + ], + ); + assert_eq!(wallet.get_descriptor(false).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/0/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/0/*),older(65535))))"); + + assert_eq!(wallet.get_descriptor(true).unwrap(), "wsh(or_d(pk([ffd63c8d/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N/1/*),and_v(v:pkh([053f423f/48'/1'/0'/2']tpubDEGZMZiz8Vnp7N7cTM9Cty897GJpQ8jqmw2yyDKMPfbMzqPtRbo8wViKtkx6zfrzY6jW5NPNULeN9j7oYCqvrFxCkhSdJs7QxwZ3qQ1PXSp/1/*),older(65535))))"); } } diff --git a/bitcoin_client_rs/tests/client.rs b/bitcoin_client_rs/tests/client.rs index 21a8e8d0..2a530349 100644 --- a/bitcoin_client_rs/tests/client.rs +++ b/bitcoin_client_rs/tests/client.rs @@ -2,11 +2,11 @@ mod utils; use std::str::FromStr; use bitcoin::{ - consensus::encode::deserialize, - hashes::hex::{FromHex, ToHex}, - util::{bip32::DerivationPath, psbt::Psbt}, + bip32::DerivationPath, + hashes::{hex::FromHex, Hash}, + psbt::Psbt, }; -use ledger_bitcoin_client::{async_client, client, wallet}; +use ledger_bitcoin_client::{async_client, client, psbt::PartialSignature, wallet}; fn test_cases(path: &str) -> Vec { let data = std::fs::read_to_string(path).expect("Unable to read file"); @@ -173,7 +173,7 @@ async fn test_register_wallet() { .register_wallet(&wallet) .unwrap(); - assert_eq!(hmac.to_hex(), hmac_result); + assert_eq!(hmac, <[u8; 32]>::from_hex(&hmac_result).unwrap()); let (_id, hmac) = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) @@ -181,7 +181,7 @@ async fn test_register_wallet() { .await .unwrap(); - assert_eq!(hmac.to_hex(), hmac_result); + assert_eq!(hmac, <[u8; 32]>::from_hex(&hmac_result).unwrap()); } } @@ -250,7 +250,7 @@ async fn test_get_wallet_address() { .get_wallet_address(&wallet, hmac.as_ref(), change, address_index, display) .unwrap(); - assert_eq!(address.to_string(), address_result); + assert_eq!(address.assume_checked().to_string(), address_result); let address = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) @@ -258,7 +258,7 @@ async fn test_get_wallet_address() { .await .unwrap(); - assert_eq!(address.to_string(), address_result); + assert_eq!(address.assume_checked().to_string(), address_result); } } @@ -300,23 +300,71 @@ async fn test_sign_psbt() { h }); + let sigs: Vec = case + .get("sigs") + .map(|v| serde_json::from_value(v.clone()).unwrap()) + .unwrap(); + let psbt_str: String = case .get("psbt") .map(|v| serde_json::from_value(v.clone()).unwrap()) .unwrap(); - let psbt: Psbt = deserialize(&base64::decode(&psbt_str).unwrap()).unwrap(); + let psbt = Psbt::deserialize(&base64::decode(&psbt_str).unwrap()).unwrap(); let wallet = wallet::WalletPolicy::new(name, wallet::Version::V2, policy, keys); let store = utils::RecordStore::new(&exchanges); - let _res = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) + let res = client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_psbt(&psbt, &wallet, hmac.as_ref()) .unwrap(); - let _res = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) + let check_signatures = |sigs: &[serde_json::Value], res: Vec<(usize, PartialSignature)>| { + for (i, psbt_sig) in res { + for (j, res_sig) in sigs.iter().enumerate() { + if i == j { + match psbt_sig { + PartialSignature::TapScriptSig(key, tapleaf_hash, sig) => { + assert_eq!( + res_sig + .get("key") + .map(|v| serde_json::from_value::(v.clone()) + .unwrap()) + .unwrap(), + key.to_string() + ); + if let Some(tapleaf_hash_res) = res_sig + .get("tapleaf_hash") + .map(|v| serde_json::from_value::(v.clone()).unwrap()) + { + assert_eq!( + tapleaf_hash_res, + hex::encode(tapleaf_hash.unwrap().to_byte_array()) + ); + } + assert_eq!( + res_sig + .get("sig") + .map(|v| serde_json::from_value::(v.clone()) + .unwrap()) + .unwrap(), + hex::encode(sig.to_vec()) + ); + } + _ => {} + } + } + } + } + }; + + check_signatures(&sigs, res); + + let res = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_psbt(&psbt, &wallet, hmac.as_ref()) .await .unwrap(); + + check_signatures(&sigs, res); } } diff --git a/bitcoin_client_rs/tests/data/sign_psbt.json b/bitcoin_client_rs/tests/data/sign_psbt.json index a1d286f6..593afcff 100644 --- a/bitcoin_client_rs/tests/data/sign_psbt.json +++ b/bitcoin_client_rs/tests/data/sign_psbt.json @@ -455,7 +455,9 @@ "<= 100021024ba3b77d933de9fa3f9583348c40f3caaf2effad5b6e244ece8abbfcc7244f6730440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201e000", "=> f801000100", "<= 9000" - ]}, + ], + "sigs": [] + }, { "name": "Liana", "policy": "wsh(or_d(pk(@0/<0;1>/*),and_v(v:pkh(@1/<0;1>/*),older(10))))", @@ -856,6 +858,446 @@ "<= 400060dcf3ace170d59f287f4047d6d7ac279e2bd16f6a64a06424bebdf9dfa43224e000", "=> f80100017c7a7a005b38613634663261395d7470756244364e7a56626b7259685a34576d7a466a765172703773446134454355785469396f6279384b34465a6b64335843427445644b7755695179594a6178694a6f357934326779445745637a7246706f7a456a654c784d50786a663257746b66636270556466764e6e6f7a5746", "<= 9000" + ], + "sigs": [] + }, + { + "name": "Taproot foreign internal key, and our script key", + "policy": "tr(@0/**,pk(@1/**))", + "keys": [ + "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF", + "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK" + ], + "hmac": "dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7", + "psbt": "cHNidP8BAFICAAAAAR/BzFdxy4OGDMVtlLz+2ThgjBf2NmJDW0HpxE/8/TFCAQAAAAD9////ATkFAAAAAAAAFgAUqo7zdMr638p2kC3bXPYcYLv9nYUAAAAAAAEBK0wGAAAAAAAAIlEg/AoQ0wjH5BtLvDZC+P2KwomFOxznVaDG0NSV8D2fLaQBAwQBAAAAIhXBUBcQi+zqje3FMAuyI4azqzA2esJi+c5eWDJuuD46IvUjIGsW6MH5efpMwPBbajAK//+UFFm28g3nfeVbAWDvjkysrMAhFlAXEIvs6o3txTALsiOGs6swNnrCYvnOXlgybrg+OiL1HQB2IjpuMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIRZrFujB+Xn6TMDwW2owCv//lBRZtvIN533lWwFg745MrD0BCS7aAzYX4hDuf30ON4pASuocSLVqoQMCK+z3dG5HAKT1rML9MAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAARcgUBcQi+zqje3FMAuyI4azqzA2esJi+c5eWDJuuD46IvUBGCAJLtoDNhfiEO5/fQ43ikBK6hxItWqhAwIr7Pd0bkcApAAA", + "exchanges": [ + "=> e1040001c305519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216206d0d3e783926a53f1a696f04944e03bc43440cf47684d9a959e98d2add8510f10152a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b01f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141fba1a89c470ab0c36c38beaebb577b754c33d90b51df53977e2b01018fbe25f33dae925660e20859ed8833025d46444483ce264fdb77e34569aabe9d590da8fb7", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200500e000", + "=> f801000182fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177f0303583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d4b8c129ed14cce2c08cfc6766db7f8cdb133b5f698b8de3d5890ea7ff7f0a8d195811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 4000fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177fe000", + "=> f80100010402020002", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200501e000", + "=> f801000182583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0303fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177f4b8c129ed14cce2c08cfc6766db7f8cdb133b5f698b8de3d5890ea7ff7f0a8d195811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200502e000", + "=> f8010001824f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a403039f1afa4dc124cba73134e82ff50f17c8f7164257c79fed9a13f5943a6acb8e3d52c56b473e5246933e7852989cd9feba3b38f078742b93afff1e65ed4679782595811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 40004f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f80100010402020004", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200503e000", + "=> f8010001829f1afa4dc124cba73134e82ff50f17c8f7164257c79fed9a13f5943a6acb8e3d03034f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a452c56b473e5246933e7852989cd9feba3b38f078742b93afff1e65ed4679782595811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 40009f1afa4dc124cba73134e82ff50f17c8f7164257c79fed9a13f5943a6acb8e3de000", + "=> f80100010402020005", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200504e000", + "=> f80100014295811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926010112885c5025dece82b9e180bdaf19d6e5571772906c9c24de31790023755c8888", + "<= 400095811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926e000", + "=> f801000104020200fb", + "<= 42519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e31321620fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177fe000", + "=> f8010001020100", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200500e000", + "=> f801000182fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177f0303583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d4b8c129ed14cce2c08cfc6766db7f8cdb133b5f698b8de3d5890ea7ff7f0a8d195811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 416d0d3e783926a53f1a696f04944e03bc43440cf47684d9a959e98d2add8510f10500e000", + "=> f8010001820bd288cecce2dfc6c9b9245ab747a10870f84c16e986e61b259603b59cf1f3b903038855508aade16ec573d21e6a485dfd0a7624085c1a14b5ecdd6485de0c6839a4c178813b8617f884a8135fd9f95e1a4596188ab7705a3356480c01c0f977db930bd288cecce2dfc6c9b9245ab747a10870f84c16e986e61b259603b59cf1f3b9", + "<= 40000bd288cecce2dfc6c9b9245ab747a10870f84c16e986e61b259603b59cf1f3b9e000", + "=> f80100010705050002000000", + "<= 42519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e31321620583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f8010001020101", + "<= 41519b38dae74447b72151f354cb138ca3591a5ff8ac813289b18a004e313216200501e000", + "=> f801000182583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0303fcf0a6c700dd13e274b6fba8deea8dd9b26e4eedde3495717cac8408c9c5177f4b8c129ed14cce2c08cfc6766db7f8cdb133b5f698b8de3d5890ea7ff7f0a8d195811f41d3d5c58240be155bb7d1dcb8f47add7e3417c24e1d52d41653013926", + "<= 416d0d3e783926a53f1a696f04944e03bc43440cf47684d9a959e98d2add8510f10501e000", + "=> f8010001828855508aade16ec573d21e6a485dfd0a7624085c1a14b5ecdd6485de0c6839a403030bd288cecce2dfc6c9b9245ab747a10870f84c16e986e61b259603b59cf1f3b9c178813b8617f884a8135fd9f95e1a4596188ab7705a3356480c01c0f977db930bd288cecce2dfc6c9b9245ab747a10870f84c16e986e61b259603b59cf1f3b9", + "<= 40008855508aade16ec573d21e6a485dfd0a7624085c1a14b5ecdd6485de0c6839a4e000", + "=> f80100010705050000000000", + "<= 4000ba1a89c470ab0c36c38beaebb577b754c33d90b51df53977e2b01018fbe25f33e000", + "=> f80100017674740230546170726f6f7420666f726569676e20696e7465726e616c206b65792c20616e64206f757220736372697074206b657913da4cb76634983c02c657de46cbe9a23e5c3ec2a41a02f41dd65bf065469c352402516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb", + "<= 4000da4cb76634983c02c657de46cbe9a23e5c3ec2a41a02f41dd65bf065469c3524e000", + "=> f801000115131374722840302f2a2a2c706b2840312f2a2a2929", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0200e000", + "=> f801000142521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775010179ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1", + "<= 4000521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775e000", + "=> f8010001898787005b37363232336136652f3438272f31272f30272f32275d747075624445374e51796d7234414674657770417357746e726579713967686b7a51425870435a6a574c46565241766e62663776796132654d54765432665061704e714c38537556764c51646255624d66574c5644435a4b6e734542717036554b393351457a4c38436b3233417746", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 4152a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0100e000", + "=> f80100012252a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0000", + "<= 400052a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302be000", + "=> f8010001444242000a2ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f80100010402020001", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a02e000", + "=> f8010001a29f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b19604043b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae905096c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40009f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b196e000", + "=> f8010001040202000e", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a03e000", + "=> f8010001a23b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae9050904049f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b1966c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40003b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae90509e000", + "=> f8010001040202000f", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a04e000", + "=> f8010001a20298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe70404d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40000298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7e000", + "=> f80100010402020010", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a05e000", + "=> f8010001a2d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b5904040298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59e000", + "=> f80100012523230015c15017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a06e000", + "=> f8010001a2fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d310404cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f1982c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d31e000", + "=> f801000124222200165017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a06e000", + "=> f8010001a2e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aae04048ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf187c538f7b0c49c8ebdbce106354ec9a98c2055a9266918bfcafd0ad2fb747188ae0abe70738502d881c6fc68bab1aab8becbf0a892f500e8f5da49732af7ce81216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aaee000", + "=> f8010001201e1e000076223a6e300000800100008000000080020000800000000000000000", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a07e000", + "=> f8010001a2cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f190404fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d3182c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f19e000", + "=> f801000124222200166b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a07e000", + "=> f8010001a28ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf1870404e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aaec538f7b0c49c8ebdbce106354ec9a98c2055a9266918bfcafd0ad2fb747188ae0abe70738502d881c6fc68bab1aab8becbf0a892f500e8f5da49732af7ce81216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 40008ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf187e000", + "=> f8010001403e3e0001092eda033617e210ee7f7d0e378a404aea1c48b56aa103022becf7746e4700a4f5acc2fd300000800100008000000080020000800000000000000000", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a08e000", + "=> f80100016267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed019040120202b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 400067a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012e000", + "=> f80100010402020017", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a09e000", + "=> f801000162b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0020267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 4000b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0e000", + "=> f80100010402020018", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f8010001020100", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a00e000", + "=> f8010001a2eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521040486f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b833f38e6af142b139e21811914f8a4079e2702a261477eacde0f0653cbe3e7fa04a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521e000", + "=> f80100012e2c2c004c06000000000000225120fc0a10d308c7e41b4bbc3642f8fd8ac289853b1ce755a0c6d0d495f03d9f2da4", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0200e000", + "=> f801000142521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775010179ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1", + "<= 4000521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775e000", + "=> f8010001898787005b37363232336136652f3438272f31272f30272f32275d747075624445374e51796d7234414674657770417357746e726579713967686b7a51425870435a6a574c46565241766e62663776796132654d54765432665061704e714c38537556764c51646255624d66574c5644435a4b6e734542717036554b393351457a4c38436b3233417746", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f8010001020101", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a01e000", + "=> f8010001a286f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b0404eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521833f38e6af142b139e21811914f8a4079e2702a261477eacde0f0653cbe3e7fa04a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 400086f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3be000", + "=> f80100010705050001000000", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0200e000", + "=> f801000142521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775010179ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1", + "<= 4000521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775e000", + "=> f8010001898787005b37363232336136652f3438272f31272f30272f32275d747075624445374e51796d7234414674657770417357746e726579713967686b7a51425870435a6a574c46565241766e62663776796132654d54765432665061704e714c38537556764c51646255624d66574c5644435a4b6e734542717036554b393351457a4c38436b3233417746", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 41f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0100e000", + "=> f801000122f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0000", + "<= 4000f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141fe000", + "=> f8010001444242000278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8dfdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff256101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0200e000", + "=> f801000142583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d01014f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 40004f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f80100010402020004", + "<= 4278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d4f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f8010001020101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 41fdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff2561010201e000", + "=> f801000142f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364e010160b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceeb", + "<= 4000f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364ee000", + "=> f8010001191717000014aa8ef374cafadfca76902ddb5cf61c60bbfd9d85", + "<= 41f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0100e000", + "=> f801000122f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0000", + "<= 4000f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141fe000", + "=> f8010001444242000278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8dfdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff256101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0200e000", + "=> f801000142583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d01014f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 40004f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f80100010402020004", + "<= 4278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f8010001020100", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0200e000", + "=> f801000142583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d01014f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4", + "<= 41fdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff2561010200e000", + "=> f80100014260b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceeb0101f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364e", + "<= 400060b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceebe000", + "=> f80100010b0909003905000000000000", + "<= 4278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d4f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f8010001020101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 41fdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff2561010201e000", + "=> f801000142f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364e010160b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceeb", + "<= 4000f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364ee000", + "=> f8010001191717000014aa8ef374cafadfca76902ddb5cf61c60bbfd9d85", + "<= 4152a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0100e000", + "=> f80100012252a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0000", + "<= 400052a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302be000", + "=> f8010001444242000a2ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f80100010402020001", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a02e000", + "=> f8010001a29f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b19604043b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae905096c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40009f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b196e000", + "=> f8010001040202000e", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a03e000", + "=> f8010001a23b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae9050904049f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b1966c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40003b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae90509e000", + "=> f8010001040202000f", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a04e000", + "=> f8010001a20298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe70404d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40000298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7e000", + "=> f80100010402020010", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a05e000", + "=> f8010001a2d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b5904040298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59e000", + "=> f80100012523230015c15017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a06e000", + "=> f8010001a2fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d310404cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f1982c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d31e000", + "=> f801000124222200165017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a07e000", + "=> f8010001a2cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f190404fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d3182c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f19e000", + "=> f801000124222200166b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a08e000", + "=> f80100016267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed019040120202b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 400067a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012e000", + "=> f80100010402020017", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a09e000", + "=> f801000162b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0020267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 4000b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0e000", + "=> f80100010402020018", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa9f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b196e000", + "=> f8010001020102", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a02e000", + "=> f8010001a29f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b19604043b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae905096c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a02e000", + "=> f8010001a259dfa3432052a07e205c5852a8115f36b5751bc38c0d16e99339bf11786cb3eb040486f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3bfbd01be915e2775cfe7a13b8a797721df7906f1303bf566c1dc1d93c1cf57b9504a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 400059dfa3432052a07e205c5852a8115f36b5751bc38c0d16e99339bf11786cb3ebe000", + "=> f8010001232121001fc1cc5771cb83860cc56d94bcfed938608c17f63662435b41e9c44ffcfd3142", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa3b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae90509e000", + "=> f8010001020103", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a03e000", + "=> f8010001a23b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae9050904049f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b1966c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a03e000", + "=> f8010001a286f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b040459dfa3432052a07e205c5852a8115f36b5751bc38c0d16e99339bf11786cb3ebfbd01be915e2775cfe7a13b8a797721df7906f1303bf566c1dc1d93c1cf57b9504a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 400086f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3be000", + "=> f80100010705050001000000", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7e000", + "=> f8010001020104", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a04e000", + "=> f8010001a20298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe70404d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a04e000", + "=> f8010001a2b2db18c190abf44354f0286c60b2a6b6a2db6d1a36a6829e66298918b55e1d980404e349f9d63a6274c4ac4d94f77cb5c37e690004074eb0eda8ebd0212051c09b37a76b95c125e315db4e6cbbe2d64b2ff399194bc71841593586bfd4efa850d6b10abe70738502d881c6fc68bab1aab8becbf0a892f500e8f5da49732af7ce81216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000b2db18c190abf44354f0286c60b2a6b6a2db6d1a36a6829e66298918b55e1d98e000", + "=> f801000107050500fdffffff", + "<= 41f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0100e000", + "=> f801000122f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141f0000", + "<= 4000f812f66891ac8edd9c988a8766f261a416f8cec1124ff7c778359f061abc141fe000", + "=> f8010001444242000278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8dfdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff256101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0200e000", + "=> f801000142583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d01014f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 40004f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f80100010402020004", + "<= 4278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f8010001020100", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0200e000", + "=> f801000142583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d01014f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4", + "<= 41fdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff2561010200e000", + "=> f80100014260b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceeb0101f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364e", + "<= 400060b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceebe000", + "=> f80100010b0909003905000000000000", + "<= 4278850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d4f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a4e000", + "=> f8010001020101", + "<= 4178850a5ab36238b076dd99fd258c70d523168704247988a94caa8c9ccd056b8d0201e000", + "=> f8010001424f35212d12f9ad2036492c95f1fe79baf4ec7bd9bef3dffa7579f2293ff546a40101583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d", + "<= 41fdd2e53a1c8aaa496b5d9e9f85de15d038d6c10bee94ffbd749193feff2561010201e000", + "=> f801000142f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364e010160b259ffaad54b5fa53da25cef41baef40707b982a39aaa1386ba593fbf0ceeb", + "<= 4000f6457f2e5f16c18653329752399861b1c47c6ec1e1933f7fdd3ad1c7a6b5364ee000", + "=> f8010001191717000014aa8ef374cafadfca76902ddb5cf61c60bbfd9d85", + "<= 4152a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0100e000", + "=> f80100012252a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0000", + "<= 400052a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302be000", + "=> f8010001444242000a2ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f80100010402020001", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a02e000", + "=> f8010001a29f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b19604043b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae905096c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40009f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b196e000", + "=> f8010001040202000e", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a03e000", + "=> f8010001a23b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae9050904049f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b1966c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40003b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae90509e000", + "=> f8010001040202000f", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a04e000", + "=> f8010001a20298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe70404d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40000298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7e000", + "=> f80100010402020010", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a05e000", + "=> f8010001a2d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b5904040298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59e000", + "=> f80100012523230015c15017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a06e000", + "=> f8010001a2fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d310404cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f1982c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d31e000", + "=> f801000124222200165017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a07e000", + "=> f8010001a2cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f190404fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d3182c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f19e000", + "=> f801000124222200166b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a08e000", + "=> f80100016267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed019040120202b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 400067a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012e000", + "=> f80100010402020017", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a09e000", + "=> f801000162b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0020267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 4000b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0e000", + "=> f80100010402020018", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f8010001020100", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a00e000", + "=> f8010001a2eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521040486f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b833f38e6af142b139e21811914f8a4079e2702a261477eacde0f0653cbe3e7fa04a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521e000", + "=> f80100012e2c2c004c06000000000000225120fc0a10d308c7e41b4bbc3642f8fd8ac289853b1ce755a0c6d0d495f03d9f2da4", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0200e000", + "=> f801000142521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775010179ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1", + "<= 4000521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775e000", + "=> f8010001898787005b37363232336136652f3438272f31272f30272f32275d747075624445374e51796d7234414674657770417357746e726579713967686b7a51425870435a6a574c46565241766e62663776796132654d54765432665061704e714c38537556764c51646255624d66574c5644435a4b6e734542717036554b393351457a4c38436b3233417746", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 4152a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0100e000", + "=> f80100012252a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302b0000", + "<= 400052a2b0a36f6a220ccad78599c1a2ade65a547f60adb4742208004768ce06302be000", + "=> f8010001444242000a2ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f80100010402020001", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f80100010402020003", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a02e000", + "=> f8010001a29f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b19604043b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae905096c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40009f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b196e000", + "=> f8010001040202000e", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a03e000", + "=> f8010001a23b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae9050904049f4917386c45e2c0da0d9b475f1a19cf2db1e929195c6a9f4966ca0d2105b1966c0c1f61819138c88692eb2a0d48302b95bb44c5af18dc97de5c9144667791fcb129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40003b2b7c6ee25e2f28a6235e273eaf13f504bd445024147ebacb878262aae90509e000", + "=> f8010001040202000f", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a04e000", + "=> f8010001a20298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe70404d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 40000298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7e000", + "=> f80100010402020010", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a05e000", + "=> f8010001a2d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b5904040298d122906dcfc10892cb53a73992fc5b9f493ea4c9badb27b791b4127a7fe7b3196e9b7edd9bd22d846602ded84174b396f5d6577302f4e80a3eb314317958722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000d6e2dd3d84f0ddcb5213265124a3373e204f5ced6e24f2eea221c906ebf82b59e000", + "=> f80100012523230015c15017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a06e000", + "=> f8010001a2fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d310404cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f1982c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d31e000", + "=> f801000124222200165017108becea8dedc5300bb22386b3ab30367ac262f9ce5e58326eb83e3a22f5", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a06e000", + "=> f8010001a2e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aae04048ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf187c538f7b0c49c8ebdbce106354ec9a98c2055a9266918bfcafd0ad2fb747188ae0abe70738502d881c6fc68bab1aab8becbf0a892f500e8f5da49732af7ce81216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aaee000", + "=> f8010001201e1e000076223a6e300000800100008000000080020000800000000000000000", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a07e000", + "=> f8010001a2cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f190404fe9f3fe1fece3c192f8e8f139062141d102b0294e4171f02a27b857e38e07d3182c227d895877751e69ee9c140d456e98ecf404afc55e5d1dbc0d05c4be86c37722331f4d1466415a9ed493cda597538431c8bea1b14bed934ba20a1f565b4c0625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 4000cc42900f19b21f54505e980a9e29fa9325906d230e46009a28fa41f6ef891f19e000", + "=> f801000124222200166b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a07e000", + "=> f8010001a28ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf1870404e15593bf6543bfabc3aec6dd64b41ef6f6a54bd28a342a7dfa27486de4738aaec538f7b0c49c8ebdbce106354ec9a98c2055a9266918bfcafd0ad2fb747188ae0abe70738502d881c6fc68bab1aab8becbf0a892f500e8f5da49732af7ce81216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 40008ab4a42bd45e15046ab71d43f935d45cb8ca1243cce0ffc4d8686df1119cf187e000", + "=> f8010001403e3e0001092eda033617e210ee7f7d0e378a404aea1c48b56aa103022becf7746e4700a4f5acc2fd300000800100008000000080020000800000000000000000", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a08e000", + "=> f80100016267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed019040120202b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 400067a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012e000", + "=> f80100010402020017", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a09e000", + "=> f801000162b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0020267a88a2dc9a9749ec611eef8f47ef5ffcc335f2950d1bfaaf31169ed01904012454f801d01b657e627ac67c96acac49dbe705107621002dba84d17ceac19609d", + "<= 4000b5609376c87f00c645433e48648cb02e6a3f83467f2c827194de5d58f971c8f0e000", + "=> f80100010402020018", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 41516d2c50a89476ecffeec658057f0110674bbfafc18797dc480c7ed53802f3fb0201e000", + "=> f80100014279ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade10101521a79b1ec8019f7b8291af131d33a9dd39252161c6c8fc1f47c4edd9cfc2775", + "<= 400079ad51261747bf60b55f8900bb82bfc5dc7f52b9eb056bee94442ced92e1ade1e000", + "=> f8010001898787005b66356163633266642f3438272f31272f30272f32275d747075624446417145474e79616433356142434b554158625147446a6456684e75656e6f355a5a56456e3373516257356369343537674c52374879546d48426739336f6f757242737367557875577a316a583575686331716171466f395673796259314a35467565644c666d34644b", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de000", + "=> f8010001020101", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a01e000", + "=> f8010001a2583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5d0404b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a01e000", + "=> f8010001a286f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b0404eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521833f38e6af142b139e21811914f8a4079e2702a261477eacde0f0653cbe3e7fa04a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 400086f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3be000", + "=> f80100010705050001000000", + "<= 422ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fab413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d2e000", + "=> f8010001020100", + "<= 412ccf7826e726332a6e434b27fafbff92163ef2a69521a5d6c686399a2a18c1fa0a00e000", + "=> f8010001a2b413f47d13ee2fe6c845b2ee141af81de858df4ec549a58b7970bb96645bc8d20404583c7dfb7b3055d99465544032a571e10a134b1b6f769422bbb71fd7fa167a5de80cc247985bb408a9484b6fd53b538c321cab413033bb288ba55747dfadb6bab129229731a274be9e04d70d1f19c2b461edb2f29ddfce9309073458e039219f625b913ee9c3b93595e0eff3c6027145f86b094ecc1062d4f9cc4b1f22170534", + "<= 41b13db13cbb4cc5e475ca353b6ba53953d8c87677f1766d4e91988f7924d7b9be0a00e000", + "=> f8010001a2eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521040486f9649499b0080656c014aa244f654864bad4145c8513e9c8409f437d4a2b3b833f38e6af142b139e21811914f8a4079e2702a261477eacde0f0653cbe3e7fa04a4cbafeb669042a318ab80179304e2ffdbdc33ff0d06ebdbac655d12c9c4216e49a585e41fcb396de837e564293763fb71addf4377f356a574345fb16228fe", + "<= 4000eb8e6fe5c6218ad65d91351e9e7537081d0148ae3510d897833c1d1d69b6d521e000", + "=> f80100012e2c2c004c06000000000000225120fc0a10d308c7e41b4bbc3642f8fd8ac289853b1ce755a0c6d0d495f03d9f2da4", + "<= 1000406b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac092eda033617e210ee7f7d0e378a404aea1c48b56aa103022becf7746e4700a443493158062db6905dea9ba3ae6c14e1e155ba47aa1cfb35282052ac4dbc1c6718cda5c911a11599a869557ab34242cb0a227836e98976061530ca4de49eed9e01e000", + "=> f801000100", + "<= 9000" + ], + "sigs": [ + { + "key": "6b16e8c1f979fa4cc0f05b6a300affff941459b6f20de77de55b0160ef8e4cac", + "tapleaf_hash": "092eda033617e210ee7f7d0e378a404aea1c48b56aa103022becf7746e4700a4", + "sig": "43493158062db6905dea9ba3ae6c14e1e155ba47aa1cfb35282052ac4dbc1c6718cda5c911a11599a869557ab34242cb0a227836e98976061530ca4de49eed9e01" + } ] } ] diff --git a/bitcoin_client_rs/tests/utils/mod.rs b/bitcoin_client_rs/tests/utils/mod.rs index 3c08234b..ccd05356 100644 --- a/bitcoin_client_rs/tests/utils/mod.rs +++ b/bitcoin_client_rs/tests/utils/mod.rs @@ -2,7 +2,7 @@ use core::convert::TryFrom; use std::sync::atomic::{AtomicUsize, Ordering}; use async_trait::async_trait; -use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::hashes::hex::FromHex; use ledger_bitcoin_client::{ apdu::{APDUCommand, StatusWord}, @@ -51,7 +51,7 @@ impl TransportReplayer { let current = self.current.load(Ordering::Relaxed); if let Some((req, res)) = self.store.queue.get(current) { if payload != *req { - return Err(MockError::ExchangeNotFound(current, payload.to_hex())); + return Err(MockError::ExchangeNotFound(current, hex::encode(payload))); } self.current.store(current + 1, Ordering::Relaxed); let res = res.as_slice(); @@ -64,7 +64,7 @@ impl TransportReplayer { answer.to_vec(), )); } - Err(MockError::ExchangeNotFound(current, payload.to_hex())) + Err(MockError::ExchangeNotFound(current, hex::encode(payload))) } } diff --git a/desktop-wallet/src/main/LedgerApi.ts b/desktop-wallet/src/main/LedgerApi.ts index 0be5ab7d..f8ad641b 100644 --- a/desktop-wallet/src/main/LedgerApi.ts +++ b/desktop-wallet/src/main/LedgerApi.ts @@ -147,6 +147,7 @@ export const setupLedgerApi = (window: BrowserWindow) => { appClient .getMasterFingerprint() .then((fingerPrint) => { + console.log("Fingerprint", fingerPrint); window.webContents.send("message", method, fingerPrint); }) .catch((e) => { diff --git a/desktop-wallet/src/main/connectors/Speculos.ts b/desktop-wallet/src/main/connectors/Speculos.ts index 609155fd..ed9acfb3 100644 --- a/desktop-wallet/src/main/connectors/Speculos.ts +++ b/desktop-wallet/src/main/connectors/Speculos.ts @@ -3,7 +3,7 @@ import SpeculosTransport from "@ledgerhq/hw-transport-node-speculos-http"; import { DisconnectedDevice } from "@ledgerhq/errors"; const opts = { - baseURL: "http://localhost:5002", + baseURL: "http://127.0.0.1:5002", }; const axiosInstance = axios.create(opts); const speculosTransport = new SpeculosTransport(axiosInstance, opts); diff --git a/doc/bitcoin.md b/doc/bitcoin.md index e9cbe504..cd19cb30 100644 --- a/doc/bitcoin.md +++ b/doc/bitcoin.md @@ -12,13 +12,14 @@ The messaging format of the app is compatible with the [APDU protocol](https://d The main commands use `CLA = 0xE1`, unlike the legacy Bitcoin application that used `CLA = 0xE0`. -| CLA | INS | COMMAND NAME | DESCRIPTION | -|-----|-----|---------------------|-------------| -| E1 | 00 | GET_EXTENDED_PUBKEY | Return (and optionally show on screen) extended pubkey | -| E1 | 02 | REGISTER_WALLET | Registers a wallet on the device (with user's approval) | -| E1 | 03 | GET_WALLET_ADDRESS | Return and show on screen an address for a registered or default wallet | -| E1 | 04 | SIGN_PSBT | Signs a PSBT with a registered or default wallet | -| E1 | 10 | SIGN_MESSAGE | Sign a message with a key from a BIP32 path (Bitcoin Message Signing) | +| CLA | INS | COMMAND NAME | DESCRIPTION | +|-----|-----|------------------------|-------------| +| E1 | 00 | GET_EXTENDED_PUBKEY | Return (and optionally show on screen) extended pubkey | +| E1 | 02 | REGISTER_WALLET | Register a wallet policy on the device (with user's approval) | +| E1 | 03 | GET_WALLET_ADDRESS | Return and show on screen an address for a registered or default wallet | +| E1 | 04 | SIGN_PSBT | Sign a PSBT with a registered or default wallet | +| E1 | 05 | GET_MASTER_FINGERPRINT | Return the fingerprint of the master public key | +| E1 | 10 | SIGN_MESSAGE | Sign a message with a key from a BIP32 path (Bitcoin Message Signing) | The `CLA = 0xF8` is used for framework-specific (rather than app-specific) APDUs; at this time, only one command is present. diff --git a/doc/wallet.md b/doc/wallet.md index 85b0c292..044266cd 100644 --- a/doc/wallet.md +++ b/doc/wallet.md @@ -25,11 +25,18 @@ A wallet descriptor template is a `SCRIPT` expression. `addr` if you only know the pubkey hash). - `wpkh(KP)` (top level or inside `sh` only): P2WPKH output for the given compressed pubkey. -- `multi(k,KP_1,KP_2,...,KP_n)`: k-of-n multisig script. -- `sortedmulti(k,KP_1,KP_2,...,KP_n)`: k-of-n multisig script with keys +- `multi(k,KP_1,KP_2,...,KP_n)` (not inside `tr`): k-of-n multisig script using OP_CHECKMULTISIG. +- `sortedmulti(k,KP_1,KP_2,...,KP_n)` (not inside `tr`): k-of-n multisig script with keys sorted lexicographically in the resulting script. -- `tr(KP)`: P2TR output with the specified key as internal key. -- any valid [miniscript](https://bitcoin.sipa.be/miniscript) template (only inside top-level `wsh`). +- `multi_a(k,KP_1,KP_2,...,KP_n)` (only inside `tr`): k-of-n multisig script. +- `sortedmulti_a(k,KP_1,KP_2,...,KP_n)` (only inside `tr`): k-of-n multisig script with keys +sorted lexicographically in the resulting script. +- `tr(KP)` or `tr(KP,TREE)`: P2TR output with the specified key placeholder internal key, and optionally a tree of script paths. +- any valid [miniscript](https://bitcoin.sipa.be/miniscript) template (only inside top-level `wsh`, or in `TREE`). + +`TREE` expressions: +- any `SCRIPT`expression. +- An open brace `{`, a `TREE` expression, a comma `,`, a `TREE` expression, and a closing brace `}`. `KP` expressions (key placeholders) consist of - a single character `@` @@ -47,8 +54,6 @@ The placeholder `@i` for some number *i* represents the *i*-th key in the vector of key origin information (which must be of size at least *i* + 1, or the wallet policy is invalid). -NOTE: the `tr(KP)` descriptor will be generalized to a `tr(KP,TREE)` expression to support taproot scripts in a future version. - ### Keys information vector Each element of the keys origin information vector is a `KEY` expression. @@ -160,19 +165,27 @@ The hardware wallet will reject registration for wallet names not respecting the ## Supported policies -The following policy types are currently supported: +The following policy types are currently supported as top-level scripts: - `sh(multi(...))` and `sh(sortedmulti(...))` (legacy multisignature wallets); - `sh(wsh(multi(...)))` and `sh(wsh(sortedmulti(...)))` (wrapped-segwit multisignature wallets); -- `wsh(multi(...))` and `wsh(sortedmulti(...))` (native segwit multisignature wallets); -- `wsh(SCRIPT)` (where `SCRIPT` is an arbitrary [miniscript](https://bitcoin.sipa.be/miniscript) template). +- `wsh(SCRIPT)`; +- `tr(KP)` and `tr(KP,TREE)`. + +`SCRIPT` expression within `wsh` can be: +- `multi` or `sortedmulti`; +- a valid SegWit miniscript template. + +`SCRIPT` expression within `TREE` can be: +- `multi_a` or `sortedmulti_a`; +- a valid taproot miniscript template. # Default wallets A few policies that correspond to standardized single-key wallets can be used without requiring any registration; in the serialization, the wallet name must be a zero-length string. Those are the following policies: -- ``pkh(@0)`` - legacy addresses as per [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) -- ``wpkh(@0)`` - native segwit addresses per [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) -- ``sh(wpkh(@0))`` - nested segwit addresses as per [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) -- ``tr(@0)`` - single Key P2TR as per [BIP-86](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki) +- ``pkh(@0/**)`` - legacy addresses as per [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +- ``wpkh(@0/**)`` - native segwit addresses per [BIP-84](https://github.com/bitcoin/bips/blob/master/bip-0084.mediawiki) +- ``sh(wpkh(@0/**))`` - nested segwit addresses as per [BIP-49](https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki) +- ``tr(@0/**)`` - single Key P2TR as per [BIP-86](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki) Note that the wallet policy is considered standard (and therefore usable for signing without prior registration) only if the signing paths (defined in the key origin information) adhere to the corresponding BIP. Moreover, the BIP-44 `account` level must be at most `100`, and the `address index` at most `50000`. Larger values can still be used by registering the policy. diff --git a/glyphs/Bitcoin_64px.bmp b/glyphs/Bitcoin_64px.bmp new file mode 100644 index 0000000000000000000000000000000000000000..db706c4cc429474f8d0abdedd57bf432b4065602 GIT binary patch literal 674 zcmV;T0$u$_Nk%w1VL$*t0J8u9ot>R&X=$&ouZxR|Sy@?ndU_2F4N6K%!otELA|g9G zI~EoeE-o(K-roQJ{{R30A^8LV00000EC2ui06+jh000F4(8)=wy*TU5yZ>M)j-=HH z#gQye`z(x72xUDr1!K5;?=pjiEhHf9d`RR$INUT8$!K)h!~~AgtS?ZdG!oh{a=~m4 zu;O&fP-q6!@E*7Jj@w)FQT=|;g9>*HMGSWXAq{A1Z4L-63500|WgRScG!6qTG!1)N z2_KzVl`aMoP!CK8YJM3FX$uYw04fg*6D+n8V3Cs;s$vQi4NQ#>1QM=r!5Yb8h8YM2 z2oeBxm%eHX1OOcaqiM$&&2YIK-fEHgJ6z@^x6@1|)5a2;Z zo|u9a1n}WTVHc@h>%!=h0O-!7aV`QPOB2u3oq7}hs`=2!D9r(bc1}_l=wq6_G$3!? zVR)l}f?W(~PWTlD)<26PFlw>zQG|eRVTjrxIY3$lo@$iF(dl5-{7DqXWfzTZIMOiF? zu9!=+0PvomJIUsR=scoAET9aI&tZm3(3)B%$jot$o2cDmQL%7B?MNpsCV-H24Ab&q zr2%`A1iP5{w&T+SB6{20i%ACpH6J^Ufl%B}e_5~x3W0DKC?JC6HBkU}2pSNe4GqYK zU;^Xm!5|L?DA+^;aq;n27vh8%2?3IkXu<%{wZ=dJ8LF6&O$>aq06sIsDC3Si_UPk} I7(@U7JJbdega7~l literal 0 HcmV?d00001 diff --git a/icons/stax_app_bitcoin.gif b/icons/stax_app_bitcoin.gif new file mode 100644 index 0000000000000000000000000000000000000000..99915d7c503448a7e83c319804a6207cd9b33693 GIT binary patch literal 347 zcmV-h0i^y%Nk%w1VITk?0J8u9Sy@?WX=$CEo#Nu+i;Ih|udjM~dP+)4!otE978W8R zB0M}i3=9k|E-wH7{{R30A^8LV00000EC2ui03ZM$000F4(8)=wz1WTd6nhWT077OC z!3VBv5)5W#sBFC^L@WVzUjwm&Z@4;Sc*8(|2(%E0j{)LQ5cqPGL83<>92Sn5qu^t} zCd=MgQZP7>y#QtFYi5N#v&1@7NCpd74hS?X1v^XutE`+ulz0i02y~?qpf(MS61)i$lE8>H4Ouiskv&ah zH3tY4Rfm5*lmne03^{8Qq%9PWH6;vg0s;jD$2M{k+-g$=15k|N4w)*UL0~HJi6acWdv1G*v06VXtbgBRV literal 0 HcmV?d00001 diff --git a/src/boilerplate/dispatcher.c b/src/boilerplate/dispatcher.c index ee6c77e0..ef8b864e 100644 --- a/src/boilerplate/dispatcher.c +++ b/src/boilerplate/dispatcher.c @@ -127,7 +127,7 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[], G_dispatcher_context.read_buffer = buffer_create(cmd->data, cmd->lc); - if (cmd->p1 != 0 || cmd->p2 > 1) { + if (cmd->p2 > CURRENT_PROTOCOL_VERSION) { io_send_sw(SW_WRONG_P1P2); return; } @@ -174,6 +174,7 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[], bool is_ux_dirty = G_dispatcher_state.had_ux_flow || G_was_processing_screen_shown; if (G_dispatcher_state.termination_cb != NULL && is_ux_dirty) { G_dispatcher_state.termination_cb(); + G_was_processing_screen_shown = 0; } io_clear_processing_timeout(); diff --git a/src/boilerplate/dispatcher.h b/src/boilerplate/dispatcher.h index 22d5b518..b9545094 100644 --- a/src/boilerplate/dispatcher.h +++ b/src/boilerplate/dispatcher.h @@ -74,7 +74,7 @@ void apdu_dispatcher(command_descriptor_t const cmd_descriptors[], // Debug utilities -#if DEBUG == 0 +#if !defined(DEBUG) || DEBUG == 0 #define LOG_PROCESSOR(file, line, func) #else // Print current filename, line number and function name. diff --git a/src/boilerplate/io.c b/src/boilerplate/io.c index 872c21e8..9a4af083 100644 --- a/src/boilerplate/io.c +++ b/src/boilerplate/io.c @@ -20,6 +20,10 @@ #include "os.h" #include "ux.h" +#ifdef HAVE_NBGL +#include "nbgl_touch.h" +#include "nbgl_use_case.h" +#endif // HAVE_NBGL #include "io.h" #include "globals.h" @@ -47,12 +51,14 @@ bool G_was_processing_screen_shown; uint16_t G_interruption_timeout_start_tick; uint16_t G_processing_timeout_start_tick; +#ifdef HAVE_BAGL UX_STEP_NOCB(ux_processing_flow_1_step, pn, {&C_icon_processing, "Processing..."}); UX_FLOW(ux_processing_flow, &ux_processing_flow_1_step); void io_seproxyhal_display(const bagl_element_t *element) { io_seproxyhal_display_default((bagl_element_t *) element); } +#endif // HAVE_BAGL void io_start_interruption_timeout() { G_interruption_timeout_start_tick = G_ticks; @@ -83,7 +89,9 @@ uint8_t io_event(uint8_t channel) { switch (G_io_seproxyhal_spi_buffer[0]) { case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: +#ifdef HAVE_BAGL UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); +#endif // HAVE_BAGL break; case SEPROXYHAL_TAG_STATUS_EVENT: if (G_io_apdu_media == IO_APDU_MEDIA_USB_HID && // @@ -91,10 +99,20 @@ uint8_t io_event(uint8_t channel) { SEPROXYHAL_TAG_STATUS_EVENT_FLAG_USB_POWERED)) { THROW(EXCEPTION_IO_RESET); } - /* fallthrough */ + __attribute__((fallthrough)); case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: +#ifdef HAVE_BAGL UX_DISPLAYED_EVENT({}); +#endif // HAVE_BAGL +#ifdef HAVE_NBGL + UX_DEFAULT_EVENT(); +#endif // HAVE_NBGL + break; +#ifdef HAVE_NBGL + case SEPROXYHAL_TAG_FINGER_EVENT: + UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); break; +#endif // HAVE_NBGL case SEPROXYHAL_TAG_TICKER_EVENT: ++G_ticks; @@ -102,8 +120,15 @@ uint8_t io_event(uint8_t channel) { G_ticks - G_processing_timeout_start_tick >= PROCESSING_TIMEOUT_TICKS) { io_clear_processing_timeout(); - G_was_processing_screen_shown = true; - ux_flow_init(0, ux_processing_flow, NULL); + if (!G_was_processing_screen_shown) { + G_was_processing_screen_shown = true; +#ifdef HAVE_BAGL + ux_flow_init(0, ux_processing_flow, NULL); +#endif // HAVE_BAGL +#ifdef HAVE_NBGL + nbgl_useCaseSpinner("Processing"); +#endif // HAVE_NBGL + } } if (G_is_timeout_active.interruption && diff --git a/src/boilerplate/io.h b/src/boilerplate/io.h index 634c9702..4a6a3ea6 100644 --- a/src/boilerplate/io.h +++ b/src/boilerplate/io.h @@ -7,7 +7,9 @@ #include "common/buffer.h" +#ifdef HAVE_BAGL void io_seproxyhal_display(const bagl_element_t *element); +#endif // HAVE_BAGL /** * IO callback called when an interrupt based channel has received diff --git a/src/common/bip32.c b/src/common/bip32.c index ecae86f9..e8658089 100644 --- a/src/common/bip32.c +++ b/src/common/bip32.c @@ -141,58 +141,3 @@ bool is_pubkey_path_standard(const uint32_t *bip32_path, return true; } - -bool is_address_path_standard(const uint32_t *bip32_path, - size_t bip32_path_len, - uint32_t expected_purpose, - const uint32_t expected_coin_types[], - size_t expected_coin_types_len, - int expected_change) { - if (bip32_path_len != 5) { - return false; - } - - if (!is_pubkey_path_standard(bip32_path, - 3, - expected_purpose, - expected_coin_types, - expected_coin_types_len)) { - return false; - } - - uint32_t change = bip32_path[BIP44_CHANGE_OFFSET]; - if (change != 0 && change != 1) { - return false; - } - - if (expected_change == 0 || expected_change == 1) { - // change should match the expected one - if (change != (uint32_t) expected_change) { - return false; - } - } else if (expected_change != -1) { - return false; // wrong expected_change parameter - } - - uint32_t address_index = bip32_path[BIP44_ADDRESS_INDEX_OFFSET]; - if (address_index > - MAX_BIP44_ADDRESS_INDEX_RECOMMENDED) { // should not be hardened, and not too large - return false; - } - return true; -} - -int get_bip44_purpose(int address_type) { - switch (address_type) { - case ADDRESS_TYPE_LEGACY: - return 44; // legacy - case ADDRESS_TYPE_WIT: - return 84; // native segwit - case ADDRESS_TYPE_SH_WIT: - return 49; // wrapped segwit - case ADDRESS_TYPE_TR: - return 86; // taproot - default: - return -1; - } -} \ No newline at end of file diff --git a/src/common/bip32.h b/src/common/bip32.h index 3edc5e55..cd6707e1 100644 --- a/src/common/bip32.h +++ b/src/common/bip32.h @@ -114,52 +114,3 @@ bool is_pubkey_path_standard(const uint32_t *bip32_path, uint32_t expected_purpose, const uint32_t expected_coin_types[], size_t expected_coin_types_len); - -/** - * Verifies if a given path is standard according to the BIP44 or derived standards for the - * derivation path for an address. - * - * Returns false if any of the following conditions is not satisfied by the given bip32_path: - * - the bip32_path has exactly 5 elements; - * - the first 3 steps of the derivation are standard according to is_pubkey_path_standard; - * - change and address_index are not hardened; - * - change is 0 and is_change = false, or change is 1 and is_change = true; - * - address_index is at most MAX_BIP44_ADDRESS_INDEX_RECOMMENDED. - * - * @param[in] bip32_path - * Pointer to 32-bit integer input buffer. - * @param[in] bip32_path_len - * Maximum number of BIP32 paths in the input buffer. - * @param[in] expected_purpose - * The purpose that should be in the derivation (e.g. 44 for BIP44). - * @param[in] expected_coin_types - * Pointer to an array with the coin types that are considered acceptable. The - * elements of the array should be given as simple numbers (not their hardened version); - * for example, the coin type for Bitcoin is 0. - * Ignored if expected_coin_types_len is 0; in that case, it is only checked - * that the coin_type is hardened, as expected in the standard. - * @param[in] expected_coin_types_len - * The length of expected_coin_types. - * @param[in] expected_change - * It must be -1, 0 or 1. If -1, only checks that the provided change step is 0 or 1. If 0 or 1, - * the change step must equal `expected_change`. - * - * @return true if the given address is standard, false otherwise. - * - */ -bool is_address_path_standard(const uint32_t *bip32_path, - size_t bip32_path_len, - uint32_t expected_purpose, - const uint32_t expected_coin_types[], - size_t expected_coin_types_len, - int expected_change); - -/** - * Returns the appropriate value of the "purpose" step in a supported BIP44-compliant derivation. - * - * @param[in] address_type - * One of ADDRESS_TYPE_LEGACY, ADDRESS_TYPE_WIT, ADDRESS_TYPE_SH_WIT, ADDRESS_TYPE_TR. - * - * @return the correct BIP44 purpose, or -1 if the `address_type` parameter is wrong. - */ -int get_bip44_purpose(int address_type); \ No newline at end of file diff --git a/src/common/script.c b/src/common/script.c index 0c645b7f..aeb35144 100644 --- a/src/common/script.c +++ b/src/common/script.c @@ -1,5 +1,6 @@ #include #include +#include #include #include "../common/bip32.h" @@ -122,60 +123,70 @@ int format_opscript_script(const uint8_t script[], return -1; } - strcpy(out, "OP_RETURN "); + strncpy(out, "OP_RETURN ", MAX_OPRETURN_OUTPUT_DESC_SIZE); int out_ctr = 10; - uint8_t opcode = script[1]; // the push opcode - if (opcode > OP_16 || opcode == OP_RESERVED || opcode == OP_PUSHDATA2 || - opcode == OP_PUSHDATA4) { - return -1; // unsupported - } + // If the length of the script is 1 (just "OP_RETURN"), then it's not standard per bitcoin-core. + // However, signing such outputs is part of BIP-0322, and there's no danger in allowing them. - int hex_offset = 1; - size_t hex_length = - 0; // if non-zero, `hex_length` bytes starting from script[hex_offset] must be hex-encoded - - if (opcode == OP_0) { - if (script_len != 1 + 1) return -1; - out[out_ctr++] = '0'; - } else if (opcode >= 1 && opcode <= 75) { - hex_offset += 1; - hex_length = opcode; - - if (script_len != 1 + 1 + hex_length) return -1; - } else if (opcode == OP_PUSHDATA1) { - // OP_RETURN OP_PUSHDATA1 - hex_offset += 2; - hex_length = script[2]; - - if (script_len != 1 + 1 + 1 + hex_length || hex_length > 80) return -1; - } else if (opcode == OP_1NEGATE) { - if (script_len != 1 + 1) return -1; - - out[out_ctr++] = '-'; - out[out_ctr++] = '1'; - } else if (opcode >= OP_1 && opcode <= OP_16) { - if (script_len != 1 + 1) return -1; - - // encode OP_1 to OP_16 as a decimal number - uint8_t num = opcode - 0x50; - if (num >= 10) { - out[out_ctr++] = '0' + (num / 10); - } - out[out_ctr++] = '0' + (num % 10); + if (script_len == 1) { + --out_ctr; // remove extra space } else { - return -1; // can never happen - } + // We parse the rest as a single push opcode. + // This supports a subset of the scripts that bitcoin-core considers standard. - if (hex_length > 0) { - const char hex[] = "0123456789abcdef"; + uint8_t opcode = script[1]; // the push opcode + if (opcode > OP_16 || opcode == OP_RESERVED || opcode == OP_PUSHDATA2 || + opcode == OP_PUSHDATA4) { + return -1; // unsupported + } - out[out_ctr++] = '0'; - out[out_ctr++] = 'x'; - for (unsigned int i = 0; i < hex_length; i++) { - uint8_t data = script[hex_offset + i]; - out[out_ctr++] = hex[data / 16]; - out[out_ctr++] = hex[data % 16]; + int hex_offset = 1; + size_t hex_length = 0; // if non-zero, `hex_length` bytes starting from script[hex_offset] + // must be hex-encoded + + if (opcode == OP_0) { + if (script_len != 1 + 1) return -1; + out[out_ctr++] = '0'; + } else if (opcode >= 1 && opcode <= 75) { + hex_offset += 1; + hex_length = opcode; + + if (script_len != 1 + 1 + hex_length) return -1; + } else if (opcode == OP_PUSHDATA1) { + // OP_RETURN OP_PUSHDATA1 + hex_offset += 2; + hex_length = script[2]; + + if (script_len != 1 + 1 + 1 + hex_length || hex_length > 80) return -1; + } else if (opcode == OP_1NEGATE) { + if (script_len != 1 + 1) return -1; + + out[out_ctr++] = '-'; + out[out_ctr++] = '1'; + } else if (opcode >= OP_1 && opcode <= OP_16) { + if (script_len != 1 + 1) return -1; + + // encode OP_1 to OP_16 as a decimal number + uint8_t num = opcode - 0x50; + if (num >= 10) { + out[out_ctr++] = '0' + (num / 10); + } + out[out_ctr++] = '0' + (num % 10); + } else { + return -1; // can never happen + } + + if (hex_length > 0) { + const char hex[] = "0123456789abcdef"; + + out[out_ctr++] = '0'; + out[out_ctr++] = 'x'; + for (unsigned int i = 0; i < hex_length; i++) { + uint8_t data = script[hex_offset + i]; + out[out_ctr++] = hex[data / 16]; + out[out_ctr++] = hex[data % 16]; + } } } diff --git a/src/common/wallet.c b/src/common/wallet.c index 430b4fc0..8590cb29 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -2,6 +2,7 @@ #include #include +#include "../common/base58.h" #include "../common/bip32.h" #include "../common/buffer.h" #include "../common/script.h" @@ -358,20 +359,41 @@ int parse_policy_map_key_info(buffer_t *buffer, policy_map_key_info_t *out, int // consume the rest of the buffer into the pubkey, except possibly the final "/**" unsigned int ext_pubkey_len = 0; + char ext_pubkey_str[MAX_SERIALIZED_PUBKEY_LENGTH]; uint8_t c; while (ext_pubkey_len < MAX_SERIALIZED_PUBKEY_LENGTH && buffer_peek(buffer, &c) && is_alphanumeric(c)) { - out->ext_pubkey[ext_pubkey_len] = c; + ext_pubkey_str[ext_pubkey_len] = c; ++ext_pubkey_len; buffer_seek_cur(buffer, 1); } - out->ext_pubkey[ext_pubkey_len] = '\0'; + ext_pubkey_str[ext_pubkey_len] = '\0'; if (ext_pubkey_len < 111 || ext_pubkey_len > 112) { // loose sanity check; pubkeys in bitcoin can be 111 or 112 characters long return WITH_ERROR(-1, "Invalid extended pubkey length"); } + serialized_extended_pubkey_check_t ext_pubkey_check; + if (base58_decode(ext_pubkey_str, + ext_pubkey_len, + (uint8_t *) &ext_pubkey_check, + sizeof(ext_pubkey_check)) < 0) { + return WITH_ERROR(-1, "Error decoding serialized extended pubkey"); + } + + // verify checksum + uint8_t checksum[4]; + crypto_get_checksum((uint8_t *) &ext_pubkey_check.serialized_extended_pubkey, + sizeof(ext_pubkey_check.serialized_extended_pubkey), + checksum); + + if (memcmp(&ext_pubkey_check.checksum, checksum, sizeof(checksum)) != 0) { + return WITH_ERROR(-1, "Wrong extended pubkey checksum"); + } + + out->ext_pubkey = ext_pubkey_check.serialized_extended_pubkey; + // either the string terminates now, or it has a final "/**" suffix for the wildcard. if (!buffer_can_read(buffer, 1)) { // no wildcard; this is an error in V1 @@ -988,7 +1010,7 @@ static int parse_script(buffer_t *in_buf, return WITH_ERROR(-1, "children of and_n must be miniscript"); } - // and_n(X, Y) is equivalent to andor(X, Y, 1) + // and_n(X, Y) is equivalent to andor(X, Y, 0) // X is Bdu; Y is B const policy_node_t *X = resolve_node_ptr(&node->scripts[0]); @@ -1118,7 +1140,7 @@ static int parse_script(buffer_t *in_buf, node->base.flags.is_miniscript = 1; node->base.flags.miniscript_type = MINISCRIPT_TYPE_V; node->base.flags.miniscript_mod_z = X->flags.miniscript_mod_z & Z->flags.miniscript_mod_z; - node->base.flags.miniscript_mod_o = X->flags.miniscript_mod_o & Z->flags.miniscript_mod_o; + node->base.flags.miniscript_mod_o = X->flags.miniscript_mod_o & Z->flags.miniscript_mod_z; node->base.flags.miniscript_mod_n = 0; node->base.flags.miniscript_mod_d = 0; node->base.flags.miniscript_mod_u = 0; @@ -1343,8 +1365,8 @@ static int parse_script(buffer_t *in_buf, node->base.flags.miniscript_mod_z = (count_z == node->n) ? 1 : 0; node->base.flags.miniscript_mod_o = (count_z == node->n - 1 && count_o == 1) ? 1 : 0; node->base.flags.miniscript_mod_n = 0; - node->base.flags.miniscript_mod_d = 0; - node->base.flags.miniscript_mod_u = 0; + node->base.flags.miniscript_mod_d = 1; + node->base.flags.miniscript_mod_u = 1; // clang-format on break; @@ -1876,6 +1898,24 @@ int parse_descriptor_template(buffer_t *in_buf, void *out, size_t out_len, int v return parse_script(in_buf, &out_buf, version, 0, 0); } +int get_policy_segwit_version(const policy_node_t *policy) { + if (policy->type == TOKEN_TR) { + return 1; + } else if (policy->type == TOKEN_SH) { + const policy_node_t *inner = + resolve_node_ptr(&((const policy_node_with_script_t *) policy)->script); + if (inner->type == TOKEN_WPKH || inner->type == TOKEN_WSH) { + return 0; // wrapped segwit + } else { + return -1; // legacy + } + } else if (policy->type == TOKEN_WPKH || policy->type == TOKEN_WSH) { + return 0; // native segwit + } else { + return -1; // legacy + } +} + /** * Convenience function that returns a + b, except: * - returns -1 if any of a and b is negative diff --git a/src/common/wallet.h b/src/common/wallet.h index 33e391cf..b96e1781 100644 --- a/src/common/wallet.h +++ b/src/common/wallet.h @@ -6,6 +6,7 @@ #include "common/bip32.h" #include "common/buffer.h" #include "../constants.h" +#include "../crypto.h" #ifndef SKIP_FOR_CMOCKA #include "os.h" @@ -80,7 +81,7 @@ typedef struct { uint8_t master_key_derivation_len; uint8_t has_key_origin; uint8_t has_wildcard; // true iff the keys ends with the wildcard (/ followed by **) - char ext_pubkey[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; + serialized_extended_pubkey_t ext_pubkey; } policy_map_key_info_t; typedef struct { @@ -383,7 +384,7 @@ int parse_policy_map_key_info(buffer_t *buffer, policy_map_key_info_t *out, int * When parsing descriptors containing miniscript, this fails if the miniscript is not correct, * as defined by the miniscript type system. * This does NOT check non-malleability of the miniscript. - * * @param in_buf the buffer containing the policy map to parse + * @param in_buf the buffer containing the policy map to parse * @param out the pointer to the output buffer, which must be 4-byte aligned * @param out_len the length of the output buffer * @param version either WALLET_POLICY_VERSION_V1 or WALLET_POLICY_VERSION_V2 @@ -392,6 +393,17 @@ int parse_policy_map_key_info(buffer_t *buffer, policy_map_key_info_t *out, int */ int parse_descriptor_template(buffer_t *in_buf, void *out, size_t out_len, int version); +/** + * Given a valid policy that the bitcoin app is able to sign, returns the segwit version. + * The result is undefined for a node that is not a valid root of a wallet policy that the bitcoin + * app is able to sign. + * + * @param policy the root node of the wallet policy + * @return -1 if it's a legacy policy, 0 if it is a policy for SegwitV0 (possibly nested), 1 for + * SegwitV1 (taproot). + */ +int get_policy_segwit_version(const policy_node_t *policy); + /** * Computes additional properties of the given miniscript, to detect malleability and other security * properties to assess if the miniscript is sane. diff --git a/src/constants.h b/src/constants.h index 72590ebd..4a83dac6 100644 --- a/src/constants.h +++ b/src/constants.h @@ -5,6 +5,11 @@ */ #define CLA_APP 0xE1 +/** + * Encodes the protocol version, which is passed in the p2 field of APDUs. + */ +#define CURRENT_PROTOCOL_VERSION 1 + /** * Maximum length of a serialized address (in characters). * Segwit addresses can reach 74 characters; 76 on regtest because of the longer "bcrt" prefix. diff --git a/src/crypto.c b/src/crypto.c index fd051d09..2dab98f6 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -27,6 +27,7 @@ #include "cx_ram.h" #include "lcx_ripemd160.h" #include "cx_ripemd160.h" +#include "lib_standard_app/crypto_helpers.h" #include "common/base58.h" #include "common/bip32.h" @@ -78,50 +79,14 @@ static const uint8_t BIP0341_taptweak_tag[] = {'T', 'a', 'p', 'T', 'w', 'e', 'a' static const uint8_t BIP0341_tapbranch_tag[] = {'T', 'a', 'p', 'B', 'r', 'a', 'n', 'c', 'h'}; static const uint8_t BIP0341_tapleaf_tag[] = {'T', 'a', 'p', 'L', 'e', 'a', 'f'}; -static int secp256k1_point(const uint8_t scalar[static 32], uint8_t out[static 65]); - /** * Gets the point on the SECP256K1 that corresponds to kG, where G is the curve's generator point. - * Returns 0 if point is Infinity, encoding length otherwise. + * Returns -1 if point is Infinity or any error occurs; 0 otherwise. */ static int secp256k1_point(const uint8_t k[static 32], uint8_t out[static 65]) { memcpy(out, secp256k1_generator, 65); - return cx_ecfp_scalar_mult(CX_CURVE_SECP256K1, out, 65, k, 32); -} - -int crypto_derive_private_key(cx_ecfp_private_key_t *private_key, - uint8_t *chain_code, - const uint32_t *bip32_path, - uint8_t bip32_path_len) { - uint8_t raw_private_key[32] = {0}; - - int ret = 0; - BEGIN_TRY { - TRY { - // derive the seed with bip32_path - - os_perso_derive_node_bip32(CX_CURVE_256K1, - bip32_path, - bip32_path_len, - raw_private_key, - chain_code); - - // new private_key from raw - cx_ecfp_init_private_key(CX_CURVE_256K1, - raw_private_key, - sizeof(raw_private_key), - private_key); - } - CATCH_ALL { - ret = -1; - } - FINALLY { - explicit_bzero(&raw_private_key, sizeof(raw_private_key)); - } - } - END_TRY; - - return ret; + if (CX_OK != cx_ecfp_scalar_mult_no_throw(CX_CURVE_SECP256K1, out, k, 32)) return -1; + return 0; } int bip32_CKDpub(const serialized_extended_pubkey_t *parent, @@ -134,7 +99,7 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, } if (parent->depth == 255) { - return -2; // maximum derivation depth reached + return -1; // maximum derivation depth reached } uint8_t I[64]; @@ -152,7 +117,8 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, uint8_t *I_R = &I[32]; // fail if I_L is not smaller than the group order n, but the probability is < 1/2^128 - if (cx_math_cmp(I_L, secp256k1_n, 32) >= 0) { + int diff; + if (CX_OK != cx_math_cmp_no_throw(I_L, secp256k1_n, 32, &diff) || diff >= 0) { return -1; } @@ -161,18 +127,15 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, { // make sure that heavy memory allocations are freed as soon as possible // compute point(I_L) uint8_t P[65]; - secp256k1_point(I_L, P); + if (0 > secp256k1_point(I_L, P)) return -1; uint8_t K_par[65]; crypto_get_uncompressed_pubkey(parent->compressed_pubkey, K_par); // add K_par - if (cx_ecfp_add_point(CX_CURVE_SECP256K1, - child_uncompressed_pubkey, - P, - K_par, - sizeof(child_uncompressed_pubkey)) == 0) { - return -3; // the point at infinity is not a valid child pubkey (should never happen in + if (CX_OK != + cx_ecfp_add_point_no_throw(CX_CURVE_SECP256K1, child_uncompressed_pubkey, P, K_par)) { + return -1; // the point at infinity is not a valid child pubkey (should never happen in // practice) } } @@ -248,15 +211,18 @@ int crypto_get_uncompressed_pubkey(const uint8_t compressed_key[static 33], // we use y for intermediate results, in order to save memory uint8_t e = 3; - cx_math_powm(y, x, &e, 1, secp256k1_p, 32); // tmp = x^3 (mod p) + if (CX_OK != cx_math_powm_no_throw(y, x, &e, 1, secp256k1_p, 32)) + return -1; // tmp = x^3 (mod p) uint8_t scalar[32] = {0}; scalar[31] = 7; - cx_math_addm(y, y, scalar, secp256k1_p, 32); // tmp = x^3 + 7 (mod p) - cx_math_powm(y, y, secp256k1_sqr_exponent, 32, secp256k1_p, 32); // tmp = sqrt(x^3 + 7) (mod p) + if (CX_OK != cx_math_addm_no_throw(y, y, scalar, secp256k1_p, 32)) + return -1; // tmp = x^3 + 7 (mod p) + if (CX_OK != cx_math_powm_no_throw(y, y, secp256k1_sqr_exponent, 32, secp256k1_p, 32)) + return -1; // tmp = sqrt(x^3 + 7) (mod p) // if the prefix and y don't have the same parity, take the opposite root (mod p) if (((prefix ^ y[31]) & 1) != 0) { - cx_math_sub(y, secp256k1_p, y, 32); + if (CX_OK != cx_math_sub_no_throw(y, secp256k1_p, y, 32)) return -1; } out[0] = 0x04; @@ -275,47 +241,22 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], uint8_t bip32_path_len, uint8_t pubkey[static 33], uint8_t chain_code[]) { - struct { - uint8_t prefix; - uint8_t raw_public_key[64]; - uint8_t chain_code[32]; - } keydata; - - cx_ecfp_private_key_t private_key = {0}; - cx_ecfp_public_key_t public_key; - - bool result = true; - BEGIN_TRY { - TRY { - keydata.prefix = 0x04; // uncompressed public keys always start with 04 - // derive private key according to BIP32 path - crypto_derive_private_key(&private_key, keydata.chain_code, bip32_path, bip32_path_len); - - if (chain_code != NULL) { - memmove(chain_code, keydata.chain_code, 32); - } - - // generate corresponding public key - cx_ecfp_generate_pair(CX_CURVE_256K1, &public_key, &private_key, 1); - - memmove(keydata.raw_public_key, public_key.W + 1, 64); + uint8_t raw_public_key[65]; + + if (bip32_derive_get_pubkey_256(CX_CURVE_256K1, + bip32_path, + bip32_path_len, + raw_public_key, + chain_code, + CX_SHA512) != CX_OK) { + return false; + } - // compute compressed public key - if (crypto_get_compressed_pubkey((uint8_t *) &keydata, pubkey) < 0) { - result = false; - } - } - CATCH_ALL { - result = false; - } - FINALLY { - // delete sensitive data - explicit_bzero(keydata.chain_code, 32); - explicit_bzero(&private_key, sizeof(private_key)); - } + if (crypto_get_compressed_pubkey(raw_public_key, pubkey) < 0) { + return false; } - END_TRY; - return result; + + return true; } uint32_t crypto_get_key_fingerprint(const uint8_t pub_key[static 33]) { @@ -332,33 +273,33 @@ uint32_t crypto_get_master_key_fingerprint() { return crypto_get_key_fingerprint(master_pub_key); } -void crypto_derive_symmetric_key(const char *label, size_t label_len, uint8_t key[static 32]) { +bool crypto_derive_symmetric_key(const char *label, size_t label_len, uint8_t key[static 32]) { // TODO: is there a better way? - // The label is a byte string in SLIP-0021, but os_perso_derive_node_with_seed_key + // The label is a byte string in SLIP-0021, but os_derive_bip32_with_seed_no_throw // accesses the `path` argument as an array of uint32_t, causing a device freeze if memory // is not aligned. uint8_t label_copy[32] __attribute__((aligned(4))); memcpy(label_copy, label, label_len); - os_perso_derive_node_with_seed_key(HDW_SLIP21, - CX_CURVE_SECP256K1, - (uint32_t *) label_copy, - label_len, - key, - NULL, - NULL, - 0); + if (os_derive_bip32_with_seed_no_throw(HDW_SLIP21, + CX_CURVE_SECP256K1, + (uint32_t *) label_copy, + label_len, + key, + NULL, + NULL, + 0) != CX_OK) { + return false; + } + + return true; } -// TODO: Split serialization from key derivation? -// It might be difficult to have a clean API without wasting memory, as the checksum -// needs to be concatenated to the data before base58 serialization. -int get_serialized_extended_pubkey_at_path(const uint32_t bip32_path[], - uint8_t bip32_path_len, - uint32_t bip32_pubkey_version, - char out_xpub[static MAX_SERIALIZED_PUBKEY_LENGTH + 1], - serialized_extended_pubkey_t *out_pubkey) { +int get_extended_pubkey_at_path(const uint32_t bip32_path[], + uint8_t bip32_path_len, + uint32_t bip32_pubkey_version, + serialized_extended_pubkey_t *out_pubkey) { // find parent key's fingerprint and child number uint32_t parent_fingerprint = 0; uint32_t child_number = 0; @@ -367,43 +308,30 @@ int get_serialized_extended_pubkey_at_path(const uint32_t bip32_path[], // for the response, in order to save memory uint8_t parent_pubkey[33]; - crypto_get_compressed_pubkey_at_path(bip32_path, bip32_path_len - 1, parent_pubkey, NULL); + if (!crypto_get_compressed_pubkey_at_path(bip32_path, + bip32_path_len - 1, + parent_pubkey, + NULL)) { + return -1; + } parent_fingerprint = crypto_get_key_fingerprint(parent_pubkey); child_number = bip32_path[bip32_path_len - 1]; } - struct { - serialized_extended_pubkey_t ext_pubkey; - uint8_t checksum[4]; - } ext_pubkey_check; // extended pubkey and checksum - - serialized_extended_pubkey_t *ext_pubkey = &ext_pubkey_check.ext_pubkey; + write_u32_be(out_pubkey->version, 0, bip32_pubkey_version); + out_pubkey->depth = bip32_path_len; + write_u32_be(out_pubkey->parent_fingerprint, 0, parent_fingerprint); + write_u32_be(out_pubkey->child_number, 0, child_number); - write_u32_be(ext_pubkey->version, 0, bip32_pubkey_version); - ext_pubkey->depth = bip32_path_len; - write_u32_be(ext_pubkey->parent_fingerprint, 0, parent_fingerprint); - write_u32_be(ext_pubkey->child_number, 0, child_number); - - crypto_get_compressed_pubkey_at_path(bip32_path, - bip32_path_len, - ext_pubkey->compressed_pubkey, - ext_pubkey->chain_code); - crypto_get_checksum((uint8_t *) ext_pubkey, 78, ext_pubkey_check.checksum); - - if (out_pubkey != NULL) { - memcpy(out_pubkey, &ext_pubkey_check.ext_pubkey, sizeof(ext_pubkey_check.ext_pubkey)); + if (!crypto_get_compressed_pubkey_at_path(bip32_path, + bip32_path_len, + out_pubkey->compressed_pubkey, + out_pubkey->chain_code)) { + return -1; } - int serialized_pubkey_len = base58_encode((uint8_t *) &ext_pubkey_check, - 78 + 4, - out_xpub, - MAX_SERIALIZED_PUBKEY_LENGTH); - - if (serialized_pubkey_len > 0) { - out_xpub[serialized_pubkey_len] = '\0'; - } - return serialized_pubkey_len; + return 0; } int base58_encode_address(const uint8_t in[20], uint32_t version, char *out, size_t out_len) { @@ -436,38 +364,45 @@ int crypto_ecdsa_sign_sha256_hash_with_key(const uint32_t bip32_path[], cx_ecfp_public_key_t public_key; uint32_t info_internal = 0; - int sig_len = 0; - bool error = false; - BEGIN_TRY { - TRY { - crypto_derive_private_key(&private_key, NULL, bip32_path, bip32_path_len); - sig_len = cx_ecdsa_sign(&private_key, - CX_RND_RFC6979, - CX_SHA256, - hash, - 32, - out, - MAX_DER_SIG_LEN, - &info_internal); - - // generate corresponding public key - cx_ecfp_generate_pair(CX_CURVE_256K1, &public_key, &private_key, 1); - - if (pubkey != NULL) { - // compute compressed public key - if (crypto_get_compressed_pubkey(public_key.W, pubkey) < 0) { - error = true; - } - } - } - CATCH_ALL { - error = true; + size_t sig_len = MAX_DER_SIG_LEN; + bool error = true; + + if (bip32_derive_init_privkey_256(CX_CURVE_256K1, + bip32_path, + bip32_path_len, + &private_key, + NULL) != CX_OK) { + goto end; + } + + if (cx_ecdsa_sign_no_throw(&private_key, + CX_RND_RFC6979, + CX_SHA256, + hash, + 32, + out, + &sig_len, + &info_internal) != CX_OK) { + goto end; + } + + if (pubkey != NULL) { + // Generate associated pubkey + if (cx_ecfp_generate_pair_no_throw(CX_CURVE_256K1, &public_key, &private_key, true) != + CX_OK) { + goto end; } - FINALLY { - explicit_bzero(&private_key, sizeof(private_key)); + + // compute compressed public key + if (crypto_get_compressed_pubkey(public_key.W, pubkey) < 0) { + goto end; } } - END_TRY; + + error = false; + +end: + explicit_bzero(&private_key, sizeof(private_key)); if (error) { // unexpected error when signing @@ -505,24 +440,27 @@ static int crypto_tr_lift_x(const uint8_t x[static 32], uint8_t out[static 65]) uint8_t *c = out + 1; uint8_t e = 3; - cx_math_powm(c, x, &e, 1, secp256k1_p, 32); // c = x^3 (mod p) + if (CX_OK != cx_math_powm_no_throw(c, x, &e, 1, secp256k1_p, 32)) return -1; // c = x^3 (mod p) uint8_t scalar[32] = {0}; scalar[31] = 7; - cx_math_addm(c, c, scalar, secp256k1_p, 32); // c = x^3 + 7 (mod p) + if (CX_OK != cx_math_addm_no_throw(c, c, scalar, secp256k1_p, 32)) + return -1; // c = x^3 + 7 (mod p) - cx_math_powm(y, c, secp256k1_sqr_exponent, 32, secp256k1_p, 32); // y = sqrt(x^3 + 7) (mod p) + if (CX_OK != cx_math_powm_no_throw(y, c, secp256k1_sqr_exponent, 32, secp256k1_p, 32)) + return -1; // y = sqrt(x^3 + 7) (mod p) // sanity check: fail if y * y % p != x^3 + 7 uint8_t y_2[32]; e = 2; - cx_math_powm(y_2, y, &e, 1, secp256k1_p, 32); // y^2 (mod p) - if (cx_math_cmp(y_2, c, 32) != 0) { + if (CX_OK != cx_math_powm_no_throw(y_2, y, &e, 1, secp256k1_p, 32)) return -1; // y^2 (mod p) + int diff; + if (CX_OK != cx_math_cmp_no_throw(y_2, c, 32, &diff) || diff != 0) { return -1; } if (y[31] & 1) { // y must be even: take the negation - cx_math_sub(out + 1 + 32, secp256k1_p, y, 32); + if (CX_OK != cx_math_sub_no_throw(out + 1 + 32, secp256k1_p, y, 32)) return -1; } // add the 0x04 prefix; copy x verbatim @@ -591,7 +529,8 @@ int crypto_tr_tweak_pubkey(const uint8_t pubkey[static 32], t); // fail if t is not smaller than the curve order - if (cx_math_cmp(t, secp256k1_n, 32) >= 0) { + int diff; + if (CX_OK != cx_math_cmp_no_throw(t, secp256k1_n, 32, &diff) || diff >= 0) { return -1; } @@ -602,13 +541,13 @@ int crypto_tr_tweak_pubkey(const uint8_t pubkey[static 32], return -1; } - if (secp256k1_point(t, Q) == 0) { - // point at infinity + if (0 > secp256k1_point(t, Q)) { + // point at infinity, or error return -1; } - if (cx_ecfp_add_point(CX_CURVE_SECP256K1, Q, Q, lifted_pubkey, sizeof(Q)) == 0) { - return -1; // the point at infinity is not valid (should never happen in practice) + if (CX_OK != cx_ecfp_add_point_no_throw(CX_CURVE_SECP256K1, Q, Q, lifted_pubkey)) { + return -1; // error, or point at Infinity } *y_parity = Q[64] & 1; @@ -623,45 +562,36 @@ int crypto_tr_tweak_seckey(const uint8_t seckey[static 32], uint8_t out[static 32]) { uint8_t P[65]; - int ret = 0; - BEGIN_TRY { - TRY { - secp256k1_point(seckey, P); - - memmove(out, seckey, 32); - - if (P[64] & 1) { - // odd y, negate the secret key - cx_math_sub(out, secp256k1_n, out, 32); - } - - uint8_t t[32]; - crypto_tr_tagged_hash(BIP0341_taptweak_tag, - sizeof(BIP0341_taptweak_tag), - &P[1], // P[1:33] is x(P) - 32, - h, - h_len, - t); - - // fail if t is not smaller than the curve order - if (cx_math_cmp(t, secp256k1_n, 32) >= 0) { - CLOSE_TRY; - ret = -1; - goto end; - } - - cx_math_addm(out, out, t, secp256k1_n, 32); - } - CATCH_ALL { - ret = -1; - } - FINALLY { - end: - explicit_bzero(&P, sizeof(P)); + int ret = -1; + do { // loop to break out in case of error + if (0 > secp256k1_point(seckey, P)) break; + + memmove(out, seckey, 32); + + if (P[64] & 1) { + // odd y, negate the secret key + if (CX_OK != cx_math_sub_no_throw(out, secp256k1_n, out, 32)) break; } - } - END_TRY; + + uint8_t t[32]; + crypto_tr_tagged_hash(BIP0341_taptweak_tag, + sizeof(BIP0341_taptweak_tag), + &P[1], // P[1:33] is x(P) + 32, + h, + h_len, + t); + + // fail if t is not smaller than the curve order + int diff; + if (CX_OK != cx_math_cmp_no_throw(t, secp256k1_n, 32, &diff) || diff >= 0) break; + + if (CX_OK != cx_math_addm_no_throw(out, out, t, secp256k1_n, 32)) break; + + ret = 0; + } while (0); + + explicit_bzero(&P, sizeof(P)); return ret; } diff --git a/src/crypto.h b/src/crypto.h index a5cf0e53..4765ca18 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -30,26 +30,6 @@ typedef struct { uint8_t checksum[4]; } serialized_extended_pubkey_check_t; -/** - * Derive private key given BIP32 path. - * It must be wrapped in a TRY block that wipes the output private key in the FINALLY block. - * - * @param[out] private_key - * Pointer to private key. - * @param[out] chain_code - * Pointer to 32 bytes array for chain code, or NULL if the chain_code is not required. - * @param[in] bip32_path - * Pointer to buffer with BIP32 path. - * @param[in] bip32_path_len - * Number of path in BIP32 path. - * - * @return 0 if success, -1 otherwise. - */ -int crypto_derive_private_key(cx_ecfp_private_key_t *private_key, - uint8_t *chain_code, - const uint32_t *bip32_path, - uint8_t bip32_path_len); - /** * Generates the child extended public key, from a parent extended public key and non-hardened * index. @@ -70,7 +50,7 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, serialized_extended_pubkey_t *child); /** - * Convenience wrapper for cx_hash to add some data to an initialized hash context. + * Convenience wrapper for cx_hash_no_throw to add some data to an initialized hash context. * * @param[in] hash_context * The context of the hash, which must already be initialized. @@ -79,14 +59,14 @@ int bip32_CKDpub(const serialized_extended_pubkey_t *parent, * @param[in] in_len * Size of the passed data. * - * @return the return value of cx_hash. + * @return the return value of cx_hash_no_throw. */ static inline int crypto_hash_update(cx_hash_t *hash_context, const void *in, size_t in_len) { - return cx_hash(hash_context, 0, in, in_len, NULL, 0); + return cx_hash_no_throw(hash_context, 0, in, in_len, NULL, 0); } /** - * Convenience wrapper for cx_hash to compute the final hash, without adding any extra data + * Convenience wrapper for cx_hash_no_throw to compute the final hash, without adding any extra data * to the hash context. * * @param[in] hash_context @@ -96,10 +76,10 @@ static inline int crypto_hash_update(cx_hash_t *hash_context, const void *in, si * @param[in] out_len * Size of output buffer, which must be large enough to contain the result. * - * @return the return value of cx_hash. + * @return the return value of cx_hash_no_throw. */ static inline int crypto_hash_digest(cx_hash_t *hash_context, uint8_t *out, size_t out_len) { - return cx_hash(hash_context, CX_LAST, NULL, 0, out, out_len); + return cx_hash_no_throw(hash_context, CX_LAST, NULL, 0, out, out_len); } /** @@ -280,23 +260,18 @@ uint32_t crypto_get_master_key_fingerprint(); * Number of steps in the BIP32 derivation. * @param[in] bip32_pubkey_version * Version prefix to use for the pubkey. - * @param[out] out_xpub - * Pointer to the output buffer, which must be long enough to contain the result - * (including the terminating null character). * @param[out] out_pubkey - * If not NULL, pointer to a serialized_extended_pubkey_t. + * A pointer to a serialized_extended_pubkey_t. * - * @return the length of the output pubkey (not including the null character), or -1 on error. + * @return 0 on success, or -1 on error. */ -int get_serialized_extended_pubkey_at_path(const uint32_t bip32_path[], - uint8_t bip32_path_len, - uint32_t bip32_pubkey_version, - char out_xpub[static MAX_SERIALIZED_PUBKEY_LENGTH + 1], - serialized_extended_pubkey_t *out_pubkey); +int get_extended_pubkey_at_path(const uint32_t bip32_path[], + uint8_t bip32_path_len, + uint32_t bip32_pubkey_version, + serialized_extended_pubkey_t *out_pubkey); /** * Derives the level-1 symmetric key at the given label using SLIP-0021. - * Must be wrapped in a TRY/FINALLY block to make sure that the output key is wiped after using it. * * @param[in] label * Pointer to the label. The first byte of the label must be 0x00 to comply with SLIP-0021. @@ -305,7 +280,7 @@ int get_serialized_extended_pubkey_at_path(const uint32_t bip32_path[], * @param[out] key * Pointer to a 32-byte output buffer that will contain the generated key. */ -void crypto_derive_symmetric_key(const char *label, size_t label_len, uint8_t key[static 32]); +bool crypto_derive_symmetric_key(const char *label, size_t label_len, uint8_t key[static 32]); /** * Encodes a 20-bytes hash in base58 with checksum, after prepending a version prefix. diff --git a/src/debug-helpers/debug.c b/src/debug-helpers/debug.c index 3a9dc5be..d01f2436 100644 --- a/src/debug-helpers/debug.c +++ b/src/debug-helpers/debug.c @@ -32,12 +32,11 @@ int semihosted_printf(const char *format, ...) { // Returns the current stack pointer static unsigned int __attribute__((noinline)) get_stack_pointer() { - int stack_top = 0; - // Returning an address on the stack is unusual, so we disable the warning -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wreturn-stack-address" - return (unsigned int) &stack_top; -#pragma GCC diagnostic pop + unsigned int stack_top = 0; + + __asm__ __volatile__("mov %0, sp" : "=r"(stack_top) : :); + + return stack_top; } void print_stack_pointer(const char *file, int line, const char *func_name) { diff --git a/src/handler/get_extended_pubkey.c b/src/handler/get_extended_pubkey.c index c8d783aa..4a21c467 100644 --- a/src/handler/get_extended_pubkey.c +++ b/src/handler/get_extended_pubkey.c @@ -20,6 +20,7 @@ #include "boilerplate/io.h" #include "boilerplate/dispatcher.h" #include "boilerplate/sw.h" +#include "../common/base58.h" #include "../common/bip32.h" #include "../commands.h" #include "../constants.h" @@ -29,10 +30,7 @@ #define H 0x80000000ul -static bool is_path_safe_for_pubkey_export(const uint32_t bip32_path[], - size_t bip32_path_len, - const uint32_t coin_types[], - size_t coin_types_length) { +static bool is_path_safe_for_pubkey_export(const uint32_t bip32_path[], size_t bip32_path_len) { // Exception for Electrum: it historically used "m/4541509h/1112098098h" // to derive encryption keys, so we whitelist it. if (bip32_path_len == 2 && bip32_path[0] == (4541509 ^ H) && @@ -85,14 +83,7 @@ static bool is_path_safe_for_pubkey_export(const uint32_t bip32_path[], } uint32_t coin_type = bip32_path[1] & 0x7FFFFFFF; - bool coin_type_found = false; - for (unsigned int i = 0; i < coin_types_length; i++) { - if (coin_type == coin_types[i]) { - coin_type_found = true; - } - } - - if (!coin_type_found) { + if (coin_type != BIP44_COIN_TYPE) { return false; } @@ -114,8 +105,8 @@ static bool is_path_safe_for_pubkey_export(const uint32_t bip32_path[], return true; } -void handler_get_extended_pubkey(dispatcher_context_t *dc, uint8_t p2) { - (void) p2; +void handler_get_extended_pubkey(dispatcher_context_t *dc, uint8_t protocol_version) { + (void) protocol_version; LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -144,35 +135,48 @@ void handler_get_extended_pubkey(dispatcher_context_t *dc, uint8_t p2) { return; } - uint32_t coin_types[2] = {BIP44_COIN_TYPE, BIP44_COIN_TYPE_2}; - bool is_safe = is_path_safe_for_pubkey_export(bip32_path, bip32_path_len, coin_types, 2); + bool is_safe = is_path_safe_for_pubkey_export(bip32_path, bip32_path_len); if (!is_safe && !display) { SEND_SW(dc, SW_NOT_SUPPORTED); return; } - char serialized_pubkey_str[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; + serialized_extended_pubkey_check_t pubkey_check; + if (0 > get_extended_pubkey_at_path(bip32_path, + bip32_path_len, + BIP32_PUBKEY_VERSION, + &pubkey_check.serialized_extended_pubkey)) { + PRINTF("Failed getting bip32 pubkey\n"); + SEND_SW(dc, SW_BAD_STATE); + return; + } + + crypto_get_checksum((uint8_t *) &pubkey_check.serialized_extended_pubkey, + sizeof(pubkey_check.serialized_extended_pubkey), + pubkey_check.checksum); - int serialized_pubkey_len = get_serialized_extended_pubkey_at_path(bip32_path, - bip32_path_len, - BIP32_PUBKEY_VERSION, - serialized_pubkey_str, - NULL); - if (serialized_pubkey_len == -1) { + char pubkey_str[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; + int pubkey_str_len = base58_encode((uint8_t *) &pubkey_check, + sizeof(pubkey_check), + pubkey_str, + sizeof(pubkey_str)); + if (pubkey_str_len != 111 && pubkey_str_len != 112) { + PRINTF("Failed encoding base58 pubkey\n"); SEND_SW(dc, SW_BAD_STATE); return; } + pubkey_str[pubkey_str_len] = 0; char path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1] = "(Master key)"; if (bip32_path_len > 0) { bip32_path_format(bip32_path, bip32_path_len, path_str, sizeof(path_str)); } - if (display && !ui_display_pubkey(dc, path_str, !is_safe, serialized_pubkey_str)) { + if (display && !ui_display_pubkey(dc, path_str, !is_safe, pubkey_str)) { SEND_SW(dc, SW_DENY); return; } - SEND_RESPONSE(dc, serialized_pubkey_str, strlen(serialized_pubkey_str), SW_OK); + SEND_RESPONSE(dc, pubkey_str, pubkey_str_len, SW_OK); } diff --git a/src/handler/get_master_fingerprint.c b/src/handler/get_master_fingerprint.c index e6d26ca1..4f46917c 100644 --- a/src/handler/get_master_fingerprint.c +++ b/src/handler/get_master_fingerprint.c @@ -24,8 +24,8 @@ #include "handlers.h" -void handler_get_master_fingerprint(dispatcher_context_t *dc, uint8_t p2) { - (void) p2; +void handler_get_master_fingerprint(dispatcher_context_t *dc, uint8_t protocol_version) { + (void) protocol_version; // Device must be unlocked if (os_global_pin_is_validated() != BOLOS_UX_OK) { diff --git a/src/handler/get_wallet_address.c b/src/handler/get_wallet_address.c index 34454550..d1f1744a 100644 --- a/src/handler/get_wallet_address.c +++ b/src/handler/get_wallet_address.c @@ -42,8 +42,8 @@ #include "handlers.h" #include "client_commands.h" -void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { - (void) p2; +void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_version) { + (void) protocol_version; LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -61,8 +61,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { uint8_t wallet_id[32]; uint8_t wallet_hmac[32]; - bool is_wallet_canonical; - int address_type; + bool is_wallet_default; // whether the wallet policy can be used without being registered policy_map_wallet_header_t wallet_header; @@ -93,6 +92,10 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { SEND_SW(dc, SW_WRONG_DATA_LENGTH); return; } + if (address_index >= BIP32_FIRST_HARDENED_CHILD) { + SEND_SW(dc, SW_INCORRECT_DATA); // it must be unhardened + return; + } { uint8_t serialized_wallet_policy[MAX_WALLET_POLICY_SERIALIZED_LENGTH]; @@ -129,93 +132,26 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { } if (hmac_or == 0) { - // No hmac, verify that the policy is a canonical one that is allowed by default - address_type = get_policy_address_type(&wallet_policy_map.parsed); - if (address_type == -1) { - PRINTF("Non-standard policy, and no hmac provided\n"); - SEND_SW(dc, SW_SIGNATURE_FAIL); - return; - } - - if (wallet_header.n_keys != 1) { - PRINTF("Standard wallets must have exactly 1 key\n"); - SEND_SW(dc, SW_INCORRECT_DATA); - return; - } - - // we check if the key is indeed internal - uint32_t master_key_fingerprint = crypto_get_master_key_fingerprint(); - - uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; - int key_info_len = call_get_merkle_leaf_element(dc, - wallet_header.keys_info_merkle_root, - wallet_header.n_keys, - 0, // only one key - key_info_str, - sizeof(key_info_str)); - if (key_info_len < 0) { - SEND_SW(dc, SW_INCORRECT_DATA); - return; - } - - // Make a sub-buffer for the pubkey info - buffer_t key_info_buffer = buffer_create(key_info_str, key_info_len); - - policy_map_key_info_t key_info; - if (parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_header.version) == -1) { - SEND_SW(dc, SW_INCORRECT_DATA); - return; - } + // No hmac, verify that the policy is indeed a default one - if (read_u32_be(key_info.master_key_fingerprint, 0) != master_key_fingerprint) { + if (!is_wallet_policy_standard(dc, &wallet_header, &wallet_policy_map.parsed)) { SEND_SW(dc, SW_INCORRECT_DATA); return; } - // generate pubkey and check if it matches - char pubkey_derived[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; - int serialized_pubkey_len = - get_serialized_extended_pubkey_at_path(key_info.master_key_derivation, - key_info.master_key_derivation_len, - BIP32_PUBKEY_VERSION, - pubkey_derived, - NULL); - if (serialized_pubkey_len == -1) { - PRINTF("Failed to derive pubkey\n"); - SEND_SW(dc, SW_BAD_STATE); - return; - } - - if (strncmp(key_info.ext_pubkey, pubkey_derived, MAX_SERIALIZED_PUBKEY_LENGTH) != 0) { + if (wallet_header.name_len != 0) { + PRINTF("Name must be zero-length for a standard wallet policy\n"); SEND_SW(dc, SW_INCORRECT_DATA); return; } - // check if derivation path is indeed standard - - // Based on the address type, we set the expected bip44 purpose for this canonical wallet - int bip44_purpose = get_bip44_purpose(address_type); - - if (key_info.master_key_derivation_len != 3) { - SEND_SW(dc, SW_INCORRECT_DATA); - return; - } - - uint32_t coin_types[2] = {BIP44_COIN_TYPE, BIP44_COIN_TYPE_2}; - - uint32_t bip32_path[5]; - for (int i = 0; i < 3; i++) { - bip32_path[i] = key_info.master_key_derivation[i]; - } - bip32_path[3] = is_change ? 1 : 0; - bip32_path[4] = address_index; - - if (!is_address_path_standard(bip32_path, 5, bip44_purpose, coin_types, 2, -1)) { + if (address_index > MAX_BIP44_ADDRESS_INDEX_RECOMMENDED) { + PRINTF("Address index is too large\n"); SEND_SW(dc, SW_INCORRECT_DATA); return; } - is_wallet_canonical = true; + is_wallet_default = true; } else { // Verify hmac @@ -225,12 +161,12 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { return; } - is_wallet_canonical = false; + is_wallet_default = false; } - // Swap feature: check that wallet is canonical - if (G_swap_state.called_from_swap && !is_wallet_canonical) { - PRINTF("Must be a canonical wallet for swap feature\n"); + // Swap feature: check that the wallet policy is a default one + if (G_swap_state.called_from_swap && !is_wallet_default) { + PRINTF("Must be a default wallet policy for swap feature\n"); SEND_SW(dc, SW_INCORRECT_DATA); return; } @@ -278,7 +214,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t p2) { if (display_address != 0) { if (!ui_display_wallet_address(dc, - is_wallet_canonical ? NULL : wallet_header.name, + is_wallet_default ? NULL : wallet_header.name, address)) { SEND_SW(dc, SW_DENY); return; diff --git a/src/handler/lib/get_preimage.c b/src/handler/lib/get_preimage.c index ea0d0f75..b5f5f5cd 100644 --- a/src/handler/lib/get_preimage.c +++ b/src/handler/lib/get_preimage.c @@ -86,8 +86,7 @@ int call_get_preimage(dispatcher_context_t *dispatcher_context, return -8; } - uint8_t *data_ptr = - dispatcher_context->read_buffer.ptr + dispatcher_context->read_buffer.offset; + data_ptr = dispatcher_context->read_buffer.ptr + dispatcher_context->read_buffer.offset; // update hash crypto_hash_update(&hash_context.header, data_ptr, n_bytes); diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 688b5c6c..7a5a3667 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -7,6 +7,7 @@ #include "../../crypto.h" #include "../../common/base58.h" #include "../../common/bitvector.h" +#include "../../common/read.h" #include "../../common/script.h" #include "../../common/segwit_addr.h" #include "../../common/wallet.h" @@ -237,6 +238,7 @@ static const generic_processor_command_t commands_or_i[] = {{CMD_CODE_OP, OP_IF} static const generic_processor_command_t commands_a[] = {{CMD_CODE_OP, OP_TOALTSTACK}, {CMD_CODE_PROCESS_CHILD, 0}, + {CMD_CODE_OP, OP_FROMALTSTACK}, {CMD_CODE_END, 0}}; static const generic_processor_command_t commands_s[] = {{CMD_CODE_OP, OP_SWAP}, @@ -325,9 +327,9 @@ int read_and_parse_wallet_policy( /** * Pushes a node onto the stack. Returns 0 on success, -1 if the stack is exhausted. */ -static int state_stack_push(policy_parser_state_t *state, - const policy_node_t *policy_node, - uint8_t flags) { +__attribute__((warn_unused_result)) static int state_stack_push(policy_parser_state_t *state, + const policy_node_t *policy_node, + uint8_t flags) { ++state->node_stack_eos; if (state->node_stack_eos >= MAX_POLICY_DEPTH) { @@ -347,7 +349,7 @@ static int state_stack_push(policy_parser_state_t *state, * Pops a node from the stack. * Returns the emitted length on success, -1 on error. */ -static int state_stack_pop(policy_parser_state_t *state) { +__attribute__((warn_unused_result)) static int state_stack_pop(policy_parser_state_t *state) { policy_parser_node_state_t *node = &state->nodes[state->node_stack_eos]; if (state->node_stack_eos <= -1) { @@ -362,9 +364,8 @@ static int state_stack_pop(policy_parser_state_t *state) { return node->length; } -static inline int execute_processor(policy_parser_state_t *state, - policy_parser_processor_t proc, - const void *arg) { +__attribute__((warn_unused_result)) static inline int +execute_processor(policy_parser_state_t *state, policy_parser_processor_t proc, const void *arg) { int ret = proc(state, arg); // if the processor is done, pop the stack @@ -381,10 +382,11 @@ static inline int execute_processor(policy_parser_state_t *state, // convenience function, split from get_derived_pubkey only to improve stack usage // returns -1 on error, 0 if the returned key info has no wildcard (**), 1 if it has the wildcard -static int __attribute__((noinline)) get_extended_pubkey(dispatcher_context_t *dispatcher_context, - const wallet_derivation_info_t *wdi, - int key_index, - serialized_extended_pubkey_t *out) { +__attribute__((noinline, warn_unused_result)) static int get_extended_pubkey( + dispatcher_context_t *dispatcher_context, + const wallet_derivation_info_t *wdi, + int key_index, + serialized_extended_pubkey_t *out) { PRINT_STACK_POINTER(); policy_map_key_info_t key_info; @@ -409,28 +411,16 @@ static int __attribute__((noinline)) get_extended_pubkey(dispatcher_context_t *d return -1; } } - - // decode pubkey - serialized_extended_pubkey_check_t decoded_pubkey_check; - if (base58_decode(key_info.ext_pubkey, - strlen(key_info.ext_pubkey), - (uint8_t *) &decoded_pubkey_check, - sizeof(decoded_pubkey_check)) == -1) { - return -1; - } - // TODO: validate checksum - - memcpy(out, - &decoded_pubkey_check.serialized_extended_pubkey, - sizeof(decoded_pubkey_check.serialized_extended_pubkey)); + *out = key_info.ext_pubkey; return key_info.has_wildcard ? 1 : 0; } -static int get_derived_pubkey(dispatcher_context_t *dispatcher_context, - const wallet_derivation_info_t *wdi, - const policy_node_key_placeholder_t *key_placeholder, - uint8_t out[static 33]) { +__attribute__((warn_unused_result)) static int get_derived_pubkey( + dispatcher_context_t *dispatcher_context, + const wallet_derivation_info_t *wdi, + const policy_node_key_placeholder_t *key_placeholder, + uint8_t out[static 33]) { PRINT_STACK_POINTER(); serialized_extended_pubkey_t ext_pubkey; @@ -512,7 +502,8 @@ static void update_output_op_v(policy_parser_state_t *state, uint8_t op) { } } -static int process_generic_node(policy_parser_state_t *state, const void *arg) { +__attribute__((warn_unused_result)) static int process_generic_node(policy_parser_state_t *state, + const void *arg) { policy_parser_node_state_t *node = &state->nodes[state->node_stack_eos]; const generic_processor_command_t *commands = (const generic_processor_command_t *) arg; @@ -597,21 +588,29 @@ static int process_generic_node(policy_parser_state_t *state, const void *arg) { case CMD_CODE_PROCESS_CHILD: { const policy_node_with_scripts_t *policy = (const policy_node_with_scripts_t *) node->policy_node; - state_stack_push(state, resolve_node_ptr(&policy->scripts[cmd_data]), 0); + if (0 > state_stack_push(state, resolve_node_ptr(&policy->scripts[cmd_data]), 0)) { + return -1; + } break; } case CMD_CODE_PROCESS_CHILD_V: { const policy_node_with_scripts_t *policy = (const policy_node_with_scripts_t *) node->policy_node; - state_stack_push(state, resolve_node_ptr(&policy->scripts[cmd_data]), node->flags); + if (0 > state_stack_push(state, + resolve_node_ptr(&policy->scripts[cmd_data]), + node->flags)) { + return -1; + } break; } case CMD_CODE_PROCESS_CHILD_VV: { const policy_node_with_scripts_t *policy = (const policy_node_with_scripts_t *) node->policy_node; - state_stack_push(state, - resolve_node_ptr(&policy->scripts[cmd_data]), - node->flags | PROCESSOR_FLAG_V); + if (0 > state_stack_push(state, + resolve_node_ptr(&policy->scripts[cmd_data]), + node->flags | PROCESSOR_FLAG_V)) { + return -1; + } break; } default: @@ -623,7 +622,8 @@ static int process_generic_node(policy_parser_state_t *state, const void *arg) { } } -static int process_pkh_wpkh_node(policy_parser_state_t *state, const void *arg) { +__attribute__((warn_unused_result)) static int process_pkh_wpkh_node(policy_parser_state_t *state, + const void *arg) { UNUSED(arg); PRINT_STACK_POINTER(); @@ -666,7 +666,8 @@ static int process_pkh_wpkh_node(policy_parser_state_t *state, const void *arg) return 1; } -static int process_thresh_node(policy_parser_state_t *state, const void *arg) { +__attribute__((warn_unused_result)) static int process_thresh_node(policy_parser_state_t *state, + const void *arg) { UNUSED(arg); PRINT_STACK_POINTER(); @@ -674,11 +675,30 @@ static int process_thresh_node(policy_parser_state_t *state, const void *arg) { policy_parser_node_state_t *node = &state->nodes[state->node_stack_eos]; const policy_node_thresh_t *policy = (const policy_node_thresh_t *) node->policy_node; - // [X1] [X2] ADD ... [Xn] ADD ... EQUAL + // [X1] [X2] ADD ... [Xn] ADD EQUAL + + /* + It's a bit unnatural to encode thresh in a way that is compatible with our + stack-based encoder, as every "step" that needs to recur on a child Script + must emit the child script as its last thing. The natural way of splitting + this would be: + + [X1] / [X2] ADD / [X3] ADD / ... / [Xn] ADD / EQUAL + + Instead, we have to split it as follows: + + [X1] / [X2] / ADD [X3] / ... / ADD [Xn] / ADD EQUAL + + But this is incorrect if n == 1, because the correct encoding is just + + [X1] EQUAL + + Therefore, the case n == 1 needs to be handled separately to avoid the extra ADD. + */ // n+1 steps - // at step i, for 0 <= i < n, we produce [Xi] (or ADD X[i]) - // at step i, for i == n, we produce ADD EQUAL + // at step i, for 0 <= i < n, we produce [Xi] if i <= 1, or ADD [Xi] otherwise + // at step n, we produce EQUAL if n == 1, or ADD EQUAL otherwise if (node->step < policy->n) { // find the current child node @@ -699,7 +719,8 @@ static int process_thresh_node(policy_parser_state_t *state, const void *arg) { return 0; } else { // final step - if (policy->n >= 1) { + if (policy->n >= 2) { + // no OP_ADD if n == 1, per comment above update_output_u8(state, OP_ADD); } update_output_push_u32(state, policy->k); @@ -708,7 +729,9 @@ static int process_thresh_node(policy_parser_state_t *state, const void *arg) { } } -static int process_multi_sortedmulti_node(policy_parser_state_t *state, const void *arg) { +__attribute__((warn_unused_result)) static int process_multi_sortedmulti_node( + policy_parser_state_t *state, + const void *arg) { UNUSED(arg); PRINT_STACK_POINTER(); @@ -784,7 +807,9 @@ static int process_multi_sortedmulti_node(policy_parser_state_t *state, const vo return 1; } -static int process_multi_a_sortedmulti_a_node(policy_parser_state_t *state, const void *arg) { +__attribute__((warn_unused_result)) static int process_multi_a_sortedmulti_a_node( + policy_parser_state_t *state, + const void *arg) { UNUSED(arg); PRINT_STACK_POINTER(); @@ -855,10 +880,11 @@ static int process_multi_a_sortedmulti_a_node(policy_parser_state_t *state, cons return 1; } -static int __attribute__((noinline)) compute_tapleaf_hash(dispatcher_context_t *dispatcher_context, - const wallet_derivation_info_t *wdi, - const policy_node_t *script_policy, - uint8_t out[static 32]) { +__attribute__((warn_unused_result, noinline)) static int compute_tapleaf_hash( + dispatcher_context_t *dispatcher_context, + const wallet_derivation_info_t *wdi, + const policy_node_t *script_policy, + uint8_t out[static 32]) { cx_sha256_t hash_context; crypto_tr_tapleaf_hash_init(&hash_context); @@ -890,11 +916,11 @@ static int __attribute__((noinline)) compute_tapleaf_hash(dispatcher_context_t * } // Separated from compute_taptree_hash to optimize its stack usage -static int __attribute__((noinline)) -compute_and_combine_taptree_child_hashes(dispatcher_context_t *dc, - const wallet_derivation_info_t *wdi, - const policy_node_tree_t *tree, - uint8_t out[static 32]) { +__attribute__((warn_unused_result, noinline)) static int compute_and_combine_taptree_child_hashes( + dispatcher_context_t *dc, + const wallet_derivation_info_t *wdi, + const policy_node_tree_t *tree, + uint8_t out[static 32]) { uint8_t left_h[32], right_h[32]; if (0 > compute_taptree_hash(dc, wdi, resolve_ptr(&tree->left_tree), left_h)) return -1; if (0 > compute_taptree_hash(dc, wdi, resolve_ptr(&tree->right_tree), right_h)) return -1; @@ -903,7 +929,7 @@ compute_and_combine_taptree_child_hashes(dispatcher_context_t *dc, } // See taproot_tree_helper in BIP-0341 -int __attribute__((noinline)) compute_taptree_hash(dispatcher_context_t *dc, +__attribute__((noinline)) int compute_taptree_hash(dispatcher_context_t *dc, const wallet_derivation_info_t *wdi, const policy_node_tree_t *tree, uint8_t out[static 32]) { @@ -1043,7 +1069,9 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, int h_length = 0; if (tr_policy->tree != NULL) { - compute_taptree_hash(dispatcher_context, wdi, tr_policy->tree, h); + if (0 > compute_taptree_hash(dispatcher_context, wdi, tr_policy->tree, h)) { + return -1; + } h_length = 32; } @@ -1057,11 +1085,12 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, return -1; } -int get_wallet_internal_script_hash(dispatcher_context_t *dispatcher_context, - const policy_node_t *policy, - const wallet_derivation_info_t *wdi, - internal_script_type_e script_type, - cx_hash_t *hash_context) { +__attribute__((noinline)) int get_wallet_internal_script_hash( + dispatcher_context_t *dispatcher_context, + const policy_node_t *policy, + const wallet_derivation_info_t *wdi, + internal_script_type_e script_type, + cx_hash_t *hash_context) { const uint8_t *whitelist; size_t whitelist_len; switch (script_type) { @@ -1244,43 +1273,141 @@ int get_wallet_internal_script_hash(dispatcher_context_t *dispatcher_context, #pragma GCC diagnostic pop -int get_policy_address_type(const policy_node_t *policy) { - // legacy, native segwit, wrapped segwit, or taproot - switch (policy->type) { +// For a standard descriptor template, return the corresponding BIP44 purpose +// Otherwise, returns -1. +static int get_bip44_purpose(const policy_node_t *descriptor_template) { + const policy_node_key_placeholder_t *kp = NULL; + int purpose = -1; + switch (descriptor_template->type) { case TOKEN_PKH: - return ADDRESS_TYPE_LEGACY; + kp = ((const policy_node_with_key_t *) descriptor_template)->key_placeholder; + purpose = 44; // legacy + break; case TOKEN_WPKH: - return ADDRESS_TYPE_WIT; - case TOKEN_SH: - // wrapped segwit - if (resolve_node_ptr(&((const policy_node_with_script_t *) policy)->script)->type == - TOKEN_WPKH) { - return ADDRESS_TYPE_SH_WIT; + kp = ((const policy_node_with_key_t *) descriptor_template)->key_placeholder; + purpose = 84; // native segwit + break; + case TOKEN_SH: { + const policy_node_t *inner = resolve_node_ptr( + &((const policy_node_with_script_t *) descriptor_template)->script); + if (inner->type != TOKEN_WPKH) { + return -1; } - return -1; + + kp = ((const policy_node_with_key_t *) inner)->key_placeholder; + purpose = 49; // nested segwit + break; + } case TOKEN_TR: - return ADDRESS_TYPE_TR; + if (((const policy_node_tr_t *) descriptor_template)->tree != NULL) { + return -1; + } + + kp = ((const policy_node_tr_t *) descriptor_template)->key_placeholder; + purpose = 86; // standard single-key P2TR + break; default: return -1; } + + if (kp->key_index != 0 || kp->num_first != 0 || kp->num_second != 1) { + return -1; + } + + return purpose; +} + +bool is_wallet_policy_standard(dispatcher_context_t *dispatcher_context, + const policy_map_wallet_header_t *wallet_policy_header, + const policy_node_t *descriptor_template) { + // Based on the address type, we set the expected bip44 purpose + int bip44_purpose = get_bip44_purpose(descriptor_template); + if (bip44_purpose < 0) { + PRINTF("Non-standard policy, and no hmac provided\n"); + return false; + } + + if (wallet_policy_header->n_keys != 1) { + PRINTF("Standard wallets must have exactly 1 key\n"); + return false; + } + + // we check if the key is indeed internal + uint32_t master_key_fingerprint = crypto_get_master_key_fingerprint(); + + uint8_t key_info_str[MAX_POLICY_KEY_INFO_LEN]; + int key_info_len = call_get_merkle_leaf_element(dispatcher_context, + wallet_policy_header->keys_info_merkle_root, + wallet_policy_header->n_keys, + 0, // only one key + key_info_str, + sizeof(key_info_str)); + if (key_info_len < 0) { + return false; + } + + // Make a sub-buffer for the pubkey info + buffer_t key_info_buffer = buffer_create(key_info_str, key_info_len); + + policy_map_key_info_t key_info; + if (0 > parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_policy_header->version)) { + return false; + } + + if (!key_info.has_key_origin) { + return false; + } + + if (read_u32_be(key_info.master_key_fingerprint, 0) != master_key_fingerprint) { + return false; + } + + // generate pubkey and check if it matches + serialized_extended_pubkey_t derived_pubkey; + if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, + key_info.master_key_derivation_len, + BIP32_PUBKEY_VERSION, + &derived_pubkey)) { + PRINTF("Failed to derive pubkey\n"); + return false; + } + + if (memcmp(&key_info.ext_pubkey, &derived_pubkey, sizeof(derived_pubkey)) != 0) { + return false; + } + + // check if derivation path of the key is indeed standard + + // per BIP-0044, derivation must be + // m / purpose' / coin_type' / account' + + const uint32_t H = BIP32_FIRST_HARDENED_CHILD; + if (key_info.master_key_derivation_len != 3 || + key_info.master_key_derivation[0] != H + bip44_purpose || + key_info.master_key_derivation[1] != H + BIP44_COIN_TYPE || + key_info.master_key_derivation[2] < H || + key_info.master_key_derivation[2] > H + MAX_BIP44_ACCOUNT_RECOMMENDED) { + return false; + } + + return true; } bool compute_wallet_hmac(const uint8_t wallet_id[static 32], uint8_t wallet_hmac[static 32]) { uint8_t key[32]; bool result = false; - BEGIN_TRY { - TRY { - crypto_derive_symmetric_key(WALLET_SLIP0021_LABEL, WALLET_SLIP0021_LABEL_LEN, key); - cx_hmac_sha256(key, sizeof(key), wallet_id, 32, wallet_hmac, 32); - result = true; - } - FINALLY { - explicit_bzero(key, sizeof(key)); - } + if (!crypto_derive_symmetric_key(WALLET_SLIP0021_LABEL, WALLET_SLIP0021_LABEL_LEN, key)) { + goto end; } - END_TRY; + + cx_hmac_sha256(key, sizeof(key), wallet_id, 32, wallet_hmac, 32); + + result = true; + +end: + explicit_bzero(key, sizeof(key)); return result; } @@ -1290,22 +1417,20 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ uint8_t correct_hmac[32]; bool result = false; - BEGIN_TRY { - TRY { - crypto_derive_symmetric_key(WALLET_SLIP0021_LABEL, WALLET_SLIP0021_LABEL_LEN, key); - - cx_hmac_sha256(key, sizeof(key), wallet_id, 32, correct_hmac, 32); - // It is important to use a constant-time function to compare the hmac, - // to avoid timing-attack that could be exploited to extract it. - result = os_secure_memcmp((void *) wallet_hmac, (void *) correct_hmac, 32) == 0; - } - FINALLY { - explicit_bzero(key, sizeof(key)); - explicit_bzero(correct_hmac, sizeof(correct_hmac)); - } + if (!crypto_derive_symmetric_key(WALLET_SLIP0021_LABEL, WALLET_SLIP0021_LABEL_LEN, key)) { + goto end; } - END_TRY; + + cx_hmac_sha256(key, sizeof(key), wallet_id, 32, correct_hmac, 32); + + // It is important to use a constant-time function to compare the hmac, + // to avoid timing-attack that could be exploited to extract it. + result = os_secure_memcmp((void *) wallet_hmac, (void *) correct_hmac, 32) == 0; + +end: + explicit_bzero(key, sizeof(key)); + explicit_bzero(correct_hmac, sizeof(correct_hmac)); return result; } @@ -1520,13 +1645,28 @@ int get_key_placeholder_by_index(const policy_node_t *policy, return -1; } -// Utility function to extract the i-th xpub from the keys information vector -static int get_xpub_from_merkle_tree(dispatcher_context_t *dispatcher_context, - int wallet_version, - const uint8_t keys_merkle_root[static 32], - uint32_t n_keys, - uint32_t index, - char out[static MAX_SERIALIZED_PUBKEY_LENGTH + 1]) { +int count_distinct_keys_info(const policy_node_t *policy) { + policy_node_key_placeholder_t placeholder; + int ret = -1, cur, n_placeholders; + + for (cur = 0; + cur < (n_placeholders = get_key_placeholder_by_index(policy, cur, NULL, &placeholder)); + ++cur) { + if (n_placeholders < 0) { + return -1; + } + ret = MAX(ret, placeholder.key_index + 1); + } + return ret; +} + +// Utility function to extract and decode the i-th xpub from the keys information vector +static int get_pubkey_from_merkle_tree(dispatcher_context_t *dispatcher_context, + int wallet_version, + const uint8_t keys_merkle_root[static 32], + uint32_t n_keys, + uint32_t index, + serialized_extended_pubkey_t *out) { char key_info_str[MAX_POLICY_KEY_INFO_LEN]; int key_info_len = call_get_merkle_leaf_element(dispatcher_context, keys_merkle_root, @@ -1545,7 +1685,7 @@ static int get_xpub_from_merkle_tree(dispatcher_context_t *dispatcher_context, if (parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_version) == -1) { return WITH_ERROR(-1, "Failed to parse key information"); } - strncpy(out, key_info.ext_pubkey, MAX_SERIALIZED_PUBKEY_LENGTH + 1); + *out = key_info.ext_pubkey; return 0; } @@ -1609,28 +1749,33 @@ int is_policy_sane(dispatcher_context_t *dispatcher_context, // check that all the xpubs are different for (unsigned int i = 0; i < n_keys - 1; i++) { // no point in running this for the last key - char xpub_i[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; - if (0 > get_xpub_from_merkle_tree(dispatcher_context, - wallet_version, - keys_merkle_root, - n_keys, - i, - xpub_i)) { + serialized_extended_pubkey_t pubkey_i; + if (0 > get_pubkey_from_merkle_tree(dispatcher_context, + wallet_version, + keys_merkle_root, + n_keys, + i, + &pubkey_i)) { return -1; } for (unsigned int j = i + 1; j < n_keys; j++) { - char xpub_j[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; - if (0 > get_xpub_from_merkle_tree(dispatcher_context, - wallet_version, - keys_merkle_root, - n_keys, - j, - xpub_j)) { + serialized_extended_pubkey_t pubkey_j; + if (0 > get_pubkey_from_merkle_tree(dispatcher_context, + wallet_version, + keys_merkle_root, + n_keys, + j, + &pubkey_j)) { return -1; } - if (strncmp(xpub_i, xpub_j, sizeof(xpub_i)) == 0) { + // We reject if any two xpubs have the same pubkey + // Conservatively, we only compare the compressed pubkey, rather than the whole xpub: + // there is no good reason for allowing two different xpubs with the same pubkey. + if (memcmp(pubkey_i.compressed_pubkey, + pubkey_j.compressed_pubkey, + sizeof(pubkey_i.compressed_pubkey)) == 0) { // duplicated pubkey return WITH_ERROR(-1, "Repeated pubkey in wallet policy"); } diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 9ffe880d..d2c9dcd9 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -22,7 +22,7 @@ * @return 0 on success, a negative number in case of error. */ // TODO: we should distinguish actual errors from just "policy too big to fit in memory" -int read_and_parse_wallet_policy( +__attribute__((warn_unused_result)) int read_and_parse_wallet_policy( dispatcher_context_t *dispatcher_context, buffer_t *buf, policy_map_wallet_header_t *wallet_header, @@ -65,10 +65,11 @@ typedef struct { * * @return 0 on success, a negative number on failure. */ -int compute_taptree_hash(dispatcher_context_t *dispatcher_context, - const wallet_derivation_info_t *wdi, - const policy_node_tree_t *tree, - uint8_t out[static 32]); +__attribute__((warn_unused_result)) int compute_taptree_hash( + dispatcher_context_t *dispatcher_context, + const wallet_derivation_info_t *wdi, + const policy_node_tree_t *tree, + uint8_t out[static 32]); /** * Computes the script corresponding to a wallet policy, for a certain change and address index. @@ -86,10 +87,10 @@ int compute_taptree_hash(dispatcher_context_t *dispatcher_context, * @return The length of the output on success; -1 in case of error. * */ -int get_wallet_script(dispatcher_context_t *dispatcher_context, - const policy_node_t *policy, - const wallet_derivation_info_t *wdi, - uint8_t out[static 34]); +__attribute__((warn_unused_result)) int get_wallet_script(dispatcher_context_t *dispatcher_context, + const policy_node_t *policy, + const wallet_derivation_info_t *wdi, + uint8_t out[static 34]); /** * Computes the script corresponding to a wallet policy, for a certain change and address index. @@ -107,11 +108,12 @@ int get_wallet_script(dispatcher_context_t *dispatcher_context, * @return the length of the script on success; a negative number in case of error. * */ -int get_wallet_internal_script_hash(dispatcher_context_t *dispatcher_context, - const policy_node_t *policy, - const wallet_derivation_info_t *wdi, - internal_script_type_e script_type, - cx_hash_t *hash_context); +__attribute__((warn_unused_result)) int get_wallet_internal_script_hash( + dispatcher_context_t *dispatcher_context, + const policy_node_t *policy, + const wallet_derivation_info_t *wdi, + internal_script_type_e script_type, + cx_hash_t *hash_context); /** * Returns the address type constant corresponding to a standard policy type. @@ -124,6 +126,30 @@ int get_wallet_internal_script_hash(dispatcher_context_t *dispatcher_context, */ int get_policy_address_type(const policy_node_t *policy); +/** + * Returns true if the descriptor template is a standard one. + * Standard wallet policies are single-signature policies as per the following standards: + * - BIP-44 (legacy, P2PKH) + * - BIP-84 (native segwit, P2WPKH) + * - BIP-49 (wrapped segwit, P2SH-P2WPKH) + * - BIP-86 (standard single key P2TR) + * with the standard derivations for the key placeholders, and unhardened steps for the + * change / address_index steps (using 0 for non-change, 1 for change addresses). + * + * @param[in] dispatcher_context + * Pointer to the dispatcher context + * @param[in] wallet_policy_header + * Pointer the wallet policy header + * @param[in] descriptor_template + * Pointer to the root node of the policy + * + * @return true if the descriptor_template is not standard; false if not, or in case of error. + */ +__attribute__((warn_unused_result)) bool is_wallet_policy_standard( + dispatcher_context_t *dispatcher_context, + const policy_map_wallet_header_t *wallet_policy_header, + const policy_node_t *descriptor_template); + /** * Computes and returns the wallet_hmac, using the symmetric key derived * with the WALLET_SLIP0021_LABEL label according to SLIP-0021. @@ -163,10 +189,23 @@ bool check_wallet_hmac(const uint8_t wallet_id[static 32], const uint8_t wallet_ * If not NULL, it is a pointer that will receive the i-th placeholder of the policy. * @return the number of placeholders in the policy on success; -1 in case of error. */ -int get_key_placeholder_by_index(const policy_node_t *policy, - unsigned int i, - const policy_node_t **out_tapleaf_ptr, - policy_node_key_placeholder_t *out_placeholder); +__attribute__((warn_unused_result)) int get_key_placeholder_by_index( + const policy_node_t *policy, + unsigned int i, + const policy_node_t **out_tapleaf_ptr, + policy_node_key_placeholder_t *out_placeholder); + +/** + * Determines the expected number of unique keys in the provided policy's key information. + * The function calculates this by finding the maximum key index from placeholders and increments it + * by 1. For instance, if the maximum key index found in the placeholders is `n`, then the result + * would be `n + 1`. + * + * @param[in] policy + * Pointer to the root node of the policy + * @return the expected number of items in the keys information vector; -1 in case of error. + */ +__attribute__((warn_unused_result)) int count_distinct_keys_info(const policy_node_t *policy); /** * Checks if a wallet policy is sane, verifying that pubkeys are never repeated and (if miniscript) @@ -183,8 +222,8 @@ int get_key_placeholder_by_index(const policy_node_t *policy, * The number of keys in the vector of keys * @return 0 on success; -1 in case of error. */ -int is_policy_sane(dispatcher_context_t *dispatcher_context, - const policy_node_t *policy, - int wallet_version, - const uint8_t keys_merkle_root[static 32], - uint32_t n_keys); \ No newline at end of file +__attribute__((warn_unused_result)) int is_policy_sane(dispatcher_context_t *dispatcher_context, + const policy_node_t *policy, + int wallet_version, + const uint8_t keys_merkle_root[static 32], + uint32_t n_keys); \ No newline at end of file diff --git a/src/handler/lib/stream_preimage.c b/src/handler/lib/stream_preimage.c index bea9a108..cd6a0c79 100644 --- a/src/handler/lib/stream_preimage.c +++ b/src/handler/lib/stream_preimage.c @@ -90,8 +90,7 @@ int call_stream_preimage(dispatcher_context_t *dispatcher_context, return -8; } - uint8_t *data_ptr = - dispatcher_context->read_buffer.ptr + dispatcher_context->read_buffer.offset; + data_ptr = dispatcher_context->read_buffer.ptr + dispatcher_context->read_buffer.offset; // update hash crypto_hash_update(&hash_context.header, data_ptr, n_bytes); diff --git a/src/handler/register_wallet.c b/src/handler/register_wallet.c index 3e8785da..be793c7a 100644 --- a/src/handler/register_wallet.c +++ b/src/handler/register_wallet.c @@ -50,8 +50,8 @@ static bool is_policy_name_acceptable(const char *name, size_t name_len); * Validates the input, initializes the hash context and starts accumulating the wallet header in * it. */ -void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { - (void) p2; +void handler_register_wallet(dispatcher_context_t *dc, uint8_t protocol_version) { + (void) protocol_version; LOG_PROCESSOR(__FILE__, __LINE__, __func__); @@ -88,11 +88,18 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { return; } + if (count_distinct_keys_info(&policy_map.parsed) != (int) wallet_header.n_keys) { + PRINTF("Number of keys in descriptor template doesn't provided keys\n"); + SEND_SW(dc, SW_INCORRECT_DATA); + return; + } + // Compute the wallet id (sha256 of the serialization) get_policy_wallet_id(&wallet_header, wallet_id); // Verify that the name is acceptable if (!is_policy_name_acceptable(wallet_header.name, wallet_header.name_len)) { + PRINTF("Policy name is not acceptable\n"); SEND_SW(dc, SW_INCORRECT_DATA); return; } @@ -119,6 +126,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { if (!ui_display_register_wallet(dc, &wallet_header, (char *) policy_map_descriptor)) { SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_wallet_registration(dc, false); return; } @@ -140,6 +148,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { if (pubkey_info_len < 0) { SEND_SW(dc, SW_INCORRECT_DATA); + ui_post_processing_confirm_wallet_registration(dc, false); return; } @@ -152,6 +161,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { if (parse_policy_map_key_info(&key_info_buffer, &key_info, wallet_header.version) == -1) { PRINTF("Incorrect policy map.\n"); SEND_SW(dc, SW_INCORRECT_DATA); + ui_post_processing_confirm_wallet_registration(dc, false); return; } @@ -167,19 +177,19 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { if (key_info.has_key_origin && read_u32_be(key_info.master_key_fingerprint, 0) == master_key_fingerprint) { // we verify that we can actually generate the same pubkey - char pubkey_derived[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; + serialized_extended_pubkey_t pubkey_derived; int serialized_pubkey_len = - get_serialized_extended_pubkey_at_path(key_info.master_key_derivation, - key_info.master_key_derivation_len, - BIP32_PUBKEY_VERSION, - pubkey_derived, - NULL); + get_extended_pubkey_at_path(key_info.master_key_derivation, + key_info.master_key_derivation_len, + BIP32_PUBKEY_VERSION, + &pubkey_derived); if (serialized_pubkey_len == -1) { SEND_SW(dc, SW_BAD_STATE); + ui_post_processing_confirm_wallet_registration(dc, false); return; } - if (strncmp(key_info.ext_pubkey, pubkey_derived, MAX_SERIALIZED_PUBKEY_LENGTH) == 0) { + if (memcmp(&key_info.ext_pubkey, &pubkey_derived, sizeof(pubkey_derived)) == 0) { is_key_internal = true; ++n_internal_keys; } @@ -204,11 +214,13 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { // We disallow that, might reconsider in future versions if needed. PRINTF("Wallet policy with no internal keys\n"); SEND_SW(dc, SW_INCORRECT_DATA); + ui_post_processing_confirm_wallet_registration(dc, false); return; } else if (n_internal_keys != 1 && wallet_header.version == WALLET_POLICY_VERSION_V1) { // for legacy policies, we keep the restriction to exactly 1 internal key PRINTF("V1 policies must have exactly 1 internal key\n"); SEND_SW(dc, SW_INCORRECT_DATA); + ui_post_processing_confirm_wallet_registration(dc, false); return; } @@ -231,6 +243,7 @@ void handler_register_wallet(dispatcher_context_t *dc, uint8_t p2) { compute_wallet_hmac(wallet_id, response.hmac); SEND_RESPONSE(dc, &response, sizeof(response), SW_OK); + ui_post_processing_confirm_wallet_registration(dc, true); } static bool is_policy_acceptable(const policy_node_t *policy) { diff --git a/src/handler/sign_message.c b/src/handler/sign_message.c index 3ef5087f..f42e2f39 100644 --- a/src/handler/sign_message.c +++ b/src/handler/sign_message.c @@ -38,8 +38,8 @@ static unsigned char const SYSM_SIGN_MAGIC[] = {'\x18', 'S', 'y', 's', 'c', 'o', 'S', 'i', 'g', 'n', 'e', 'd', ' ', 'M', 'e', 's', 's', 'a', 'g', 'e', ':', '\n'}; -void handler_sign_message(dispatcher_context_t *dc, uint8_t p2) { - (void) p2; +void handler_sign_message(dispatcher_context_t *dc, uint8_t protocol_version) { + (void) protocol_version; uint8_t bip32_path_len; uint32_t bip32_path[MAX_BIP32_PATH_STEPS]; @@ -114,6 +114,7 @@ void handler_sign_message(dispatcher_context_t *dc, uint8_t p2) { if (!ui_display_message_hash(dc, path_str, message_hash_str)) { SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_message(dc, false); return; } @@ -129,6 +130,7 @@ void handler_sign_message(dispatcher_context_t *dc, uint8_t p2) { if (sig_len < 0) { // unexpected error when signing SEND_SW(dc, SW_BAD_STATE); + ui_post_processing_confirm_message(dc, false); return; } @@ -144,6 +146,7 @@ void handler_sign_message(dispatcher_context_t *dc, uint8_t p2) { if (r_length > 33 || s_length > 33) { SEND_SW(dc, SW_BAD_STATE); // can never happen + ui_post_processing_confirm_message(dc, false); return; } @@ -159,5 +162,7 @@ void handler_sign_message(dispatcher_context_t *dc, uint8_t p2) { result[0] = 27 + 4 + ((info & CX_ECCINFO_PARITY_ODD) ? 1 : 0); SEND_RESPONSE(dc, result, sizeof(result), SW_OK); + ui_post_processing_confirm_message(dc, true); + return; } } diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 0cd05a51..e7b79ed0 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -17,6 +17,8 @@ #include +#include "lib_standard_app/crypto_helpers.h" + #include "../boilerplate/dispatcher.h" #include "../boilerplate/sw.h" #include "../common/bitvector.h" @@ -130,16 +132,19 @@ typedef struct { unsigned int n_outputs; uint8_t outputs_root[32]; // merkle root of the vector of output maps commitments - uint64_t inputs_total_value; - uint64_t outputs_total_value; - - uint64_t internal_inputs_total_value; + uint64_t inputs_total_amount; - uint64_t change_outputs_total_value; + // aggregate info on outputs + struct { + uint64_t total_amount; // amount of all the outputs (external + change) + uint64_t change_total_amount; // total amount of all change outputs + int n_change; // count of outputs compatible with change outputs + int n_external; // count of external outputs + } outputs; - bool is_wallet_canonical; + bool is_wallet_default; - uint8_t p2; + uint8_t protocol_version; union { uint8_t wallet_policy_map_bytes[MAX_WALLET_POLICY_BYTES]; @@ -155,9 +160,6 @@ typedef struct { // if any of the internal inputs has non-default sighash, we show a warning bool show_nondefault_sighash_warning; - - int external_outputs_count; // count of external outputs that are shown to the user - int change_count; // count of outputs compatible with change outputs } sign_psbt_state_t; /* BIP0341 tags for computing the tagged hashes when computing he sighash */ @@ -237,20 +239,6 @@ static int hash_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st, cx_hash return 0; } -static int get_segwit_version(const uint8_t scriptPubKey[], int scriptPubKey_len) { - if (scriptPubKey_len <= 1) { - return -1; - } - - if (scriptPubKey[0] == 0x00) { - return 0; - } else if (scriptPubKey[0] >= 0x51 && scriptPubKey[0] <= 0x60) { - return scriptPubKey[0] - 0x50; - } - - return -1; -} - /* Convenience function to get the amount and scriptpubkey from the non-witness-utxo of a certain input in a PSBTv2. @@ -602,9 +590,9 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { return false; } - st->is_wallet_canonical = false; + st->is_wallet_default = false; } else { - st->is_wallet_canonical = true; + st->is_wallet_default = true; } { @@ -639,87 +627,44 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { sizeof(wallet_header.keys_info_merkle_root)); st->wallet_header_n_keys = wallet_header.n_keys; - if (st->is_wallet_canonical) { - // verify that the policy is indeed a canonical one that is allowed by default - - if (st->wallet_header_n_keys != 1) { - PRINTF("Non-standard policy, it should only have 1 key\n"); - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } - - int address_type = get_policy_address_type(&st->wallet_policy_map); - if (address_type == -1) { + if (st->is_wallet_default) { + // No hmac, verify that the policy is indeed a default one + if (!is_wallet_policy_standard(dc, &wallet_header, &st->wallet_policy_map)) { PRINTF("Non-standard policy, and no hmac provided\n"); SEND_SW(dc, SW_INCORRECT_DATA); return false; } - // Based on the address type, we set the expected bip44 purpose for this canonical - // wallet - int bip44_purpose = get_bip44_purpose(address_type); - if (bip44_purpose < 0) { - SEND_SW(dc, SW_BAD_STATE); - return false; - } - - // We check that the pubkey has indeed 3 derivation steps, and it follows bip44 - // standards We skip checking that we can indeed deriva the same pubkey (no security - // risk here, as the xpub itself isn't really used for the canonical wallet policies). - policy_map_key_info_t key_info; - { - char key_info_str[MAX_POLICY_KEY_INFO_LEN]; - - int key_info_len = - call_get_merkle_leaf_element(dc, - st->wallet_header_keys_info_merkle_root, - st->wallet_header_n_keys, - 0, - (uint8_t *) key_info_str, - sizeof(key_info_str)); - if (key_info_len == -1) { - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } - - buffer_t key_info_buffer = buffer_create(key_info_str, key_info_len); - - if (parse_policy_map_key_info(&key_info_buffer, - &key_info, - st->wallet_header_version) == -1) { - SEND_SW(dc, SW_INCORRECT_DATA); - return false; - } - } - - uint32_t coin_types[2] = {BIP44_COIN_TYPE, BIP44_COIN_TYPE_2}; - if (key_info.master_key_derivation_len != 3 || - !is_pubkey_path_standard(key_info.master_key_derivation, - key_info.master_key_derivation_len, - bip44_purpose, - coin_types, - 2)) { + if (wallet_header.name_len != 0) { + PRINTF("Name must be zero-length for a standard wallet policy\n"); SEND_SW(dc, SW_INCORRECT_DATA); return false; } + + // unlike in get_wallet_address, we do not check if the address_index is small: + // if funds were already sent there, there is no point in preventing to spend them. } } - // Swap feature: check that wallet is canonical - if (G_swap_state.called_from_swap && !st->is_wallet_canonical) { - PRINTF("Must be a canonical wallet for swap feature\n"); + // Swap feature: check that wallet policy is a default one + if (G_swap_state.called_from_swap && !st->is_wallet_default) { + PRINTF("Must be a default wallet policy for swap feature\n"); SEND_SW(dc, SW_INCORRECT_DATA); return false; } - // If it's not a canonical wallet, ask the user for confirmation, and abort if they deny - if (!st->is_wallet_canonical && !ui_authorize_wallet_spend(dc, wallet_header.name)) { + // If it's not a default wallet policy, ask the user for confirmation, and abort if they deny + if (!st->is_wallet_default && !ui_authorize_wallet_spend(dc, wallet_header.name)) { SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_wallet_spend(dc, false); return false; } st->master_key_fingerprint = crypto_get_master_key_fingerprint(); + if (!st->is_wallet_default) { + ui_post_processing_confirm_wallet_spend(dc, true); + } return true; } @@ -760,19 +705,17 @@ fill_placeholder_info_if_internal(dispatcher_context_t *dc, { // it could be a collision on the fingerprint; we verify that we can actually generate // the same pubkey - char pubkey_derived[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; - int serialized_pubkey_len = - get_serialized_extended_pubkey_at_path(key_info.master_key_derivation, - key_info.master_key_derivation_len, - BIP32_PUBKEY_VERSION, - pubkey_derived, - &placeholder_info->pubkey); - if (serialized_pubkey_len == -1) { + if (0 > get_extended_pubkey_at_path(key_info.master_key_derivation, + key_info.master_key_derivation_len, + BIP32_PUBKEY_VERSION, + &placeholder_info->pubkey)) { SEND_SW(dc, SW_BAD_STATE); return false; } - if (strncmp(key_info.ext_pubkey, pubkey_derived, MAX_SERIALIZED_PUBKEY_LENGTH) != 0) { + if (memcmp(&key_info.ext_pubkey, + &placeholder_info->pubkey, + sizeof(placeholder_info->pubkey)) != 0) { return false; } @@ -940,7 +883,7 @@ preprocess_inputs(dispatcher_context_t *dc, return false; } - st->inputs_total_value += input.prevout_amount; + st->inputs_total_amount += input.prevout_amount; } if (input.has_witnessUtxo) { @@ -972,7 +915,7 @@ preprocess_inputs(dispatcher_context_t *dc, } } else { // we extract the scriptPubKey and prevout amount from the witness utxo - st->inputs_total_value += wit_utxo_prevout_amount; + st->inputs_total_amount += wit_utxo_prevout_amount; input.prevout_amount = wit_utxo_prevout_amount; input.in_out.scriptPubKey_len = wit_utxo_scriptPubkey_len; @@ -993,10 +936,8 @@ preprocess_inputs(dispatcher_context_t *dc, } bitvector_set(internal_inputs, cur_input_index, 1); - st->internal_inputs_total_value += input.prevout_amount; - int segwit_version = - get_segwit_version(input.in_out.scriptPubKey, input.in_out.scriptPubKey_len); + int segwit_version = get_policy_segwit_version(&st->wallet_policy_map); // For legacy inputs, the non-witness utxo must be present if (segwit_version == -1 && !input.has_nonWitnessUtxo) { @@ -1157,6 +1098,7 @@ static void output_keys_callback(dispatcher_context_t *dc, static bool __attribute__((noinline)) display_output(dispatcher_context_t *dc, sign_psbt_state_t *st, int cur_output_index, + int external_outputs_count, const output_info_t *output) { (void) cur_output_index; @@ -1199,7 +1141,8 @@ static bool __attribute__((noinline)) display_output(dispatcher_context_t *dc, } else { // Show address to the user if (!ui_validate_output(dc, - st->external_outputs_count, + external_outputs_count, + st->outputs.n_external, output_address, COIN_COINID_SHORT, output->value)) { @@ -1210,27 +1153,20 @@ static bool __attribute__((noinline)) display_output(dispatcher_context_t *dc, return true; } -static bool __attribute__((noinline)) -process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { - /** OUTPUTS VERIFICATION FLOW - * - * For each output, check if it's a change address. - * Show each output that is not a change address to the user for verification. - */ - - LOG_PROCESSOR(__FILE__, __LINE__, __func__); - - placeholder_info_t placeholder_info; - memset(&placeholder_info, 0, sizeof(placeholder_info)); - - if (!find_first_internal_key_placeholder(dc, st, &placeholder_info)) return false; +static bool read_outputs(dispatcher_context_t *dc, + sign_psbt_state_t *st, + placeholder_info_t *placeholder_info, + bool dry_run) { + // the counter used when showing outputs to the user, which ignores change outputs + // (0-indexed here, although the UX starts with 1) + int external_outputs_count = 0; for (unsigned int cur_output_index = 0; cur_output_index < st->n_outputs; cur_output_index++) { output_info_t output; memset(&output, 0, sizeof(output)); output_keys_callback_data_t callback_data = {.output = &output, - .placeholder_info = &placeholder_info}; + .placeholder_info = placeholder_info}; int res = call_get_merkleized_map_with_callback( dc, (void *) &callback_data, @@ -1239,6 +1175,7 @@ process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { cur_output_index, (merkle_tree_elements_callback_t) output_keys_callback, &output.in_out.map); + if (res < 0) { SEND_SW(dc, SW_INCORRECT_DATA); return false; @@ -1250,33 +1187,34 @@ process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { return false; } - // read output amount and scriptpubkey + if (!dry_run) { + // Read output amount + uint8_t raw_result[8]; - uint8_t raw_result[8]; + // Read the output's amount + int result_len = call_get_merkleized_map_value(dc, + &output.in_out.map, + (uint8_t[]){PSBT_OUT_AMOUNT}, + 1, + raw_result, + sizeof(raw_result)); + if (result_len != 8) { + SEND_SW(dc, SW_INCORRECT_DATA); + return false; + } + uint64_t value = read_u64_le(raw_result, 0); - // Read the output's amount - int result_len = call_get_merkleized_map_value(dc, - &output.in_out.map, - (uint8_t[]){PSBT_OUT_AMOUNT}, - 1, - raw_result, - sizeof(raw_result)); - if (result_len != 8) { - SEND_SW(dc, SW_INCORRECT_DATA); - return false; + output.value = value; + st->outputs.total_amount += value; } - uint64_t value = read_u64_le(raw_result, 0); - - output.value = value; - st->outputs_total_value += value; // Read the output's scriptPubKey - result_len = call_get_merkleized_map_value(dc, - &output.in_out.map, - (uint8_t[]){PSBT_OUT_SCRIPT}, - 1, - output.in_out.scriptPubKey, - sizeof(output.in_out.scriptPubKey)); + int result_len = call_get_merkleized_map_value(dc, + &output.in_out.map, + (uint8_t[]){PSBT_OUT_SCRIPT}, + 1, + output.in_out.scriptPubKey, + sizeof(output.in_out.scriptPubKey)); if (result_len == -1 || result_len > (int) sizeof(output.in_out.scriptPubKey)) { SEND_SW(dc, SW_INCORRECT_DATA); @@ -1293,18 +1231,57 @@ process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { return false; } else if (is_internal == 0) { // external output, user needs to validate - ++st->external_outputs_count; - - if (!display_output(dc, st, cur_output_index, &output)) return false; + ++external_outputs_count; - } else { + if (!dry_run && + !display_output(dc, st, cur_output_index, external_outputs_count, &output)) + return false; + } else if (!dry_run) { // valid change address, nothing to show to the user - st->change_outputs_total_value += output.value; - ++st->change_count; + st->outputs.change_total_amount += output.value; + ++st->outputs.n_change; } } + st->outputs.n_external = external_outputs_count; + + return true; +} + +static bool __attribute__((noinline)) +process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { + /** OUTPUTS VERIFICATION FLOW + * + * For each output, check if it's a change address. + * Show each output that is not a change address to the user for verification. + */ + + LOG_PROCESSOR(__FILE__, __LINE__, __func__); + + placeholder_info_t placeholder_info; + memset(&placeholder_info, 0, sizeof(placeholder_info)); + + if (!find_first_internal_key_placeholder(dc, st, &placeholder_info)) return false; + + memset(&st->outputs, 0, sizeof(st->outputs)); + +#ifdef HAVE_NBGL + // Only on Stax, we need to preprocess all the outputs in order to + // compute the total number of non-change outputs. + // As it's a time-consuming operation, we use avoid doing this useless + // work on other models. + + if (!read_outputs(dc, st, &placeholder_info, true)) return false; + + if (!G_swap_state.called_from_swap && !ui_transaction_prompt(dc, st->outputs.n_external)) { + SEND_SW(dc, SW_DENY); + return false; + } +#endif + + if (!read_outputs(dc, st, &placeholder_info, false)) return false; + return true; } @@ -1312,39 +1289,39 @@ static bool __attribute__((noinline)) confirm_transaction(dispatcher_context_t *dc, sign_psbt_state_t *st) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); - if (st->inputs_total_value < st->outputs_total_value) { + if (st->inputs_total_amount < st->outputs.total_amount) { PRINTF("Negative fee is invalid\n"); // negative fee transaction is invalid SEND_SW(dc, SW_INCORRECT_DATA); return false; } - if (st->change_count > 10) { + if (st->outputs.n_change > 10) { // As the information regarding change outputs is aggregated, we want to prevent the user // from unknowingly signing a transaction that sends the change to too many (possibly // unspendable) outputs. - PRINTF("Too many change outputs: %d\n", st->change_count); + PRINTF("Too many change outputs: %d\n", st->outputs.n_change); SEND_SW(dc, SW_NOT_SUPPORTED); return false; } - uint64_t fee = st->inputs_total_value - st->outputs_total_value; + uint64_t fee = st->inputs_total_amount - st->outputs.total_amount; if (G_swap_state.called_from_swap) { - // Swap feature: check total amount and fees are as expected; moreover, only one external - // output - if (st->external_outputs_count != 1) { + // Swap feature: there must be only one external output + if (st->outputs.n_external != 1) { PRINTF("Swap transaction must have exactly 1 external output\n"); SEND_SW(dc, SW_INCORRECT_DATA); return false; } + // Swap feature: check total amount and fees are as expected if (fee != G_swap_state.fees) { PRINTF("Mismatching fee for swap\n"); SEND_SW(dc, SW_INCORRECT_DATA); return false; } - uint64_t spent_amount = st->outputs_total_value - st->change_outputs_total_value; + uint64_t spent_amount = st->outputs.total_amount - st->outputs.change_total_amount; if (spent_amount != G_swap_state.amount) { PRINTF("Mismatching spent amount for swap\n"); SEND_SW(dc, SW_INCORRECT_DATA); @@ -1352,10 +1329,12 @@ confirm_transaction(dispatcher_context_t *dc, sign_psbt_state_t *st) { } } else { // Show final user validation UI - if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee)) { + bool is_self_transfer = st->outputs.n_external == 0; + if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee, is_self_transfer)) { SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_transaction(dc, false); return false; - }; + } } return true; @@ -1829,7 +1808,7 @@ static bool __attribute__((noinline)) yield_signature(dispatcher_context_t *dc, uint8_t augm_pubkey_len = pubkey_len + (tapleaf_hash != NULL ? 32 : 0); // the pubkey is not output in version 0 of the protocol - if (st->p2 >= 1) { + if (st->protocol_version >= 1) { dc->add_to_response(&augm_pubkey_len, 1); dc->add_to_response(pubkey, pubkey_len); @@ -1938,7 +1917,11 @@ sign_sighash_schnorr_and_yield(dispatcher_context_t *dc, int sign_path_len = placeholder_info->key_derivation_length + 2; - if (0 > crypto_derive_private_key(&private_key, NULL, sign_path, sign_path_len)) { + if (bip32_derive_init_privkey_256(CX_CURVE_256K1, + sign_path, + sign_path_len, + &private_key, + NULL) != CX_OK) { error = true; break; } @@ -2198,8 +2181,6 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ sighash)) return false; } else { - int segwit_version; - { uint64_t amount; if (0 > get_amount_scriptpubkey_from_psbt_witness(dc, @@ -2245,22 +2226,13 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ input->script_len = redeemScript_length; memcpy(input->script, redeemScript, redeemScript_length); - segwit_version = get_segwit_version(redeemScript, redeemScript_length); } else { input->script_len = input->in_out.scriptPubKey_len; memcpy(input->script, input->in_out.scriptPubKey, input->in_out.scriptPubKey_len); - - segwit_version = - get_segwit_version(input->in_out.scriptPubKey, input->in_out.scriptPubKey_len); - } - - if (segwit_version > 1) { - PRINTF("Segwit version not supported: %d\n", segwit_version); - SEND_SW(dc, SW_NOT_SUPPORTED); - return false; } } + int segwit_version = get_policy_segwit_version(&st->wallet_policy_map); uint8_t sighash[32]; if (segwit_version == 0) { if (!input->has_sighash_type) { @@ -2308,6 +2280,7 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_ policy->tree, input->taptree_hash)) { PRINTF("Error while computing taptree hash\n"); + SEND_SW(dc, SW_BAD_STATE); return false; } } @@ -2408,6 +2381,9 @@ sign_transaction(dispatcher_context_t *dc, if (n_key_placeholders < 0) { SEND_SW(dc, SW_BAD_STATE); // should never happen + if (!G_swap_state.called_from_swap) { + ui_post_processing_confirm_transaction(dc, false); + } return false; } @@ -2441,6 +2417,9 @@ sign_transaction(dispatcher_context_t *dc, &input.in_out.map); if (res < 0) { SEND_SW(dc, SW_INCORRECT_DATA); + if (!G_swap_state.called_from_swap) { + ui_post_processing_confirm_transaction(dc, false); + } return false; } @@ -2451,18 +2430,26 @@ sign_transaction(dispatcher_context_t *dc, &placeholder_info)) return false; - if (!sign_transaction_input(dc, st, &hashes, &placeholder_info, &input, i)) + if (!sign_transaction_input(dc, st, &hashes, &placeholder_info, &input, i)) { + SEND_SW(dc, SW_BAD_STATE); // should never happen + if (!G_swap_state.called_from_swap) { + ui_post_processing_confirm_transaction(dc, false); + } return false; + } } } ++placeholder_index; } + if (!G_swap_state.called_from_swap) { + ui_post_processing_confirm_transaction(dc, true); + } return true; } -void handler_sign_psbt(dispatcher_context_t *dc, uint8_t p2) { +void handler_sign_psbt(dispatcher_context_t *dc, uint8_t protocol_version) { LOG_PROCESSOR(__FILE__, __LINE__, __func__); sign_psbt_state_t st; @@ -2474,7 +2461,7 @@ void handler_sign_psbt(dispatcher_context_t *dc, uint8_t p2) { return; } - st.p2 = p2; + st.protocol_version = protocol_version; // read APDU inputs, intialize global state and read global PSBT map if (!init_global_state(dc, &st)) return; diff --git a/src/main.c b/src/main.c index ead6cf1a..fd772f21 100644 --- a/src/main.c +++ b/src/main.c @@ -45,17 +45,9 @@ #include "swap/handle_get_printable_amount.h" #include "swap/handle_check_address.h" -// we don't import main_old.h in legacy-only mode, but we still need libargs_s; will refactor later -struct libargs_s { - unsigned int id; - unsigned int command; - void *unused; // it used to be the coin_config; unused in the new app - union { - check_address_parameters_t *check_address; - create_transaction_parameters_t *create_transaction; - get_printable_amount_parameters_t *get_printable_amount; - }; -}; +#ifdef HAVE_NBGL +#include "nbgl_use_case.h" +#endif #ifdef HAVE_BOLOS_APP_STACK_CANARY extern unsigned int app_stack_canary; @@ -163,7 +155,8 @@ void app_main() { &cmd); if (G_swap_state.called_from_swap && G_swap_state.should_exit) { - os_sched_exit(0); + // Bitcoin app will keep listening as long as it does not receive a valid TX + finalize_exchange_sign_transaction(true); } } } @@ -223,10 +216,10 @@ void coin_main() { TRY { io_seproxyhal_init(); -#ifdef TARGET_NANOX +#ifdef HAVE_BLE // grab the current plane mode setting G_io_app.plane_mode = os_setting_get(OS_SETTING_PLANEMODE, NULL, 0); -#endif // TARGET_NANOX +#endif // HAVE_BLE USB_power(0); USB_power(1); @@ -257,7 +250,7 @@ void coin_main() { app_exit(); } -static void swap_library_main_helper(struct libargs_s *args) { +static void swap_library_main_helper(libargs_t *args) { check_api_level(CX_COMPAT_APILEVEL); PRINTF("Inside a library \n"); switch (args->command) { @@ -278,19 +271,21 @@ static void swap_library_main_helper(struct libargs_s *args) { io_seproxyhal_init(); UX_INIT(); +#ifdef HAVE_BAGL ux_stack_push(); +#elif defined(HAVE_NBGL) + nbgl_useCaseSpinner("Signing"); +#endif // HAVE_BAGL USB_power(0); USB_power(1); // ui_idle(); PRINTF("USB power ON/OFF\n"); -#ifdef TARGET_NANOX +#ifdef HAVE_BLE // grab the current plane mode setting G_io_app.plane_mode = os_setting_get(OS_SETTING_PLANEMODE, NULL, 0); -#endif // TARGET_NANOX -#ifdef HAVE_BLE BLE_power(0, NULL); - BLE_power(1, "Nano X"); + BLE_power(1, NULL); #endif // HAVE_BLE app_main(); } @@ -308,7 +303,7 @@ static void swap_library_main_helper(struct libargs_s *args) { } } -void swap_library_main(struct libargs_s *args) { +void swap_library_main(libargs_t *args) { bool end = false; /* This loop ensures that swap_library_main_helper and os_lib_end are called * within a try context, even if an exception is thrown */ @@ -342,7 +337,7 @@ __attribute__((section(".boot"))) int main(int arg0) { } // Application launched as library (for swap support) - struct libargs_s *args = (struct libargs_s *) arg0; + libargs_t *args = (libargs_t *) arg0; if (args->id != 0x100) { app_exit(); return 0; diff --git a/src/swap/btchip_bcd.c b/src/swap/btchip_bcd.c index 36f3e47f..20e99383 100644 --- a/src/swap/btchip_bcd.c +++ b/src/swap/btchip_bcd.c @@ -69,7 +69,6 @@ unsigned char btchip_convert_hex_amount_to_displayable_no_globals(unsigned char* workOffset = offset; for (i = 0; i < LOOP2; i++) { unsigned char allZero = 1; - unsigned char j; for (j = i; j < LOOP2; j++) { if (scratch[workOffset + j] != 0) { allZero = 0; diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index a5eb9613..61e1d220 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -3,6 +3,7 @@ #include "ux.h" #include "usbd_core.h" #include "os_io_seproxyhal.h" +#include "os.h" #include "handle_swap_sign_transaction.h" @@ -10,6 +11,9 @@ #include "../swap/swap_globals.h" #include "../common/read.h" +// Save the BSS address where we will write the return value when finished +static uint8_t* G_swap_sign_return_value_address; + bool copy_transaction_parameters(create_transaction_parameters_t* sign_transaction_params) { char destination_address[65]; uint8_t amount[8]; @@ -41,6 +45,9 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti sign_transaction_params->fee_amount, sign_transaction_params->fee_amount_length); + os_explicit_zero_BSS_segment(); + G_swap_sign_return_value_address = &sign_transaction_params->result; + G_swap_state.amount = read_u64_be(amount, 0); G_swap_state.fees = read_u64_be(fees, 0); memcpy(G_swap_state.destination_address, @@ -48,3 +55,8 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti sizeof(G_swap_state.destination_address)); return true; } + +void __attribute__((noreturn)) finalize_exchange_sign_transaction(bool is_success) { + *G_swap_sign_return_value_address = is_success; + os_lib_end(); +} diff --git a/src/swap/handle_swap_sign_transaction.h b/src/swap/handle_swap_sign_transaction.h index d961b94c..bbd82b24 100644 --- a/src/swap/handle_swap_sign_transaction.h +++ b/src/swap/handle_swap_sign_transaction.h @@ -3,3 +3,5 @@ #include "swap_lib_calls.h" bool copy_transaction_parameters(create_transaction_parameters_t* sign_transaction_params); + +void __attribute__((noreturn)) finalize_exchange_sign_transaction(bool is_success); diff --git a/src/swap/swap_lib_calls.h b/src/swap/swap_lib_calls.h index d41d37ab..dc88417a 100644 --- a/src/swap/swap_lib_calls.h +++ b/src/swap/swap_lib_calls.h @@ -1,6 +1,13 @@ #pragma once -#include +/* This file is the shared API between Exchange and the apps started in Library mode for Exchange + * + * DO NOT MODIFY THIS FILE IN APPLICATIONS OTHER THAN EXCHANGE + * On modification in Exchange, forward the changes to all applications supporting Exchange + */ + +#include "stdbool.h" +#include "stdint.h" #define RUN_APPLICATION 1 @@ -10,17 +17,27 @@ #define GET_PRINTABLE_AMOUNT 4 +/* + * Amounts are stored as bytes, with a max size of 16 (see protobuf + * specifications). Max 16B integer is 340282366920938463463374607431768211455 + * in decimal, which is a 32-long char string. + * The printable amount also contains spaces, the ticker symbol (with variable + * size, up to 12 in Ethereum for instance) and a terminating null byte, so 50 + * bytes total should be a fair maximum. + */ +#define MAX_PRINTABLE_AMOUNT_SIZE 50 + // structure that should be send to specific coin application to get address typedef struct check_address_parameters_s { // IN - unsigned char* coin_configuration; - unsigned char coin_configuration_length; + uint8_t *coin_configuration; + uint8_t coin_configuration_length; // serialized path, segwit, version prefix, hash used, dictionary etc. // fields and serialization format depends on spesific coin app - unsigned char* address_parameters; - unsigned char address_parameters_length; - char* address_to_check; - char* extra_id_to_check; + uint8_t *address_parameters; + uint8_t address_parameters_length; + char *address_to_check; + char *extra_id_to_check; // OUT int result; } check_address_parameters_t; @@ -28,23 +45,36 @@ typedef struct check_address_parameters_s { // structure that should be send to specific coin application to get printable amount typedef struct get_printable_amount_parameters_s { // IN - unsigned char* coin_configuration; - unsigned char coin_configuration_length; - unsigned char* amount; - unsigned char amount_length; + uint8_t *coin_configuration; + uint8_t coin_configuration_length; + uint8_t *amount; + uint8_t amount_length; bool is_fee; // OUT - char printable_amount[30]; - // int result; + char printable_amount[MAX_PRINTABLE_AMOUNT_SIZE]; } get_printable_amount_parameters_t; typedef struct create_transaction_parameters_s { - unsigned char* coin_configuration; - unsigned char coin_configuration_length; - unsigned char* amount; - unsigned char amount_length; - unsigned char* fee_amount; - unsigned char fee_amount_length; - char* destination_address; - char* destination_address_extra_id; + // IN + uint8_t *coin_configuration; + uint8_t coin_configuration_length; + uint8_t *amount; + uint8_t amount_length; + uint8_t *fee_amount; + uint8_t fee_amount_length; + char *destination_address; + char *destination_address_extra_id; + // OUT + uint8_t result; } create_transaction_parameters_t; + +typedef struct libargs_s { + unsigned int id; + unsigned int command; + unsigned int unused; + union { + check_address_parameters_t *check_address; + create_transaction_parameters_t *create_transaction; + get_printable_amount_parameters_t *get_printable_amount; + }; +} libargs_t; diff --git a/src/ui/display.c b/src/ui/display.c index f70b724e..a4b00178 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -10,15 +10,6 @@ #include "ux.h" #include "./display.h" -#include "./display_utils.h" -#include "../constants.h" -#include "../globals.h" -#include "../boilerplate/io.h" -#include "../boilerplate/sw.h" -#include "../common/bip32.h" -#include "../common/format.h" -#include "../common/script.h" -#include "../constants.h" // These globals are a workaround for a limitation of the UX library that // does not allow to pass proper callbacks and context. @@ -28,66 +19,6 @@ static bool g_ux_flow_response; extern dispatcher_context_t G_dispatcher_context; -// TODO: hard to keep track of what globals are used in the same flows -// (especially since the same flow step can be shared in different flows) - -typedef struct { - char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; -} ui_path_state_t; - -typedef struct { - char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; - char pubkey[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; -} ui_path_and_pubkey_state_t; - -typedef struct { - char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; - char address[MAX_ADDRESS_LENGTH_STR + 1]; -} ui_path_and_address_state_t; - -typedef struct { - char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; - char hash_hex[64 + 1]; -} ui_path_and_hash_state_t; - -typedef struct { - char wallet_name[MAX_WALLET_NAME_LENGTH + 1]; - - // no flows show together both a policy map and an address, therefore we share memory - union { - char descriptor_template[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1]; - char address[MAX_ADDRESS_LENGTH_STR + 1]; - }; -} ui_wallet_state_t; - -typedef struct { - char pubkey[MAX_POLICY_KEY_INFO_LEN + 1]; - char signer_index[sizeof("Key @999 ")]; -} ui_cosigner_pubkey_and_index_state_t; - -typedef struct { - char index[sizeof("output #999")]; - char address_or_description[MAX(MAX_ADDRESS_LENGTH_STR + 1, MAX_OPRETURN_OUTPUT_DESC_SIZE)]; - char amount[MAX_AMOUNT_LENGTH + 1]; -} ui_validate_output_state_t; - -typedef struct { - char fee[MAX_AMOUNT_LENGTH + 1]; -} ui_validate_transaction_state_t; - -/** - * Union of all the states for each of the UI screens, in order to save memory. - */ -typedef union { - ui_path_and_pubkey_state_t path_and_pubkey; - ui_path_and_address_state_t path_and_address; - ui_path_and_hash_state_t path_and_hash; - ui_wallet_state_t wallet; - ui_cosigner_pubkey_and_index_state_t cosigner_pubkey_and_index; - ui_validate_output_state_t validate_output; - ui_validate_transaction_state_t validate_transaction; -} ui_state_t; - ui_state_t g_ui_state; void send_deny_sw(dispatcher_context_t *dc) { @@ -99,430 +30,16 @@ void set_ux_flow_response(bool approved) { g_ux_flow_response = approved; } -/* - STATELESS STEPS - As these steps do not access per-step globals (except possibly a callback), they can be used in - any flow. -*/ - -// Step with icon and text for pubkey -UX_STEP_NOCB(ux_display_confirm_pubkey_step, pn, {&C_icon_eye, "Confirm public key"}); - -// Step with icon and text for address -UX_STEP_NOCB(ux_display_confirm_address_step, pn, {&C_icon_eye, "Confirm receive address"}); - -// Step with icon and text for a suspicious address -UX_STEP_NOCB(ux_display_unusual_derivation_path_step, - pnn, - { - &C_icon_warning, - "The derivation", - "path is unusual", - }); - -// Step with icon and text to caution the user to reject if unsure -UX_STEP_CB(ux_display_reject_if_not_sure_step, - pnn, - set_ux_flow_response(false), - { - &C_icon_crossmark, - "Reject if you're", - "not sure", - }); - -// Step with approve button -UX_STEP_CB(ux_display_approve_step, - pb, - set_ux_flow_response(true), - { - &C_icon_validate_14, - "Approve", - }); - -// Step with continue button -UX_STEP_CB(ux_display_continue_step, - pb, - set_ux_flow_response(true), - { - &C_icon_validate_14, - "Continue", - }); - -// Step with reject button -UX_STEP_CB(ux_display_reject_step, - pb, - set_ux_flow_response(false), - { - &C_icon_crossmark, - "Reject", - }); - -/* - STATEFUL STEPS - These can only be used in the context of specific flows, as they access a common shared space - for strings. -*/ - -// PATH/PUBKEY or PATH/ADDRESS - -// Step with title/text for BIP32 path -UX_STEP_NOCB(ux_display_path_step, - bnnn_paging, - { - .title = "Path", - .text = g_ui_state.path_and_pubkey.bip32_path_str, - }); - -// Step with title/text for pubkey -UX_STEP_NOCB(ux_display_pubkey_step, - bnnn_paging, - { - .title = "Public key", - .text = g_ui_state.path_and_pubkey.pubkey, - }); - -// Step with title/text for address -UX_STEP_NOCB(ux_display_address_step, - bnnn_paging, - { - .title = "Address", - .text = g_ui_state.path_and_address.address, - }); - -// Step with description of a wallet policy -UX_STEP_NOCB(ux_display_wallet_policy_map_step, - bnnn_paging, - { - .title = "Wallet policy:", - .text = g_ui_state.wallet.descriptor_template, - }); - -// Step with index and xpub of a cosigner of a policy_map wallet -UX_STEP_NOCB(ux_display_wallet_policy_cosigner_pubkey_step, - bnnn_paging, - { - .title = g_ui_state.cosigner_pubkey_and_index.signer_index, - .text = g_ui_state.cosigner_pubkey_and_index.pubkey, - }); - -// Step with title/text for address, used when showing a wallet receive address -UX_STEP_NOCB(ux_display_wallet_address_step, - bnnn_paging, - { - .title = "Address", - .text = g_ui_state.wallet.address, - }); - -// Step with warning icon and text explaining that there are external inputs -UX_STEP_NOCB(ux_display_warning_external_inputs_step, - pnn, - { - &C_icon_warning, - "There are", - "external inputs", - }); - -// Step with warning icon for unverified inputs (segwit inputs with no non-witness-utxo) -UX_STEP_NOCB(ux_unverified_segwit_input_flow_1_step, pb, {&C_icon_warning, "Unverified inputs"}); -UX_STEP_NOCB(ux_unverified_segwit_input_flow_2_step, nn, {"Update", "Ledger Live"}); -UX_STEP_NOCB(ux_unverified_segwit_input_flow_3_step, nn, {"or third party", "wallet software"}); - -// Step with warning icon for nondefault sighash -UX_STEP_NOCB(ux_nondefault_sighash_flow_1_step, pb, {&C_icon_warning, "Non-default sighash"}); - -// Step with eye icon and "Review" and the output index -UX_STEP_NOCB(ux_review_step, - pnn, - { - &C_icon_eye, - "Review", - g_ui_state.validate_output.index, - }); - -// Step with "Amount" and an output amount -UX_STEP_NOCB(ux_validate_amount_step, - bnnn_paging, - { - .title = "Amount", - .text = g_ui_state.validate_output.amount, - }); - -// Step with "Address" and a paginated address -UX_STEP_NOCB(ux_validate_address_step, - bnnn_paging, - { - .title = "Address", - .text = g_ui_state.validate_output.address_or_description, - }); - -UX_STEP_NOCB(ux_confirm_transaction_step, pnn, {&C_icon_eye, "Confirm", "transaction"}); -UX_STEP_NOCB(ux_confirm_transaction_fees_step, - bnnn_paging, - { - .title = "Fees", - .text = g_ui_state.validate_transaction.fee, - }); -UX_STEP_CB(ux_accept_and_send_step, - pbb, - set_ux_flow_response(true), - {&C_icon_validate_14, "Accept", "and send"}); - -// Step with wallet icon and "Register wallet" -UX_STEP_NOCB(ux_display_register_wallet_step, - pb, - { - &C_icon_wallet, - "Register wallet", - }); - -// Step with wallet icon and "Receive in known wallet" -UX_STEP_NOCB(ux_display_receive_in_registered_wallet_step, - pnn, - { - &C_icon_wallet, - "Receive in", - "known wallet", - }); - -// Step with wallet icon and "Spend from known wallet" -UX_STEP_NOCB(ux_display_spend_from_registered_wallet_step, - pnn, - { - &C_icon_wallet, - "Spend from", - "known wallet", - }); - -// Step with "Wallet name:", followed by the wallet name -UX_STEP_NOCB(ux_display_wallet_name_step, - bnnn_paging, - { - .title = "Wallet name:", - .text = g_ui_state.wallet.wallet_name, - }); - -////////////////////////////////////////////////////////////////////// -UX_STEP_NOCB(ux_sign_message_step, - pnn, - { - &C_icon_certificate, - "Sign", - "message", - }); - -UX_STEP_NOCB(ux_message_sign_display_path_step, - bnnn_paging, - { - .title = "Path", - .text = g_ui_state.path_and_hash.bip32_path_str, - }); - -UX_STEP_NOCB(ux_message_hash_step, - bnnn_paging, - { - .title = "Message hash", - .text = g_ui_state.path_and_hash.hash_hex, - }); - -UX_STEP_CB(ux_sign_message_accept_new, - pbb, - set_ux_flow_response(true), - {&C_icon_validate_14, "Sign", "message"}); - -// FLOW to display BIP32 path and a message hash to sign: -// #1 screen: certificate icon + "Sign message" -// #2 screen: display BIP32 Path -// #3 screen: display message hash -// #4 screen: "Sign message" and approve button -// #5 screen: reject button -UX_FLOW(ux_sign_message_flow, - &ux_sign_message_step, - &ux_message_sign_display_path_step, - &ux_message_hash_step, - &ux_sign_message_accept_new, - &ux_display_reject_step); - -// FLOW to display BIP32 path and pubkey: -// #1 screen: eye icon + "Confirm Pubkey" -// #2 screen: display BIP32 Path -// #3 screen: display pubkey -// #4 screen: approve button -// #5 screen: reject button -UX_FLOW(ux_display_pubkey_flow, - &ux_display_confirm_pubkey_step, - &ux_display_path_step, - &ux_display_pubkey_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display BIP32 path and pubkey, for a non-standard path: -// #1 screen: warning icon + "The derivation path is unusual" -// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) -// #3 screen: eye icon + "Confirm Pubkey" -// #4 screen: display BIP32 Path -// #5 screen: display pubkey -// #6 screen: approve button -// #7 screen: reject button -UX_FLOW(ux_display_pubkey_suspicious_flow, - &ux_display_unusual_derivation_path_step, - &ux_display_confirm_pubkey_step, - &ux_display_path_step, - &ux_display_reject_if_not_sure_step, - &ux_display_pubkey_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display a receive address, for a non-standard path: -// #1 screen: warning icon + "The derivation path is unusual" -// #2 screen: display BIP32 Path -// #3 screen: crossmark icon + "Reject if not sure" (user can reject here) -// #4 screen: eye icon + "Confirm Address" -// #5 screen: display address -// #6 screen: approve button -// #7 screen: reject button -UX_FLOW(ux_display_address_suspicious_flow, - &ux_display_unusual_derivation_path_step, - &ux_display_path_step, - &ux_display_reject_if_not_sure_step, - &ux_display_confirm_address_step, - &ux_display_address_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to warn the user if a change output has an unusual derivation path -// (e.g. account index or address index too large): -// #1 screen: warning icon + "The derivation path is unusual" -// #2 screen: display BIP32 Path -// #3 screen: crossmark icon + "Reject if not sure" (user can reject here) -// #4 screen: approve button -// #5 screen: reject button -UX_FLOW(ux_display_unusual_derivation_path_flow, - &ux_display_unusual_derivation_path_step, - &ux_display_path_step, - &ux_display_reject_if_not_sure_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display the header of a policy map wallet: -// #1 screen: Wallet icon + "Register wallet" -// #2 screen: "Wallet name:" and wallet name -// #3 screen: display policy map (paginated) -// #4 screen: approve button -// #5 screen: reject button -UX_FLOW(ux_display_register_wallet_flow, - &ux_display_register_wallet_step, - &ux_display_wallet_name_step, - &ux_display_wallet_policy_map_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display the header of a policy_map wallet: -// #1 screen: Cosigner index and pubkey (paginated) -// #2 screen: approve button -// #3 screen: reject button -UX_FLOW(ux_display_policy_map_cosigner_pubkey_flow, - &ux_display_wallet_policy_cosigner_pubkey_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display the name and an address of a registered wallet: -// #1 screen: Wallet icon + "Receive in known wallet" -// #2 screen: wallet name -// #3 screen: wallet address (paginated) -// #4 screen: approve button -// #5 screen: reject button -UX_FLOW(ux_display_receive_in_wallet_flow, - &ux_display_receive_in_registered_wallet_step, - &ux_display_wallet_name_step, - &ux_display_wallet_address_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display an address of a canonical wallet: -// #1 screen: wallet address (paginated) -// #2 screen: approve button -// #3 screen: reject button -UX_FLOW(ux_display_canonical_wallet_address_flow, - &ux_display_wallet_address_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to display a registered wallet and authorize spending: -// #1 screen: "Spend from known wallet" -// #2 screen: wallet name -// #3 screen: approve button -// #4 screen: reject button -UX_FLOW(ux_display_spend_from_wallet_flow, - &ux_display_spend_from_registered_wallet_step, - &ux_display_wallet_name_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// FLOW to warn about external inputs -// #1 screen: warning icon + "There are external inputs" -// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) -// #3 screen: "continue" button -UX_FLOW(ux_display_warning_external_inputs_flow, - &ux_display_warning_external_inputs_step, - &ux_display_reject_if_not_sure_step, - &ux_display_continue_step); - -// FLOW to warn about segwitv0 inputs with no non-witness-utxo -// #1 screen: warning icon + "Unverified inputs" -// #2 screen: "Update Ledger Live" -// #3 screen: "or external wallet software" -// #4 screen: "continue" button -// #5 screen: "reject" button -UX_FLOW(ux_display_unverified_segwit_inputs_flow, - &ux_unverified_segwit_input_flow_1_step, - &ux_unverified_segwit_input_flow_2_step, - &ux_unverified_segwit_input_flow_3_step, - &ux_display_continue_step, - &ux_display_reject_step); - -// FLOW to warn about segwitv1 inputs with non-default sighash -// #1 screen: warning icon + "Non default sighash" -// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) -// #3 screen: "continue" button -// #4 screen: "reject" button -UX_FLOW(ux_display_nondefault_sighash_flow, - &ux_nondefault_sighash_flow_1_step, - &ux_display_reject_if_not_sure_step, - &ux_display_continue_step, - &ux_display_reject_step); - -// FLOW to validate a single output -// #1 screen: eye icon + "Review" + index of output to validate -// #2 screen: output amount -// #3 screen: output address (paginated) -// #4 screen: approve button -// #5 screen: reject button -UX_FLOW(ux_display_output_address_amount_flow, - &ux_review_step, - &ux_validate_amount_step, - &ux_validate_address_step, - &ux_display_approve_step, - &ux_display_reject_step); - -// Finalize see the transaction fees and finally accept signing -// #1 screen: eye icon + "Confirm Transaction" -// #2 screen: fee amount -// #3 screen: "Accept and send", with approve button -// #4 screen: reject button -UX_FLOW(ux_accept_transaction_flow, - &ux_confirm_transaction_step, - &ux_confirm_transaction_fees_step, - &ux_accept_and_send_step, - &ux_display_reject_step); - // Process UI events until the current flow terminates; does not handle any APDU exchange -// This method also sets the UI state as "dirty" so that the dispatcher refreshes resets the UI -// at the end of the command handler. +// This method also sets the UI state as "dirty" according to the input parameter +// so that the dispatcher refreshes resets the UI at the end of the command handler. // Returns true/false depending if the user accepted in the corresponding UX flow. -bool io_ui_process(dispatcher_context_t *context) { +static bool io_ui_process(dispatcher_context_t *context, bool set_dirty) { g_ux_flow_ended = false; - context->set_ui_dirty(); + if (set_dirty) { + context->set_ui_dirty(); + } // We are not waiting for the client's input, nor we are doing computations on the device io_clear_processing_timeout(); @@ -550,12 +67,12 @@ bool ui_display_pubkey(dispatcher_context_t *context, strncpy(state->pubkey, pubkey, sizeof(state->pubkey)); if (!is_path_suspicious) { - ux_flow_init(0, ux_display_pubkey_flow, NULL); + ui_display_pubkey_flow(); } else { - ux_flow_init(0, ux_display_pubkey_suspicious_flow, NULL); + ui_display_pubkey_suspicious_flow(); } - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_display_message_hash(dispatcher_context_t *context, @@ -566,9 +83,9 @@ bool ui_display_message_hash(dispatcher_context_t *context, strncpy(state->bip32_path_str, bip32_path_str, sizeof(state->bip32_path_str)); strncpy(state->hash_hex, message_hash, sizeof(state->hash_hex)); - ux_flow_init(0, ux_sign_message_flow, NULL); + ui_sign_message_flow(); - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_display_register_wallet(dispatcher_context_t *context, @@ -581,9 +98,9 @@ bool ui_display_register_wallet(dispatcher_context_t *context, strncpy(state->descriptor_template, policy_descriptor, sizeof(state->descriptor_template)); state->descriptor_template[wallet_header->descriptor_template_len] = 0; - ux_flow_init(0, ux_display_register_wallet_flow, NULL); + ui_display_register_wallet_flow(); - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_display_policy_map_cosigner_pubkey(dispatcher_context_t *context, @@ -609,10 +126,9 @@ bool ui_display_policy_map_cosigner_pubkey(dispatcher_context_t *context, "Key @%u ", cosigner_index); } + ui_display_policy_map_cosigner_pubkey_flow(); - ux_flow_init(0, ux_display_policy_map_cosigner_pubkey_flow, NULL); - - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_display_wallet_address(dispatcher_context_t *context, @@ -623,66 +139,130 @@ bool ui_display_wallet_address(dispatcher_context_t *context, strncpy(state->address, address, sizeof(state->address)); if (wallet_name == NULL) { - ux_flow_init(0, ux_display_canonical_wallet_address_flow, NULL); + ui_display_default_wallet_address_flow(); } else { strncpy(state->wallet_name, wallet_name, sizeof(state->wallet_name)); - ux_flow_init(0, ux_display_receive_in_wallet_flow, NULL); + ui_display_receive_in_wallet_flow(); } - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_authorize_wallet_spend(dispatcher_context_t *context, const char *wallet_name) { ui_wallet_state_t *state = (ui_wallet_state_t *) &g_ui_state; strncpy(state->wallet_name, wallet_name, sizeof(state->wallet_name)); + ui_display_spend_from_wallet_flow(); - ux_flow_init(0, ux_display_spend_from_wallet_flow, NULL); - - return io_ui_process(context); + return io_ui_process(context, true); } bool ui_warn_external_inputs(dispatcher_context_t *context) { - ux_flow_init(0, ux_display_warning_external_inputs_flow, NULL); - - return io_ui_process(context); + ui_display_warning_external_inputs_flow(); + return io_ui_process(context, true); } bool ui_warn_unverified_segwit_inputs(dispatcher_context_t *context) { - ux_flow_init(0, ux_display_unverified_segwit_inputs_flow, NULL); - return io_ui_process(context); + ui_display_unverified_segwit_inputs_flows(); + return io_ui_process(context, true); } bool ui_warn_nondefault_sighash(dispatcher_context_t *context) { - ux_flow_init(0, ux_display_nondefault_sighash_flow, NULL); + ui_display_nondefault_sighash_flow(); + return io_ui_process(context, true); +} - return io_ui_process(context); +bool ui_transaction_prompt(dispatcher_context_t *context, const int external_outputs_total_count) { + ui_display_transaction_prompt(external_outputs_total_count); + return io_ui_process(context, true); } bool ui_validate_output(dispatcher_context_t *context, int index, + int total_count, const char *address_or_description, const char *coin_name, uint64_t amount) { ui_validate_output_state_t *state = (ui_validate_output_state_t *) &g_ui_state; - snprintf(state->index, sizeof(state->index), "output #%d", index); strncpy(state->address_or_description, address_or_description, sizeof(state->address_or_description)); format_sats_amount(coin_name, amount, state->amount); - ux_flow_init(0, ux_display_output_address_amount_flow, NULL); + if (total_count == 1) { + ui_display_output_address_amount_no_index_flow(index); + } else { + ui_display_output_address_amount_flow(index); + } - return io_ui_process(context); + return io_ui_process(context, true); } -bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_name, uint64_t fee) { +bool ui_validate_transaction(dispatcher_context_t *context, + const char *coin_name, + uint64_t fee, + bool is_self_transfer) { ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state; format_sats_amount(coin_name, fee, state->fee); - ux_flow_init(0, ux_accept_transaction_flow, NULL); + ui_accept_transaction_flow(is_self_transfer); + + return io_ui_process(context, true); +} + +#ifdef HAVE_BAGL +bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success) { + (void) context; + (void) success; + return true; +} + +bool ui_post_processing_confirm_wallet_spend(dispatcher_context_t *context, bool success) { + (void) context; + (void) success; + return true; +} + +bool ui_post_processing_confirm_transaction(dispatcher_context_t *context, bool success) { + (void) context; + (void) success; + return true; +} + +bool ui_post_processing_confirm_message(dispatcher_context_t *context, bool success) { + (void) context; + (void) success; + return true; +} + +#endif // HAVE_BAGL + +#ifdef HAVE_NBGL +bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success) { + (void) context; + ui_display_post_processing_confirm_wallet_registation(success); + + return true; +} + +bool ui_post_processing_confirm_wallet_spend(dispatcher_context_t *context, bool success) { + ui_display_post_processing_confirm_wallet_spend(success); + + return io_ui_process(context, success); +} + +bool ui_post_processing_confirm_transaction(dispatcher_context_t *context, bool success) { + ui_display_post_processing_confirm_transaction(success); + + return io_ui_process(context, success); +} + +bool ui_post_processing_confirm_message(dispatcher_context_t *context, bool success) { + (void) context; + ui_display_post_processing_confirm_message(success); - return io_ui_process(context); + return true; } +#endif // HAVE_NBGL diff --git a/src/ui/display.h b/src/ui/display.h index c2835620..d3532139 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -4,6 +4,77 @@ #include "../boilerplate/dispatcher.h" #include "../common/wallet.h" +#include "./display.h" +#include "./display_utils.h" +#include "../constants.h" +#include "../globals.h" +#include "../boilerplate/io.h" +#include "../boilerplate/sw.h" +#include "../common/bip32.h" +#include "../common/format.h" +#include "../common/script.h" +#include "../constants.h" + +// TODO: hard to keep track of what globals are used in the same flows +// (especially since the same flow step can be shared in different flows) + +typedef struct { + char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; +} ui_path_state_t; + +typedef struct { + char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; + char pubkey[MAX_SERIALIZED_PUBKEY_LENGTH + 1]; +} ui_path_and_pubkey_state_t; + +typedef struct { + char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; + char address[MAX_ADDRESS_LENGTH_STR + 1]; +} ui_path_and_address_state_t; + +typedef struct { + char bip32_path_str[MAX_SERIALIZED_BIP32_PATH_LENGTH + 1]; + char hash_hex[64 + 1]; +} ui_path_and_hash_state_t; + +typedef struct { + char wallet_name[MAX_WALLET_NAME_LENGTH + 1]; + + // no flows show together both a policy map and an address, therefore we share memory + union { + char descriptor_template[MAX_DESCRIPTOR_TEMPLATE_LENGTH + 1]; + char address[MAX_ADDRESS_LENGTH_STR + 1]; + }; +} ui_wallet_state_t; + +typedef struct { + char pubkey[MAX_POLICY_KEY_INFO_LEN + 1]; + char signer_index[sizeof("Key @999 ")]; +} ui_cosigner_pubkey_and_index_state_t; + +typedef struct { + char index[sizeof("output #999")]; + char address_or_description[MAX(MAX_ADDRESS_LENGTH_STR + 1, MAX_OPRETURN_OUTPUT_DESC_SIZE)]; + char amount[MAX_AMOUNT_LENGTH + 1]; +} ui_validate_output_state_t; + +typedef struct { + char fee[MAX_AMOUNT_LENGTH + 1]; +} ui_validate_transaction_state_t; + +/** + * Union of all the states for each of the UI screens, in order to save memory. + */ +typedef union { + ui_path_and_pubkey_state_t path_and_pubkey; + ui_path_and_address_state_t path_and_address; + ui_path_and_hash_state_t path_and_hash; + ui_wallet_state_t wallet; + ui_cosigner_pubkey_and_index_state_t cosigner_pubkey_and_index; + ui_validate_output_state_t validate_output; + ui_validate_transaction_state_t validate_transaction; +} ui_state_t; +extern ui_state_t g_ui_state; /** * Callback to reuse action with approve/reject in step FLOW. @@ -56,8 +127,60 @@ bool ui_warn_nondefault_sighash(dispatcher_context_t *context); bool ui_validate_output(dispatcher_context_t *context, int index, + int total_count, const char *address_or_description, const char *coin_name, uint64_t amount); -bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_name, uint64_t fee); +bool ui_validate_transaction(dispatcher_context_t *context, + const char *coin_name, + uint64_t fee, + bool is_self_transfer); + +void set_ux_flow_response(bool approved); + +void ui_display_pubkey_flow(void); + +void ui_display_pubkey_suspicious_flow(void); + +void ui_sign_message_flow(void); + +void ui_display_register_wallet_flow(void); + +void ui_display_policy_map_cosigner_pubkey_flow(void); + +void ui_display_receive_in_wallet_flow(void); + +void ui_display_default_wallet_address_flow(void); + +void ui_display_spend_from_wallet_flow(void); + +void ui_display_warning_external_inputs_flow(void); + +void ui_display_unverified_segwit_inputs_flows(void); + +void ui_display_nondefault_sighash_flow(void); + +void ui_display_output_address_amount_flow(int index); + +void ui_display_output_address_amount_no_index_flow(int index); + +void ui_accept_transaction_flow(bool is_self_transfer); + +void ui_display_transaction_prompt(const int external_outputs_total_count); + +bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success); + +bool ui_post_processing_confirm_wallet_spend(dispatcher_context_t *context, bool success); + +bool ui_post_processing_confirm_transaction(dispatcher_context_t *context, bool success); + +bool ui_post_processing_confirm_message(dispatcher_context_t *context, bool success); + +#ifdef HAVE_NBGL +bool ui_transaction_prompt(dispatcher_context_t *context, const int external_outputs_total_count); +void ui_display_post_processing_confirm_message(bool success); +void ui_display_post_processing_confirm_wallet_registation(bool success); +void ui_display_post_processing_confirm_transaction(bool success); +void ui_display_post_processing_confirm_wallet_spend(bool success); +#endif diff --git a/src/ui/display_bagl.c b/src/ui/display_bagl.c new file mode 100644 index 00000000..17edf831 --- /dev/null +++ b/src/ui/display_bagl.c @@ -0,0 +1,475 @@ +#ifdef HAVE_BAGL + +#pragma GCC diagnostic ignored "-Wformat-invalid-specifier" // snprintf +#pragma GCC diagnostic ignored "-Wformat-extra-args" // snprintf + +#include // bool +#include // snprintf +#include // memset +#include + +#include "os.h" +#include "ux.h" + +#include "./display.h" +#include "./display_utils.h" +#include "../constants.h" +#include "../globals.h" +#include "../boilerplate/io.h" +#include "../boilerplate/sw.h" +#include "../common/bip32.h" +#include "../common/format.h" +#include "../common/script.h" +#include "../constants.h" + +/* + STATELESS STEPS + As these steps do not access per-step globals (except possibly a callback), they can be used in + any flow. +*/ + +// Step with icon and text for pubkey +UX_STEP_NOCB(ux_display_confirm_pubkey_step, pn, {&C_icon_eye, "Confirm public key"}); + +// Step with icon and text for a suspicious address +UX_STEP_NOCB(ux_display_unusual_derivation_path_step, + pnn, + { + &C_icon_warning, + "The derivation", + "path is unusual", + }); + +// Step with icon and text to caution the user to reject if unsure +UX_STEP_CB(ux_display_reject_if_not_sure_step, + pnn, + set_ux_flow_response(false), + { + &C_icon_crossmark, + "Reject if you're", + "not sure", + }); + +// Step with approve button +UX_STEP_CB(ux_display_approve_step, + pb, + set_ux_flow_response(true), + { + &C_icon_validate_14, + "Approve", + }); + +// Step with continue button +UX_STEP_CB(ux_display_continue_step, + pb, + set_ux_flow_response(true), + { + &C_icon_validate_14, + "Continue", + }); + +// Step with reject button +UX_STEP_CB(ux_display_reject_step, + pb, + set_ux_flow_response(false), + { + &C_icon_crossmark, + "Reject", + }); + +/* + STATEFUL STEPS + These can only be used in the context of specific flows, as they access a common shared space + for strings. +*/ + +// PATH/PUBKEY or PATH/ADDRESS + +// Step with title/text for BIP32 path +UX_STEP_NOCB(ux_display_path_step, + bnnn_paging, + { + .title = "Path", + .text = g_ui_state.path_and_pubkey.bip32_path_str, + }); + +// Step with title/text for pubkey +UX_STEP_NOCB(ux_display_pubkey_step, + bnnn_paging, + { + .title = "Public key", + .text = g_ui_state.path_and_pubkey.pubkey, + }); + +// Step with description of a wallet policy +UX_STEP_NOCB(ux_display_wallet_policy_map_step, + bnnn_paging, + { + .title = "Wallet policy:", + .text = g_ui_state.wallet.descriptor_template, + }); + +// Step with index and xpub of a cosigner of a policy_map wallet +UX_STEP_NOCB(ux_display_wallet_policy_cosigner_pubkey_step, + bnnn_paging, + { + .title = g_ui_state.cosigner_pubkey_and_index.signer_index, + .text = g_ui_state.cosigner_pubkey_and_index.pubkey, + }); + +// Step with title/text for address, used when showing a wallet receive address +UX_STEP_NOCB(ux_display_wallet_address_step, + bnnn_paging, + { + .title = "Address", + .text = g_ui_state.wallet.address, + }); + +// Step with warning icon and text explaining that there are external inputs +UX_STEP_NOCB(ux_display_warning_external_inputs_step, + pnn, + { + &C_icon_warning, + "There are", + "external inputs", + }); + +// Step with warning icon for unverified inputs (segwit inputs with no non-witness-utxo) +UX_STEP_NOCB(ux_unverified_segwit_input_flow_1_step, pb, {&C_icon_warning, "Unverified inputs"}); +UX_STEP_NOCB(ux_unverified_segwit_input_flow_2_step, nn, {"Update", "Ledger Live"}); +UX_STEP_NOCB(ux_unverified_segwit_input_flow_3_step, nn, {"or third party", "wallet software"}); + +// Step with warning icon for nondefault sighash +UX_STEP_NOCB(ux_nondefault_sighash_flow_1_step, pb, {&C_icon_warning, "Non-default sighash"}); + +// Step with eye icon and "Review" and the output index +UX_STEP_NOCB(ux_review_step, + pnn, + { + &C_icon_eye, + "Review", + g_ui_state.validate_output.index, + }); + +// Step with "Amount" and an output amount +UX_STEP_NOCB(ux_validate_amount_step, + bnnn_paging, + { + .title = "Amount", + .text = g_ui_state.validate_output.amount, + }); + +// Step with "Address" and a paginated address +UX_STEP_NOCB(ux_validate_address_step, + bnnn_paging, + { + .title = "Address", + .text = g_ui_state.validate_output.address_or_description, + }); + +UX_STEP_NOCB(ux_confirm_transaction_step, pnn, {&C_icon_eye, "Confirm", "transaction"}); +UX_STEP_NOCB(ux_confirm_selftransfer_step, pnn, {&C_icon_eye, "Confirm", "self-transfer"}); +UX_STEP_NOCB(ux_confirm_transaction_fees_step, + bnnn_paging, + { + .title = "Fees", + .text = g_ui_state.validate_transaction.fee, + }); +UX_STEP_CB(ux_accept_and_send_step, + pbb, + set_ux_flow_response(true), + {&C_icon_validate_14, "Accept", "and send"}); + +// Step with wallet icon and "Register wallet" +UX_STEP_NOCB(ux_display_register_wallet_step, + pb, + { + &C_icon_wallet, + "Register wallet", + }); + +// Step with wallet icon and "Receive in known wallet" +UX_STEP_NOCB(ux_display_receive_in_registered_wallet_step, + pnn, + { + &C_icon_wallet, + "Receive in", + "known wallet", + }); + +// Step with wallet icon and "Spend from known wallet" +UX_STEP_NOCB(ux_display_spend_from_registered_wallet_step, + pnn, + { + &C_icon_wallet, + "Spend from", + "known wallet", + }); + +// Step with "Wallet name:", followed by the wallet name +UX_STEP_NOCB(ux_display_wallet_name_step, + bnnn_paging, + { + .title = "Wallet name:", + .text = g_ui_state.wallet.wallet_name, + }); + +////////////////////////////////////////////////////////////////////// +UX_STEP_NOCB(ux_sign_message_step, + pnn, + { + &C_icon_certificate, + "Sign", + "message", + }); + +UX_STEP_NOCB(ux_message_sign_display_path_step, + bnnn_paging, + { + .title = "Path", + .text = g_ui_state.path_and_hash.bip32_path_str, + }); + +UX_STEP_NOCB(ux_message_hash_step, + bnnn_paging, + { + .title = "Message hash", + .text = g_ui_state.path_and_hash.hash_hex, + }); + +UX_STEP_CB(ux_sign_message_accept_new, + pbb, + set_ux_flow_response(true), + {&C_icon_validate_14, "Sign", "message"}); + +// FLOW to display BIP32 path and a message hash to sign: +// #1 screen: certificate icon + "Sign message" +// #2 screen: display BIP32 Path +// #3 screen: display message hash +// #4 screen: "Sign message" and approve button +// #5 screen: reject button +UX_FLOW(ux_sign_message_flow, + &ux_sign_message_step, + &ux_message_sign_display_path_step, + &ux_message_hash_step, + &ux_sign_message_accept_new, + &ux_display_reject_step); + +// FLOW to display BIP32 path and pubkey: +// #1 screen: eye icon + "Confirm Pubkey" +// #2 screen: display BIP32 Path +// #3 screen: display pubkey +// #4 screen: approve button +// #5 screen: reject button +UX_FLOW(ux_display_pubkey_flow, + &ux_display_confirm_pubkey_step, + &ux_display_path_step, + &ux_display_pubkey_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display BIP32 path and pubkey, for a non-standard path: +// #1 screen: warning icon + "The derivation path is unusual" +// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) +// #3 screen: eye icon + "Confirm Pubkey" +// #4 screen: display BIP32 Path +// #5 screen: display pubkey +// #6 screen: approve button +// #7 screen: reject button +UX_FLOW(ux_display_pubkey_suspicious_flow, + &ux_display_unusual_derivation_path_step, + &ux_display_confirm_pubkey_step, + &ux_display_path_step, + &ux_display_reject_if_not_sure_step, + &ux_display_pubkey_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display the header of a policy map wallet: +// #1 screen: Wallet icon + "Register wallet" +// #2 screen: "Wallet name:" and wallet name +// #3 screen: display policy map (paginated) +// #4 screen: approve button +// #5 screen: reject button +UX_FLOW(ux_display_register_wallet_flow, + &ux_display_register_wallet_step, + &ux_display_wallet_name_step, + &ux_display_wallet_policy_map_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display the header of a policy_map wallet: +// #1 screen: Cosigner index and pubkey (paginated) +// #2 screen: approve button +// #3 screen: reject button +UX_FLOW(ux_display_policy_map_cosigner_pubkey_flow, + &ux_display_wallet_policy_cosigner_pubkey_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display the name and an address of a registered wallet: +// #1 screen: Wallet icon + "Receive in known wallet" +// #2 screen: wallet name +// #3 screen: wallet address (paginated) +// #4 screen: approve button +// #5 screen: reject button +UX_FLOW(ux_display_receive_in_wallet_flow, + &ux_display_receive_in_registered_wallet_step, + &ux_display_wallet_name_step, + &ux_display_wallet_address_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display an address of a default wallet policy: +// #1 screen: wallet address (paginated) +// #2 screen: approve button +// #3 screen: reject button +UX_FLOW(ux_display_default_wallet_address_flow, + &ux_display_wallet_address_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to display a registered wallet and authorize spending: +// #1 screen: "Spend from known wallet" +// #2 screen: wallet name +// #3 screen: approve button +// #4 screen: reject button +UX_FLOW(ux_display_spend_from_wallet_flow, + &ux_display_spend_from_registered_wallet_step, + &ux_display_wallet_name_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// FLOW to warn about external inputs +// #1 screen: warning icon + "There are external inputs" +// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) +// #3 screen: "continue" button +UX_FLOW(ux_display_warning_external_inputs_flow, + &ux_display_warning_external_inputs_step, + &ux_display_reject_if_not_sure_step, + &ux_display_continue_step); + +// FLOW to warn about segwitv0 inputs with no non-witness-utxo +// #1 screen: warning icon + "Unverified inputs" +// #2 screen: "Update Ledger Live" +// #3 screen: "or external wallet software" +// #4 screen: "continue" button +// #5 screen: "reject" button +UX_FLOW(ux_display_unverified_segwit_inputs_flow, + &ux_unverified_segwit_input_flow_1_step, + &ux_unverified_segwit_input_flow_2_step, + &ux_unverified_segwit_input_flow_3_step, + &ux_display_continue_step, + &ux_display_reject_step); + +// FLOW to warn about segwitv1 inputs with non-default sighash +// #1 screen: warning icon + "Non default sighash" +// #2 screen: crossmark icon + "Reject if not sure" (user can reject here) +// #3 screen: "continue" button +// #4 screen: "reject" button +UX_FLOW(ux_display_nondefault_sighash_flow, + &ux_nondefault_sighash_flow_1_step, + &ux_display_reject_if_not_sure_step, + &ux_display_continue_step, + &ux_display_reject_step); + +// FLOW to validate a single output +// #1 screen: eye icon + "Review" + index of output to validate +// #2 screen: output amount +// #3 screen: output address (paginated) +// #4 screen: approve button +// #5 screen: reject button +UX_FLOW(ux_display_output_address_amount_flow, + &ux_review_step, + &ux_validate_amount_step, + &ux_validate_address_step, + &ux_display_approve_step, + &ux_display_reject_step); + +// Finalize see the transaction fees and finally accept signing +// #1 screen: eye icon + "Confirm transaction" +// #2 screen: fee amount +// #3 screen: "Accept and send", with approve button +// #4 screen: reject button +UX_FLOW(ux_accept_transaction_flow, + &ux_confirm_transaction_step, + &ux_confirm_transaction_fees_step, + &ux_accept_and_send_step, + &ux_display_reject_step); + +// Finalize see the transaction fees and finally accept signing +// #1 screen: eye icon + "Confirm self-transfer" +// #2 screen: fee amount +// #3 screen: "Accept and send", with approve button +// #4 screen: reject button +UX_FLOW(ux_accept_selftransfer_flow, + &ux_confirm_selftransfer_step, + &ux_confirm_transaction_fees_step, + &ux_accept_and_send_step, + &ux_display_reject_step); + +void ui_display_pubkey_flow(void) { + ux_flow_init(0, ux_display_pubkey_flow, NULL); +} + +void ui_display_pubkey_suspicious_flow(void) { + ux_flow_init(0, ux_display_pubkey_suspicious_flow, NULL); +} + +void ui_sign_message_flow(void) { + ux_flow_init(0, ux_sign_message_flow, NULL); +} + +void ui_display_register_wallet_flow(void) { + ux_flow_init(0, ux_display_register_wallet_flow, NULL); +} + +void ui_display_policy_map_cosigner_pubkey_flow(void) { + ux_flow_init(0, ux_display_policy_map_cosigner_pubkey_flow, NULL); +} + +void ui_display_receive_in_wallet_flow(void) { + ux_flow_init(0, ux_display_receive_in_wallet_flow, NULL); +} + +void ui_display_default_wallet_address_flow(void) { + ux_flow_init(0, ux_display_default_wallet_address_flow, NULL); +} + +void ui_display_spend_from_wallet_flow(void) { + ux_flow_init(0, ux_display_spend_from_wallet_flow, NULL); +} + +void ui_display_warning_external_inputs_flow(void) { + ux_flow_init(0, ux_display_warning_external_inputs_flow, NULL); +} + +void ui_display_unverified_segwit_inputs_flows(void) { + ux_flow_init(0, ux_display_unverified_segwit_inputs_flow, NULL); +} + +void ui_display_nondefault_sighash_flow(void) { + ux_flow_init(0, ux_display_nondefault_sighash_flow, NULL); +} + +void ui_display_output_address_amount_flow(int index) { + snprintf(g_ui_state.validate_output.index, + sizeof(g_ui_state.validate_output.index), + "output #%d", + index); + ux_flow_init(0, ux_display_output_address_amount_flow, NULL); +} + +void ui_display_output_address_amount_no_index_flow(int index) { + // Currently we don't want any change in the UX so this function defaults to + // ui_display_output_address_amount_flow + ui_display_output_address_amount_flow(index); +} + +void ui_accept_transaction_flow(bool is_self_transfer) { + ux_flow_init(0, + is_self_transfer ? ux_accept_selftransfer_flow : ux_accept_transaction_flow, + NULL); +} + +#endif // HAVE_BAGL diff --git a/src/ui/display_nbgl.c b/src/ui/display_nbgl.c new file mode 100644 index 00000000..b921fcef --- /dev/null +++ b/src/ui/display_nbgl.c @@ -0,0 +1,532 @@ +#ifdef HAVE_NBGL + +#include + +#include "nbgl_use_case.h" +#include "./display.h" +#include "./menu.h" +#include "io.h" + +typedef struct { + const char *confirm; // text displayed in last transaction page + const char *confirmed_status; // text displayed in confirmation page (after long press) + const char *rejected_status; // text displayed in rejection page (after reject confirmed) + nbgl_layoutTagValue_t tagValuePair[3]; + nbgl_layoutTagValueList_t tagValueList; + nbgl_pageInfoLongPress_t infoLongPress; + int extOutputCount; + int currentOutput; +} TransactionContext_t; + +enum { + CANCEL_TOKEN = 0, + CONFIRM_TOKEN, + SILENT_CONFIRM_TOKEN, + BACK_TOKEN_TRANSACTION, // for most transactions + BACK_TOKEN_SELFTRANSFER, // special case when it's a self-transfer (no external outputs) +}; + +extern bool G_was_processing_screen_shown; +static TransactionContext_t transactionContext; + +// ux_flow_response +static void ux_flow_response_false(void) { + set_ux_flow_response(false); +} + +static void ux_flow_response_true(void) { + set_ux_flow_response(true); +} + +static void ux_flow_response(bool confirm) { + if (confirm) { + ux_flow_response_true(); + } else { + ux_flow_response_false(); + } +} + +// Statuses +static void status_confirmation_callback(bool confirm) { + if (confirm) { + ux_flow_response_true(); + nbgl_useCaseStatus(transactionContext.confirmed_status, true, ui_menu_main); + } else { + ux_flow_response_false(); + nbgl_useCaseStatus(transactionContext.rejected_status, false, ui_menu_main); + } +} + +static void status_cancel(void) { + status_confirmation_callback(false); +} + +static void confirm_cancel(void) { + nbgl_useCaseConfirm("Reject transaction?", + "", + "Yes, Reject", + "Go back to transaction", + status_cancel); +} + +static void start_processing_callback(bool confirm) { + if (confirm) { + ux_flow_response_true(); + nbgl_useCaseSpinner("Processing"); + } else { + ux_flow_response_false(); + nbgl_useCaseStatus(transactionContext.rejected_status, false, ui_menu_main); + } +} + +static void transaction_confirm_callback(int token, uint8_t index) { + (void) index; + + switch (token) { + case CANCEL_TOKEN: + confirm_cancel(); + break; + case CONFIRM_TOKEN: + start_processing_callback(true); + break; + case SILENT_CONFIRM_TOKEN: + ux_flow_response(true); + break; + case BACK_TOKEN_TRANSACTION: + ui_accept_transaction_flow(false); + break; + case BACK_TOKEN_SELFTRANSFER: + ui_accept_transaction_flow(true); + break; + default: + PRINTF("Unhandled token : %d", token); + } +} + +// Continue callbacks +static void continue_light_notify_callback(void) { + transactionContext.tagValueList.pairs = transactionContext.tagValuePair; + + transactionContext.infoLongPress.icon = &C_Bitcoin_64px; + transactionContext.infoLongPress.longPressText = "Approve"; + transactionContext.infoLongPress.longPressToken = CONFIRM_TOKEN; + transactionContext.infoLongPress.tuneId = TUNE_TAP_CASUAL; + transactionContext.infoLongPress.text = transactionContext.confirm; + + nbgl_useCaseStaticReviewLight(&transactionContext.tagValueList, + &transactionContext.infoLongPress, + "Cancel", + status_confirmation_callback); +} + +static void continue_light_processing_callback(void) { + transactionContext.tagValueList.pairs = transactionContext.tagValuePair; + + transactionContext.infoLongPress.icon = &C_Bitcoin_64px; + transactionContext.infoLongPress.longPressText = "Approve"; + transactionContext.infoLongPress.longPressToken = CONFIRM_TOKEN; + transactionContext.infoLongPress.tuneId = TUNE_TAP_CASUAL; + transactionContext.infoLongPress.text = transactionContext.confirm; + + nbgl_useCaseStaticReviewLight(&transactionContext.tagValueList, + &transactionContext.infoLongPress, + "Cancel", + start_processing_callback); +} + +static void continue_callback(void) { + transactionContext.tagValueList.pairs = transactionContext.tagValuePair; + + transactionContext.infoLongPress.icon = &C_Bitcoin_64px; + transactionContext.infoLongPress.longPressText = "Approve"; + transactionContext.infoLongPress.longPressToken = CONFIRM_TOKEN; + transactionContext.infoLongPress.tuneId = TUNE_TAP_CASUAL; + transactionContext.infoLongPress.text = transactionContext.confirm; + + nbgl_useCaseStaticReview(&transactionContext.tagValueList, + &transactionContext.infoLongPress, + "Cancel", + start_processing_callback); +} + +// Transaction flow +static void transaction_confirm(int token, uint8_t index) { + (void) index; + + // If it's a self-transfer, the UX is slightly different + int backToken = + transactionContext.extOutputCount == 0 ? BACK_TOKEN_SELFTRANSFER : BACK_TOKEN_TRANSACTION; + + if (token == CONFIRM_TOKEN) { + nbgl_pageNavigationInfo_t info = {.activePage = transactionContext.extOutputCount + 1, + .nbPages = transactionContext.extOutputCount + 2, + .navType = NAV_WITH_TAP, + .progressIndicator = true, + .navWithTap.backButton = true, + .navWithTap.backToken = backToken, + .navWithTap.nextPageText = NULL, + .navWithTap.quitText = "Reject transaction", + .quitToken = CANCEL_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; + + nbgl_pageContent_t content = {.type = INFO_LONG_PRESS, + .infoLongPress.icon = &C_Bitcoin_64px, + .infoLongPress.text = transactionContext.confirm, + .infoLongPress.longPressText = "Hold to sign", + .infoLongPress.longPressToken = CONFIRM_TOKEN, + .infoLongPress.tuneId = TUNE_TAP_NEXT}; + + nbgl_pageDrawGenericContent(&transaction_confirm_callback, &info, &content); + nbgl_refresh(); + } else { + confirm_cancel(); + } +} + +void ui_accept_transaction_flow(bool is_self_transfer) { + if (!is_self_transfer) { + transactionContext.tagValuePair[0].item = "Fees"; + transactionContext.tagValuePair[0].value = g_ui_state.validate_transaction.fee; + + transactionContext.tagValueList.nbPairs = 1; + } else { + transactionContext.tagValuePair[0].item = "Amount"; + transactionContext.tagValuePair[0].value = "Self-transfer"; + transactionContext.tagValuePair[1].item = "Fees"; + transactionContext.tagValuePair[1].value = g_ui_state.validate_transaction.fee; + + transactionContext.tagValueList.nbPairs = 2; + } + + transactionContext.confirm = "Sign transaction\nto send Bitcoin?"; + transactionContext.confirmed_status = "TRANSACTION\nSIGNED"; + transactionContext.rejected_status = "Transaction rejected"; + + nbgl_pageNavigationInfo_t info = {.activePage = transactionContext.extOutputCount, + .nbPages = transactionContext.extOutputCount + 2, + .navType = NAV_WITH_TAP, + .progressIndicator = true, + .navWithTap.backButton = false, + .navWithTap.nextPageText = "Tap to continue", + .navWithTap.nextPageToken = CONFIRM_TOKEN, + .navWithTap.quitText = "Reject transaction", + .quitToken = CANCEL_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; + + nbgl_pageContent_t content = {.type = TAG_VALUE_LIST, + .tagValueList.nbPairs = transactionContext.tagValueList.nbPairs, + .tagValueList.pairs = transactionContext.tagValuePair}; + + nbgl_pageDrawGenericContent(&transaction_confirm, &info, &content); + nbgl_refresh(); +} + +void ui_display_transaction_prompt(const int external_outputs_total_count) { + transactionContext.currentOutput = 0; + transactionContext.extOutputCount = external_outputs_total_count; + + transactionContext.rejected_status = "Transaction rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Review transaction\nto send Bitcoin", + "", + "Reject transaction", + ux_flow_response_true, + confirm_cancel); +} + +// Display outputs +static void display_output(void) { + transactionContext.rejected_status = "Transaction rejected"; + + nbgl_pageNavigationInfo_t info = {.activePage = transactionContext.currentOutput - 1, + .nbPages = transactionContext.extOutputCount + 2, + .navType = NAV_WITH_TAP, + .progressIndicator = true, + .navWithTap.backButton = false, + .navWithTap.nextPageText = "Tap to continue", + .navWithTap.nextPageToken = SILENT_CONFIRM_TOKEN, + .navWithTap.quitText = "Reject transaction", + .quitToken = CANCEL_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; + + nbgl_pageContent_t content = {.type = TAG_VALUE_LIST, + .tagValueList.nbMaxLinesForValue = 8, + .tagValueList.nbPairs = transactionContext.tagValueList.nbPairs, + .tagValueList.pairs = transactionContext.tagValuePair}; + + nbgl_pageDrawGenericContent(&transaction_confirm_callback, &info, &content); + nbgl_refresh(); +} + +void ui_display_output_address_amount_flow(int index) { + snprintf(g_ui_state.validate_output.index, + sizeof(g_ui_state.validate_output.index), + "#%d", + index); + + transactionContext.currentOutput++; + + transactionContext.tagValuePair[0].item = "Output"; + transactionContext.tagValuePair[0].value = g_ui_state.validate_output.index; + + transactionContext.tagValuePair[1].item = "Amount"; + transactionContext.tagValuePair[1].value = g_ui_state.validate_output.amount; + + transactionContext.tagValuePair[2].item = "Address"; + transactionContext.tagValuePair[2].value = g_ui_state.validate_output.address_or_description; + + transactionContext.tagValueList.nbPairs = 3; + + display_output(); +} + +void ui_display_output_address_amount_no_index_flow(int index) { + (void) index; + transactionContext.currentOutput++; + + transactionContext.tagValuePair[0].item = "Amount"; + transactionContext.tagValuePair[0].value = g_ui_state.validate_output.amount; + + transactionContext.tagValuePair[1].item = "Address"; + transactionContext.tagValuePair[1].value = g_ui_state.validate_output.address_or_description; + + transactionContext.tagValueList.nbPairs = 2; + + display_output(); +} + +// Continue light notify callback +void ui_display_pubkey_flow(void) { + transactionContext.tagValuePair[0].item = "Path"; + transactionContext.tagValuePair[0].value = g_ui_state.path_and_pubkey.bip32_path_str; + + transactionContext.tagValuePair[1].item = "Public key"; + transactionContext.tagValuePair[1].value = g_ui_state.path_and_pubkey.pubkey; + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Approve public key"; + transactionContext.confirmed_status = "PUBLIC KEY\nAPPROVED"; + transactionContext.rejected_status = "Public key rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Confirm public key", + "", + "Cancel", + continue_light_notify_callback, + status_cancel); +} + +void ui_display_receive_in_wallet_flow(void) { + transactionContext.tagValuePair[0].item = "Wallet name"; + transactionContext.tagValuePair[0].value = g_ui_state.wallet.wallet_name; + + transactionContext.tagValuePair[1].item = "Wallet Address"; + transactionContext.tagValuePair[1].value = g_ui_state.wallet.address; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Confirm address"; + transactionContext.confirmed_status = "ADDRESS\nCONFIRMED"; + transactionContext.rejected_status = "Address rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Receive\nin known wallet", + "", + "Cancel", + continue_light_notify_callback, + status_cancel); +} + +void ui_display_policy_map_cosigner_pubkey_flow(void) { + transactionContext.tagValuePair[0].item = "Index"; + transactionContext.tagValuePair[0].value = g_ui_state.cosigner_pubkey_and_index.signer_index; + + transactionContext.tagValuePair[1].item = "Public key"; + transactionContext.tagValuePair[1].value = g_ui_state.cosigner_pubkey_and_index.pubkey; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Confirm cosigner"; + transactionContext.confirmed_status = "COSIGNER\nREGISTERED"; + transactionContext.rejected_status = "Cosigner rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Register cosigner", + "", + "Cancel", + continue_light_notify_callback, + ux_flow_response_false); +} + +static void suspicious_pubkey_warning(void) { + nbgl_useCaseReviewStart(&C_round_warning_64px, + "WARNING", + "The derivation path\nis unusual", + "Cancel", + continue_light_notify_callback, + ux_flow_response_false); +} + +void ui_display_pubkey_suspicious_flow(void) { + transactionContext.tagValuePair[0].item = "Path"; + transactionContext.tagValuePair[0].value = g_ui_state.path_and_pubkey.bip32_path_str; + + transactionContext.tagValuePair[1].item = "Public key"; + transactionContext.tagValuePair[1].value = g_ui_state.path_and_pubkey.pubkey; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Approve public key"; + transactionContext.confirmed_status = "PUBLIC KEY\nAPPROVED"; + transactionContext.rejected_status = "Public key rejected"; + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Confirm public key", + "", + "Cancel", + suspicious_pubkey_warning, + status_cancel); +} + +// Continue light processing callback +void ui_display_register_wallet_flow(void) { + transactionContext.tagValuePair[0].item = "Name"; + transactionContext.tagValuePair[0].value = g_ui_state.wallet.wallet_name; + + transactionContext.tagValuePair[1].item = "Policy map"; + transactionContext.tagValuePair[1].value = g_ui_state.wallet.descriptor_template; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Register Wallet"; + transactionContext.confirmed_status = "WALLET\nREGISTERED"; + transactionContext.rejected_status = "Wallet rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Register wallet", + "", + "Cancel", + continue_light_processing_callback, + ux_flow_response_false); +} + +// Continue callback +void ui_sign_message_flow(void) { + transactionContext.tagValuePair[0].item = "Path"; + transactionContext.tagValuePair[0].value = g_ui_state.path_and_hash.bip32_path_str; + + transactionContext.tagValuePair[1].item = "Message hash"; + transactionContext.tagValuePair[1].value = g_ui_state.path_and_hash.hash_hex; + + transactionContext.tagValueList.nbPairs = 2; + + transactionContext.confirm = "Sign Message"; + transactionContext.confirmed_status = "MESSAGE\nSIGNED"; + transactionContext.rejected_status = "Message rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Confirm signature", + "", + "Cancel", + continue_callback, + ux_flow_response_false); +} + +void ui_display_spend_from_wallet_flow(void) { + transactionContext.tagValuePair[0].item = "Wallet name"; + transactionContext.tagValuePair[0].value = g_ui_state.wallet.wallet_name; + + transactionContext.tagValueList.nbPairs = 1; + + transactionContext.confirm = "Confirm wallet name"; + transactionContext.confirmed_status = "WALLET NAME\nCONFIRMED"; + transactionContext.rejected_status = "Wallet name rejected"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Spend from\nknown wallet", + "", + "Cancel", + continue_callback, + ux_flow_response_false); +} + +// Address flow +static void address_display(void) { + nbgl_useCaseAddressConfirmation(g_ui_state.wallet.address, status_confirmation_callback); +} + +void ui_display_default_wallet_address_flow(void) { + transactionContext.confirm = "Confirm address"; + transactionContext.confirmed_status = "ADDRESS\nVERIFIED"; + transactionContext.rejected_status = "Address verification\ncancelled"; + + nbgl_useCaseReviewStart(&C_Bitcoin_64px, + "Verify Bitcoin\naddress", + "", + "Cancel", + address_display, + status_cancel); +} + +// Warning Flows +void ui_display_warning_external_inputs_flow(void) { + nbgl_useCaseChoice(&C_round_warning_64px, + "Warning", + "There are external inputs", + "Continue", + "Reject if not sure", + ux_flow_response); +} + +void ui_display_unverified_segwit_inputs_flows(void) { + nbgl_useCaseChoice(&C_round_warning_64px, + "Warning", + "Unverified inputs\nUpdate Ledger Live or\nthird party wallet software", + "Continue", + "Reject if not sure", + ux_flow_response); +} + +void ui_display_nondefault_sighash_flow(void) { + nbgl_useCaseChoice(&C_round_warning_64px, + "Warning", + "Non-default sighash", + "Continue", + "Reject if not sure", + ux_flow_response); +} + +// Statuses +void ui_display_post_processing_confirm_message(bool success) { + if (success) { + nbgl_useCaseStatus("MESSAGE\nSIGNED", true, ux_flow_response_true); + } else { + nbgl_useCaseStatus("Message rejected", false, ux_flow_response_false); + } +} + +void ui_display_post_processing_confirm_wallet_registation(bool success) { + if (success) { + nbgl_useCaseStatus("WALLET\nREGISTERED", true, ux_flow_response_true); + } else { + nbgl_useCaseStatus("Wallet rejected", false, ux_flow_response_false); + } +} + +void ui_display_post_processing_confirm_transaction(bool success) { + if (success) { + nbgl_useCaseStatus("TRANSACTION\nSIGNED", true, ux_flow_response_true); + } else { + nbgl_useCaseStatus("Transaction rejected", false, ux_flow_response_false); + } +} + +void ui_display_post_processing_confirm_wallet_spend(bool success) { + if (success) { + nbgl_useCaseStatus("WALLET NAME\nCONFIRMED", true, ux_flow_response_true); + } else { + nbgl_useCaseStatus("Wallet name rejected", false, ux_flow_response_false); + } +} + +#endif // HAVE_NBGL diff --git a/src/ui/display_utils.c b/src/ui/display_utils.c index f19d8f5c..30ce98c1 100644 --- a/src/ui/display_utils.c +++ b/src/ui/display_utils.c @@ -52,7 +52,7 @@ void format_sats_amount(const char *coin_name, uint64_t amount, char out[static MAX_AMOUNT_LENGTH + 1]) { size_t coin_name_len = strlen(coin_name); - strcpy(out, coin_name); + strncpy(out, coin_name, MAX_AMOUNT_LENGTH + 1); out[coin_name_len] = ' '; char *amount_str = out + coin_name_len + 1; diff --git a/src/ui/menu.c b/src/ui/menu.c index 0944dba9..9f446217 100644 --- a/src/ui/menu.c +++ b/src/ui/menu.c @@ -21,64 +21,13 @@ #include "../globals.h" #include "menu.h" -// We have a screen with the icon and "Syscoin is ready" for Syscoin, -// "Syscoin Testnet is ready" for Syscoin Testnet. -UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_syscoin_logo, "Syscoin", "is ready"}); -UX_STEP_NOCB(ux_menu_ready_step_syscoin_regtest, - pnn, - {&C_syscoin_logo, "Syscoin Testnet", "is ready"}); - -UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); -UX_STEP_CB(ux_menu_about_step, pb, ui_menu_about(), {&C_icon_certificate, "About"}); -UX_STEP_VALID(ux_menu_exit_step, pb, os_sched_exit(-1), {&C_icon_dashboard_x, "Quit"}); - -// FLOW for the main menu (for bitcoin): -// #1 screen: ready -// #2 screen: version of the app -// #3 screen: about submenu -// #4 screen: quit -UX_FLOW(ux_menu_main_flow_bitcoin, - &ux_menu_ready_step_bitcoin, - &ux_menu_version_step, - &ux_menu_about_step, - &ux_menu_exit_step, - FLOW_LOOP); - -// FLOW for the main menu (for bitcoin testnet): -// #1 screen: ready -// #2 screen: version of the app -// #3 screen: about submenu -// #4 screen: quit -UX_FLOW(ux_menu_main_flow_syscoin_regtest, - &ux_menu_ready_step_syscoin_regtest, - &ux_menu_version_step, - &ux_menu_about_step, - &ux_menu_exit_step, - FLOW_LOOP); - #define BIP32_PUBKEY_VERSION_MAINNET 0x0488B21E #define BIP32_PUBKEY_VERSION_TESTNET 0x043587CF void ui_menu_main() { - if (G_ux.stack_count == 0) { - ux_stack_push(); - } - if (BIP32_PUBKEY_VERSION == BIP32_PUBKEY_VERSION_MAINNET) { // mainnet - ux_flow_init(0, ux_menu_main_flow_bitcoin, NULL); + ui_menu_main_flow_bitcoin(); } else if (BIP32_PUBKEY_VERSION == BIP32_PUBKEY_VERSION_TESTNET) { // testnet - ux_flow_init(0, ux_menu_main_flow_syscoin_regtest, NULL); + ui_menu_main_flow_bitcoin_testnet(); } } - -UX_STEP_NOCB(ux_menu_info_step, bn, {"Syscoin App", "(c) 2023 Ledger"}); -UX_STEP_CB(ux_menu_back_step, pb, ui_menu_main(), {&C_icon_back, "Back"}); - -// FLOW for the about submenu: -// #1 screen: app info -// #2 screen: back button to main menu -UX_FLOW(ux_menu_about_flow, &ux_menu_info_step, &ux_menu_back_step, FLOW_LOOP); - -void ui_menu_about() { - ux_flow_init(0, ux_menu_about_flow, NULL); -} diff --git a/src/ui/menu.h b/src/ui/menu.h index d9c69e08..6c3568b8 100644 --- a/src/ui/menu.h +++ b/src/ui/menu.h @@ -1,7 +1,7 @@ #pragma once /** - * Show main menu (ready screen, version, about, quit). + * Entry point function to show main menu (ready screen, version, about, quit). */ void ui_menu_main(void); @@ -9,3 +9,13 @@ void ui_menu_main(void); * Show about submenu (copyright, date). */ void ui_menu_about(void); + +/** + * Show main menu (ready screen, version, about, quit). + */ +void ui_menu_main_flow_bitcoin(void); + +/** + * Show main menu for Testnet (ready screen, version, about, quit). + */ +void ui_menu_main_flow_bitcoin_testnet(void); diff --git a/src/ui/menu_bagl.c b/src/ui/menu_bagl.c new file mode 100644 index 00000000..59d8d22f --- /dev/null +++ b/src/ui/menu_bagl.c @@ -0,0 +1,87 @@ +/***************************************************************************** + * Ledger App Syscoin. + * (c) 2021 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#ifdef HAVE_BAGL +#include "os.h" +#include "ux.h" + +#include "../globals.h" +#include "menu.h" + +// We have a screen with the icon and "Syscoin is ready" for Syscoin, +// "Syscoin Testnet is ready" for Syscoin Testnet. +UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_bitcoin_logo, "Syscoin", "is ready"}); +UX_STEP_NOCB(ux_menu_ready_step_bitcoin_testnet, + pnn, + {&C_bitcoin_logo, "Syscoin Testnet", "is ready"}); + +UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); +UX_STEP_CB(ux_menu_about_step, pb, ui_menu_about(), {&C_icon_certificate, "About"}); +UX_STEP_VALID(ux_menu_exit_step, pb, os_sched_exit(-1), {&C_icon_dashboard_x, "Quit"}); + +// FLOW for the main menu (for Syscoin): +// #1 screen: ready +// #2 screen: version of the app +// #3 screen: about submenu +// #4 screen: quit +UX_FLOW(ux_menu_main_flow_bitcoin, + &ux_menu_ready_step_bitcoin, + &ux_menu_version_step, + &ux_menu_about_step, + &ux_menu_exit_step, + FLOW_LOOP); + +// FLOW for the main menu (for Syscoin testnet): +// #1 screen: ready +// #2 screen: version of the app +// #3 screen: about submenu +// #4 screen: quit +UX_FLOW(ux_menu_main_flow_bitcoin_testnet, + &ux_menu_ready_step_bitcoin_testnet, + &ux_menu_version_step, + &ux_menu_about_step, + &ux_menu_exit_step, + FLOW_LOOP); + +UX_STEP_NOCB(ux_menu_info_step, bn, {"Syscoin App", "(c) 2023 Ledger"}); +UX_STEP_CB(ux_menu_back_step, pb, ui_menu_main(), {&C_icon_back, "Back"}); + +// FLOW for the about submenu: +// #1 screen: app info +// #2 screen: back button to main menu +UX_FLOW(ux_menu_about_flow, &ux_menu_info_step, &ux_menu_back_step, FLOW_LOOP); + +void ui_menu_main_flow_bitcoin(void) { + if (G_ux.stack_count == 0) { + ux_stack_push(); + } + + ux_flow_init(0, ux_menu_main_flow_bitcoin, NULL); +} + +void ui_menu_main_flow_bitcoin_testnet(void) { + if (G_ux.stack_count == 0) { + ux_stack_push(); + } + + ux_flow_init(0, ux_menu_main_flow_bitcoin_testnet, NULL); +} + +void ui_menu_about(void) { + ux_flow_init(0, ux_menu_about_flow, NULL); +} +#endif // HAVE_BAGL diff --git a/src/ui/menu_nbgl.c b/src/ui/menu_nbgl.c new file mode 100644 index 00000000..da3aeb0e --- /dev/null +++ b/src/ui/menu_nbgl.c @@ -0,0 +1,60 @@ +/***************************************************************************** + * Ledger App Syscoin. + * (c) 2021 Ledger SAS. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +#ifdef HAVE_NBGL +#include "nbgl_use_case.h" + +#include "../globals.h" +#include "menu.h" + +static const char* const infoTypes[] = {"Version", "Developer", "Copyright"}; +static const char* const infoContents[] = {APPVERSION, "Ledger", "(c) 2023 Ledger"}; + +static bool navigation_cb(uint8_t page, nbgl_pageContent_t* content) { + UNUSED(page); + content->type = INFOS_LIST; + content->infosList.nbInfos = 3; + content->infosList.infoTypes = (const char**) infoTypes; + content->infosList.infoContents = (const char**) infoContents; + return true; +} + +static void exit(void) { + os_sched_exit(-1); +} + +void ui_menu_main_flow_bitcoin(void) { + nbgl_useCaseHome("Syscoin", &C_Bitcoin_64px, NULL, false, ui_menu_about, exit); +} + +void ui_menu_main_flow_bitcoin_testnet(void) { + nbgl_useCaseHome("Syscoin Test", + &C_Bitcoin_64px, + "This app enables signing\ntransactions on all the Syscoin\ntest networks.", + false, + ui_menu_about, + exit); +} + +void ui_menu_about(void) { + nbgl_useCaseSettings("Syscoin", 0, 1, false, ui_menu_main, navigation_cb, NULL); +} + +void ui_menu_about_testnet(void) { + nbgl_useCaseSettings("Syscoin Test", 0, 1, false, ui_menu_main, navigation_cb, NULL); +} +#endif // HAVE_NBGL diff --git a/test_utils/requirements.txt b/test_utils/requirements.txt index cd006bce..c06bc9eb 100644 --- a/test_utils/requirements.txt +++ b/test_utils/requirements.txt @@ -1,2 +1,2 @@ mnemonic==0.20 -bip32>=3.3,<4.0 \ No newline at end of file +bip32>=3.4,<4.0 \ No newline at end of file diff --git a/tests/automations/register_wallet_accept.json b/tests/automations/register_wallet_accept.json index fa2d0db1..d96348a9 100644 --- a/tests/automations/register_wallet_accept.json +++ b/tests/automations/register_wallet_accept.json @@ -5,7 +5,16 @@ "regexp": "Register wallet|Wallet name|Wallet policy|Key", "actions": [ ["button", 2, true], - ["button", 2, false] + ["button", 2, false], + [ "finger", 55, 550, true], + [ "finger", 55, 550, false] + ] + }, + { + "regexp": "Tap to continue", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] ] }, { @@ -14,7 +23,14 @@ [ "button", 1, true ], [ "button", 2, true ], [ "button", 2, false ], - [ "button", 1, false ] + [ "button", 1, false ], + [ "finger", 55, 550, true] + ] + }, + { + "regexp": "Processing|REGISTERED", + "actions": [ + ["finger", 55, 550, false] ] } ] diff --git a/tests/automations/register_wallet_reject.json b/tests/automations/register_wallet_reject.json index 0d0a133e..f6d92812 100644 --- a/tests/automations/register_wallet_reject.json +++ b/tests/automations/register_wallet_reject.json @@ -2,10 +2,12 @@ "version": 1, "rules": [ { - "regexp": "Register wallet|Wallet name|Wallet policy|Key|Approve", + "regexp": "Register wallet|Wallet name|Wallet policy|Key|Approve|Cancel", "actions": [ ["button", 2, true], - ["button", 2, false] + ["button", 2, false], + ["finger", 55, 650, true], + ["finger", 55, 650, false] ] }, { diff --git a/tests/automations/sign_message_accept.json b/tests/automations/sign_message_accept.json index e05cd683..b3a99b1a 100644 --- a/tests/automations/sign_message_accept.json +++ b/tests/automations/sign_message_accept.json @@ -8,6 +8,25 @@ ["button", 2, false] ] }, + { + "regexp": "Tap to continue", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, + { + "regexp": "Approve", + "actions": [ + [ "finger", 55, 550, true] + ] + }, + { + "regexp": "Processing", + "actions": [ + ["finger", 55, 550, false] + ] + }, { "regexp": "[S]?ign", "conditions": [ diff --git a/tests/automations/sign_message_reject.json b/tests/automations/sign_message_reject.json index fe217ba2..f59c7e40 100644 --- a/tests/automations/sign_message_reject.json +++ b/tests/automations/sign_message_reject.json @@ -1,11 +1,20 @@ { "version": 1, "rules": [ + { + "regexp": "Tap to continue", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, { "regexp": "[S]?ign|Path|Message hash|Approve", "actions": [ ["button", 2, true], - ["button", 2, false] + ["button", 2, false], + ["finger", 55, 650, true], + ["finger", 55, 650, false] ] }, { diff --git a/tests/automations/sign_with_default_wallet_accept.json b/tests/automations/sign_with_default_wallet_accept.json index 09352e5f..128b4deb 100644 --- a/tests/automations/sign_with_default_wallet_accept.json +++ b/tests/automations/sign_with_default_wallet_accept.json @@ -1,6 +1,16 @@ { "version": 1, "rules": [ + { + "regexp": "Approve|Accept|Hold to sign", + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 2, false ], + [ "button", 1, false ], + [ "finger", 55, 550, true] + ] + }, { "regexp": "Review|Amount|Address|Confirm|Fees", "actions": [ @@ -9,12 +19,16 @@ ] }, { - "regexp": "Approve|Accept", + "regexp": "Tap to continue", "actions": [ - [ "button", 1, true ], - [ "button", 2, true ], - [ "button", 2, false ], - [ "button", 1, false ] + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, + { + "regexp": "SIGNED", + "actions": [ + ["finger", 55, 550, false] ] } ] diff --git a/tests/automations/sign_with_default_wallet_accept_nondefault_sighash.json b/tests/automations/sign_with_default_wallet_accept_nondefault_sighash.json index 5acd997b..2833c434 100644 --- a/tests/automations/sign_with_default_wallet_accept_nondefault_sighash.json +++ b/tests/automations/sign_with_default_wallet_accept_nondefault_sighash.json @@ -9,12 +9,26 @@ ] }, { - "regexp": "Approve|Accept|Continue", + "regexp": "Tap to continue|Warning", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, + { + "regexp": "Hold|Accept|Continue|Approve", "actions": [ [ "button", 1, true ], [ "button", 2, true ], [ "button", 2, false ], - [ "button", 1, false ] + [ "button", 1, false ], + [ "finger", 55, 550, true] + ] + }, + { + "regexp": "SIGNED", + "actions": [ + ["finger", 55, 550, false] ] } ] diff --git a/tests/automations/sign_with_default_wallet_missing_nonwitnessutxo_accept.json b/tests/automations/sign_with_default_wallet_missing_nonwitnessutxo_accept.json index 5875f259..09093e2c 100644 --- a/tests/automations/sign_with_default_wallet_missing_nonwitnessutxo_accept.json +++ b/tests/automations/sign_with_default_wallet_missing_nonwitnessutxo_accept.json @@ -2,19 +2,33 @@ "version": 1, "rules": [ { - "regexp": "Unverified|Update|or third party|Review|Amount|Address|Confirm|Fees", + "regexp": "Hold to sign", "actions": [ - ["button", 2, true], - ["button", 2, false] + ["finger", 55, 550, true] + ] + }, + { + "regexp": "Processing", + "actions": [ + ["finger", 55, 550, false] ] }, { - "regexp": "Continue|Approve|Accept", + "regexp": "Continue|Tap to continue|Approve|Accept|Warning", "actions": [ [ "button", 1, true ], [ "button", 2, true ], [ "button", 2, false ], - [ "button", 1, false ] + [ "button", 1, false ], + [ "finger", 55, 550, true], + [ "finger", 55, 550, false] + ] + }, + { + "regexp": "Unverified|Update|or third party|Review|Amount|Address|Confirm|Fees", + "actions": [ + ["button", 2, true], + ["button", 2, false] ] } ] diff --git a/tests/automations/sign_with_wallet_accept.json b/tests/automations/sign_with_wallet_accept.json index 0dfed646..07c63686 100644 --- a/tests/automations/sign_with_wallet_accept.json +++ b/tests/automations/sign_with_wallet_accept.json @@ -9,12 +9,32 @@ ] }, { - "regexp": "Approve|Accept", + "regexp": "Hold to sign", + "actions": [ + ["finger", 55, 550, true] + ] + }, + { + "regexp": "Tap to continue", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, + { + "regexp": "Approve|Accept|Confirm", "actions": [ [ "button", 1, true ], [ "button", 2, true ], [ "button", 2, false ], - [ "button", 1, false ] + [ "button", 1, false ], + ["finger", 55, 550, true] + ] + }, + { + "regexp": "Processing", + "actions": [ + ["finger", 55, 550, false] ] } ] diff --git a/tests/automations/sign_with_wallet_external_inputs_accept.json b/tests/automations/sign_with_wallet_external_inputs_accept.json index 7a7288bc..8111c798 100644 --- a/tests/automations/sign_with_wallet_external_inputs_accept.json +++ b/tests/automations/sign_with_wallet_external_inputs_accept.json @@ -2,19 +2,39 @@ "version": 1, "rules": [ { - "regexp": "[S]?pend from|Wallet name|There are|Reject if you're|Review|Amount|Address|Confirm|Fees", + "regexp": "Hold to sign", + "actions": [ + ["finger", 55, 550, true] + ] + }, + { + "regexp": "[S]?pend from|Wallet name|There are|Reject if you['-]re|Review|Amount|Address|Confirm|Fees", "actions": [ ["button", 2, true], ["button", 2, false] ] }, + { + "regexp": "Tap to continue|Warning", + "actions": [ + ["finger", 55, 550, true], + ["finger", 55, 550, false] + ] + }, { "regexp": "Continue|Approve|Accept", "actions": [ [ "button", 1, true ], [ "button", 2, true ], [ "button", 2, false ], - [ "button", 1, false ] + [ "button", 1, false ], + [ "finger", 55, 550, true] + ] + }, + { + "regexp": "SIGNED", + "actions": [ + ["finger", 55, 550, false] ] } ] diff --git a/tests/automations/sign_with_wallet_missing_nonwitnessutxo_accept.json b/tests/automations/sign_with_wallet_missing_nonwitnessutxo_accept.json new file mode 100644 index 00000000..5717357c --- /dev/null +++ b/tests/automations/sign_with_wallet_missing_nonwitnessutxo_accept.json @@ -0,0 +1,45 @@ +{ + "version": 1, + "rules": [ + { + "regexp": "Hold to sign|Confirm wallet name", + "actions": [ + ["finger", 55, 550, true] + ] + }, + { + "regexp": "Processing", + "actions": [ + ["finger", 55, 550, false] + ] + }, + { + "text": "Approve", + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 2, false ], + [ "button", 1, false ], + [ "finger", 55, 550, true] + ] + }, + { + "regexp": "Continue|Tap to continue|Accept|Warning", + "actions": [ + [ "button", 1, true ], + [ "button", 2, true ], + [ "button", 2, false ], + [ "button", 1, false ], + [ "finger", 55, 550, true], + [ "finger", 55, 550, false] + ] + }, + { + "regexp": "Unverified|Update|or third party|[S]?pend from|Wallet name|Review|Amount|Address|Confirm|Fees", + "actions": [ + ["button", 2, true], + ["button", 2, false] + ] + } + ] +} diff --git a/tests/requirements.txt b/tests/requirements.txt index 58945f42..23a57de0 100644 --- a/tests/requirements.txt +++ b/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/tests/skip_until_poda_test_e2e_miniscript.py b/tests/skip_until_poda_test_e2e_miniscript.py index 8d3b67ce..a7bb09d8 100644 --- a/tests/skip_until_poda_test_e2e_miniscript.py +++ b/tests/skip_until_poda_test_e2e_miniscript.py @@ -83,12 +83,8 @@ def run_test_e2e(wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: result = multisig_rpc.walletcreatefundedpsbt( outputs={ out_address: Decimal("0.01") - }, - options={ - # make sure that the fee is large enough; it looks like - # fee estimation doesn't work in core with miniscript, yet - "fee_rate": 10 - }) + } + ) psbt_b64 = result["psbt"] @@ -279,6 +275,36 @@ def test_e2e_miniscript_me_or_3of5(rpc, rpc_test_wallet, client: Client, speculo run_test_e2e(wallet_policy, [], rpc, rpc_test_wallet, client, speculos_globals, comm) +def test_e2e_miniscript_me_large_vault(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient], model: str): + if (model == "nanos"): + pytest.skip("Not supported on Nano S due to memory limitations") + + path = "48'/1'/0'/2'" + _, core_xpub_orig1 = create_new_wallet() + _, core_xpub_orig2 = create_new_wallet() + _, core_xpub_orig3 = create_new_wallet() + _, core_xpub_orig4 = create_new_wallet() + _, core_xpub_orig5 = create_new_wallet() + _, core_xpub_orig6 = create_new_wallet() + internal_xpub = get_internal_xpub(speculos_globals.seed, path) + internal_xpub_orig = f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}" + + wallet_policy = WalletPolicy( + name="Large vault", + descriptor_template="wsh(or_d(pk(@0/**),andor(thresh(1,utv:thresh(1,pkh(@1/**),a:pkh(@2/**)),autv:thresh(1,pkh(@3/**),a:pkh(@4/**))),after(1685577600),and_v(v:and_v(v:pkh(@5/**),thresh(1,pkh(@6/**))),after(1685318400)))))", + keys_info=[ + internal_xpub_orig, + f"{core_xpub_orig1}", + f"{core_xpub_orig2}", + f"{core_xpub_orig3}", + f"{core_xpub_orig4}", + f"{core_xpub_orig5}", + f"{core_xpub_orig6}", + ]) + + run_test_e2e(wallet_policy, [], rpc, rpc_test_wallet, client, speculos_globals, comm) + + def test_e2e_miniscript_me_and_bob_or_me_and_carl_1(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient]): # policy: or(and(pk(A1), pk(B)),and(pk(A2), pk(C))) # where A1 and A2 are both internal keys; therefore, two signatures per input must be returned @@ -306,27 +332,32 @@ def test_e2e_miniscript_me_and_bob_or_me_and_carl_1(rpc, rpc_test_wallet, client run_test_e2e(wallet_policy, [core_wallet_name1], rpc, rpc_test_wallet, client, speculos_globals, comm) -def test_e2e_miniscript_me_and_bob_or_me_and_carl_2(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient]): - # same as the previous example, but uses the same xpubs with two different paths for the internal keys, - # by using the //* notation instead of different internal xpubs +def test_e2e_miniscript_policy_with_a(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient]): + # versions 2.1.0 and 2.1.1 of the app incorrectly compiled the 'a:' wrapper, producing incorrect addresses - core_wallet_name1, core_xpub_orig1 = create_new_wallet() + _, core_xpub_orig1 = create_new_wallet() _, core_xpub_orig2 = create_new_wallet() + core_wallet_name3, core_xpub_orig3 = create_new_wallet() + _, core_xpub_orig4 = create_new_wallet() + _, core_xpub_orig5 = create_new_wallet() - path = "44'/1'/0'" + path = "48'/1'/0'/2'" internal_xpub = get_internal_xpub(speculos_globals.seed, path) internal_xpub_orig = f"[{speculos_globals.master_key_fingerprint.hex()}/{path}]{internal_xpub}" wallet_policy = WalletPolicy( - name="Me and Bob or me and Carl", - descriptor_template="wsh(c:andor(pk(@0/<0;1>/*),pk_k(@1/**),and_v(v:pk(@0/<2;3>/*),pk_k(@2/**))))", + name="Policy with a:", + descriptor_template="wsh(or_i(and_v(v:pkh(@0/**),older(65535)),or_d(multi(2,@1/**,@3/**),and_v(v:thresh(1,pkh(@4/**),a:pkh(@5/**)),older(64231)))))", keys_info=[ - internal_xpub_orig, f"{core_xpub_orig1}", + internal_xpub_orig, f"{core_xpub_orig2}", + f"{core_xpub_orig3}", + f"{core_xpub_orig4}", + f"{core_xpub_orig5}", ]) - run_test_e2e(wallet_policy, [core_wallet_name1], rpc, rpc_test_wallet, client, speculos_globals, comm) + run_test_e2e(wallet_policy, [core_wallet_name3], rpc, rpc_test_wallet, client, speculos_globals, comm) def test_invalid_miniscript(rpc, client: Client, speculos_globals: SpeculosGlobals): diff --git a/tests/skip_until_poda_test_e2e_tapscripts.py b/tests/skip_until_poda_test_e2e_tapscripts.py index c616ece4..debaf4da 100644 --- a/tests/skip_until_poda_test_e2e_tapscripts.py +++ b/tests/skip_until_poda_test_e2e_tapscripts.py @@ -83,12 +83,8 @@ def run_test_e2e(wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: result = multisig_rpc.walletcreatefundedpsbt( outputs={ out_address: Decimal("0.01") - }, - options={ - # make sure that the fee is large enough; it looks like - # fee estimation doesn't work in core with miniscript, yet - "fee_rate": 10 - }) + } + ) psbt_b64 = result["psbt"] @@ -220,8 +216,8 @@ def test_e2e_tapscript_one_of_three_scriptpath(rpc, rpc_test_wallet, client: Cli rpc, rpc_test_wallet, client, speculos_globals, comm) -def test_e2e_tapscript_sortedmulti_a_2of2(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient]): - # tr(foreign_key_1,sortedmulti_a(2,my_key,foreign_key_2)) +def test_e2e_tapscript_multi_a_2of2(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient]): + # tr(foreign_key_1,multi_a(2,my_key,foreign_key_2)) path = "499'/1'/0'" _, core_xpub_orig_1 = create_new_wallet() @@ -260,7 +256,8 @@ def test_e2e_tapscript_depth4(rpc, rpc_test_wallet, client: Client, speculos_glo run_test_e2e(wallet_policy, [], rpc, rpc_test_wallet, client, speculos_globals, comm) -def test_e2e_tapscript_large(rpc, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient], model): +def test_e2e_tapscript_large(rpc, rpc_test_wallet, client: Client, speculos_globals: + SpeculosGlobals, comm: Union[TransportClient, SpeculosClient], model: str): # A quite large tapscript with 8 tapleaves and 22 keys in total. # Takes more memory than Nano S can handle diff --git a/tests/syscoin.conf b/tests/syscoin.conf index eb3c5574..a23ef3f7 100644 --- a/tests/syscoin.conf +++ b/tests/syscoin.conf @@ -17,3 +17,5 @@ debug=1 fallbackfee=0.00001 printtoconsole=1 assetindex=1 +minrelaytxfee=0 +blockmintxfee=0 diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 3c574815..0bf492fb 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -9,6 +9,9 @@ def test_dashboard(comm: SpeculosClient, is_speculos: bool, app_version: str, mo if not is_speculos: pytest.skip("Requires speculos") + if model == "stax": + pytest.skip("No dashboard test for stax") + comm.press_and_release("right") comm.wait_for_text_event("Version") comm.wait_for_text_event(app_version) diff --git a/tests/test_e2e_multisig.py b/tests/test_e2e_multisig.py index bbc695e2..7cc476be 100644 --- a/tests/test_e2e_multisig.py +++ b/tests/test_e2e_multisig.py @@ -82,12 +82,8 @@ def run_test(wallet_policy: WalletPolicy, core_wallet_names: List[str], rpc: Aut result = multisig_rpc.walletcreatefundedpsbt( outputs={ out_address: Decimal("0.01") - }, - options={ - # make sure that the fee is large enough; it looks like - # fee estimation doesn't work in core with miniscript, yet - "fee_rate": 10 - }) + } + ) psbt_b64 = result["psbt"] @@ -172,6 +168,7 @@ def test_e2e_multisig_multiple_internal_keys(rpc: AuthServiceProxy, rpc_test_wal rpc, rpc_test_wallet, client, speculos_globals, comm) +@pytest.mark.timeout(0) # disable timeout def test_e2e_multisig_16_of_16(rpc: AuthServiceProxy, rpc_test_wallet, client: Client, speculos_globals: SpeculosGlobals, comm: Union[TransportClient, SpeculosClient], enable_slow_tests: bool): # Largest supported multisig with sortedmulti. # The time for an end-to-end execution on a real Ledger Nano S (including user's input) is about 520 seconds. diff --git a/tests/test_get_extended_pubkey.py b/tests/test_get_extended_pubkey.py index f2f56737..48b2ba9b 100644 --- a/tests/test_get_extended_pubkey.py +++ b/tests/test_get_extended_pubkey.py @@ -7,6 +7,58 @@ from speculos.client import SpeculosClient +def test_get_extended_pubkey_standard_display(client: Client, comm: SpeculosClient, is_speculos: + bool, model: str): + testcases = { + "m/44'/1'/0'": "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + "m/44'/1'/10'": "tpubDCwYjpDhUdPGp21gSpVay2QPJVh6WNySWMXPhbcu1DsxH31dF7mY18oibbu5RxCLBc1Szerjscuc3D5HyvfYqfRvc9mesewnFqGmPjney4d", + "m/44'/1'/2'/1/42": "tpubDGF9YgHKv6qh777rcqVhpmDrbNzgophJM9ec7nHiSfrbss7fVBXoqhmZfohmJSvhNakDHAspPHjVVNL657tLbmTXvSeGev2vj5kzjMaeupT", + "m/48'/1'/4'/1'/0/7": "tpubDK8WPFx4WJo1R9mEL7Wq325wBiXvkAe8ipgb9Q1QBDTDUD2YeCfutWtzY88NPokZqJyRPKHLGwTNLT7jBG59aC6VH8q47LDGQitPB6tX2d7", + "m/49'/1'/1'/1/3": "tpubDGnetmJDCL18TyaaoyRAYbkSE9wbHktSdTS4mfsR6inC8c2r6TjdBt3wkqEQhHYPtXpa46xpxDaCXU2PRNUGVvDzAHPG6hHRavYbwAGfnFr", + "m/84'/1'/2'/0/10": "tpubDG9YpSUwScWJBBSrhnAT47NcT4NZGLcY18cpkaiWHnkUCi19EtCh8Heeox268NaFF6o56nVeSXuTyK6jpzTvV1h68Kr3edA8AZp27MiLUNt", + "m/86'/1'/4'/1/12": "tpubDHTZ815MvTaRmo6Qg1rnU6TEU4ZkWyA56jA1UgpmMcBGomnSsyo34EZLoctzZY9MTJ6j7bhccceUeXZZLxZj5vgkVMYfcZ7DNPsyRdFpS3f", + } + + if not is_speculos: + pytest.skip("Requires speculos") + + def ux_thread(): + event = comm.wait_for_text_event("Confirm public key") + + # press right until the last screen (will press the "right" button more times than needed) + while "Reject" != event["text"]: + comm.press_and_release("right") + + event = comm.get_next_event() + + # go back to the Accept screen, then accept + comm.press_and_release("left") + comm.press_and_release("both") + + def ux_thread_stax(): + event = comm.get_next_event() + + while "Approve public key" not in event["text"]: + if "Tap to continue" in event["text"]: + comm.finger_touch(55, 550) + + event = comm.get_next_event() + + comm.finger_touch(55, 550) + + for path, pubkey in testcases.items(): + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) + x.start() + assert pubkey == client.get_extended_pubkey( + path=path, + display=True + ) + x.join() + + def test_get_extended_pubkey_standard_nodisplay(client: Client): testcases = { "m/44'/1'/0'": "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", @@ -53,7 +105,8 @@ def test_get_extended_pubkey_nonstandard_nodisplay(client: Client): ) -def test_get_extended_pubkey_non_standard(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_get_extended_pubkey_non_standard(client: Client, comm: SpeculosClient, is_speculos: bool, + model: str): # Test the successful UX flow for a non-standard path (here, root path) # (Slow test, not feasible to repeat it for many paths) @@ -73,7 +126,20 @@ def ux_thread(): comm.press_and_release("left") comm.press_and_release("both") - x = threading.Thread(target=ux_thread) + def ux_thread_stax(): + event = comm.get_next_event() + while "Approve" not in event["text"]: + if "Tap to continue" in event["text"] or "Warning" in event["text"]: + comm.finger_touch(55, 550) + + event = comm.get_next_event() + + comm.finger_touch(55, 550) + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) x.start() pub_key = client.get_extended_pubkey( @@ -86,7 +152,8 @@ def ux_thread(): assert pub_key == "tpubD6NzVbkrYhZ4YgUx2ZLNt2rLYAMTdYysCRzKoLu2BeSHKvzqPaBDvf17GeBPnExUVPkuBpx4kniP964e2MxyzzazcXLptxLXModSVCVEV1T" -def test_get_extended_pubkey_non_standard_reject_early(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_get_extended_pubkey_non_standard_reject_early(client: Client, comm: SpeculosClient, + is_speculos: bool, model: str): # Test rejecting after the "Reject if you're not sure" warning # (Slow test, not feasible to repeat it for many paths) @@ -98,12 +165,32 @@ def ux_thread(): comm.press_and_release("right") comm.wait_for_text_event("Confirm public key") comm.press_and_release("right") - comm.wait_for_text_event("111'/222'/333'") + # Temporary fix for broken OCR + if (model == "nanox"): + comm.wait_for_text_event("111-/222-/333-") + else: + comm.wait_for_text_event("111'/222'/333'") + comm.press_and_release("right") comm.wait_for_text_event("not sure") # second line of "Reject if you're not sure" comm.press_and_release("both") - x = threading.Thread(target=ux_thread) + def ux_thread_stax(): + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Approve") + comm.finger_touch(55, 650) + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) x.start() with pytest.raises(DenyError): @@ -115,7 +202,8 @@ def ux_thread(): x.join() -def test_get_extended_pubkey_non_standard_reject(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_get_extended_pubkey_non_standard_reject(client: Client, comm: SpeculosClient, is_speculos: + bool, model: str): # Test rejecting at the end # (Slow test, not feasible to repeat it for many paths) @@ -134,7 +222,22 @@ def ux_thread(): # finally, reject comm.press_and_release("both") - x = threading.Thread(target=ux_thread) + def ux_thread_stax(): + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Tap to continue") + comm.finger_touch(55, 550) + comm.wait_for_text_event("Approve") + comm.finger_touch(55, 650) + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) x.start() with pytest.raises(DenyError): diff --git a/tests/test_get_wallet_address.py b/tests/test_get_wallet_address.py index 008e0cb5..9206db58 100644 --- a/tests/test_get_wallet_address.py +++ b/tests/test_get_wallet_address.py @@ -72,7 +72,17 @@ def test_get_wallet_address_singlesig_taproot(client: Client): # Failure cases for default wallets -def test_get_wallet_address_default_fail_wrongkeys(client: Client): +def test_get_wallet_address_fail_nonstandard(client: Client): + # Not empty name should be rejected + with pytest.raises(IncorrectDataError): + client.get_wallet_address(WalletPolicy( + name="Not empty", + descriptor_template="pkh(@0/**)", + keys_info=[ + f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + ], + ), None, 0, 0, False) + # 0 keys info should be rejected with pytest.raises(IncorrectDataError): client.get_wallet_address(WalletPolicy( @@ -132,6 +142,36 @@ def test_get_wallet_address_default_fail_wrongkeys(client: Client): ], ), None, 0, 100000, False) + # missing key origin info + with pytest.raises(IncorrectDataError): + client.get_wallet_address(WalletPolicy( + name="", + descriptor_template="pkh(@0/**)", + keys_info=[ + f"tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + ], + ), None, 0, 0, False) + + # non-standard final derivation steps + with pytest.raises(IncorrectDataError): + client.get_wallet_address(WalletPolicy( + name="", + descriptor_template="pkh(@0/<0;2>/*)", + keys_info=[ + f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + ], + ), None, 0, 0, False) + + # taproot single-sig with non-empty script + with pytest.raises(IncorrectDataError): + client.get_wallet_address(WalletPolicy( + name="", + descriptor_template="tr(@0,0)", + keys_info=[ + f"[f5acc2fd/86'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", + ], + ), None, 0, 0, False) + # Multisig @@ -231,3 +271,26 @@ def test_get_wallet_address_tr_script_sortedmulti(client: Client): res = client.get_wallet_address(wallet, wallet_hmac, 0, 0, False) assert res == "tb1pdzk72dnvz3246474p4m5a97u43h6ykt2qcjrrhk6y0fkg8hx2mvswwgvv7" + + +def test_get_wallet_address_large_addr_index(client: Client): + # 2**31 - 1 is the largest index allowed, per BIP-32 + + wallet = MultisigWallet( + name="Cold storage", + address_type=AddressType.WIT, + threshold=2, + keys_info=[ + "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF", + "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK", + ], + ) + wallet_hmac = bytes.fromhex( + "d7c7a60b4ab4a14c1bf8901ba627d72140b2fb907f2b4e35d2e693bce9fbb371" + ) + + client.get_wallet_address(wallet, wallet_hmac, 0, 2**31 - 1, False) + + # too large address_index, not allowed for an unhardened step + with pytest.raises(IncorrectDataError): + client.get_wallet_address(wallet, wallet_hmac, 0, 2**31, False) diff --git a/tests/test_get_wallet_address_v1.py b/tests/test_get_wallet_address_v1.py index deb68d79..2120aeec 100644 --- a/tests/test_get_wallet_address_v1.py +++ b/tests/test_get_wallet_address_v1.py @@ -3,11 +3,13 @@ from bitcoin_client.ledger_bitcoin import Client, AddressType, MultisigWallet, WalletPolicy, WalletType from bitcoin_client.ledger_bitcoin.exception.errors import IncorrectDataError +from speculos.client import SpeculosClient +import threading import pytest -# TODO: add tests with UI +# TODO: add more tests with UI def test_get_wallet_address_singlesig_legacy_v1(client: Client): @@ -207,3 +209,103 @@ def test_get_wallet_address_multisig_wit_v1(client: Client): res = client.get_wallet_address(wallet, wallet_hmac, 0, 0, False) assert res == "tb1qmyauyzn08cduzdqweexgna2spwd0rndj55fsrkefry2cpuyt4cpsn2pg28" + + +def test_get_wallet_address_singlesig_legacy_v1_ui(client: Client, comm: SpeculosClient, + is_speculos: bool, model: str): + # legacy address (P2PKH) + def ux_thread(): + event = comm.wait_for_text_event("Address") + + # press right until the last screen (will press the "right" button more times than needed) + while "Reject" != event["text"]: + comm.press_and_release("right") + + event = comm.get_next_event() + + # go back to the Accept screen, then accept + comm.press_and_release("left") + comm.press_and_release("both") + + def ux_thread_stax(): + while True: + event = comm.get_next_event() + if "Tap to continue" in event["text"] or "Show as QR" in event["text"]: + comm.finger_touch(55, 550) + elif "VERIFIED" in event["text"]: + break + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) + + wallet = WalletPolicy( + name="", + descriptor_template="pkh(@0)", + keys_info=[ + f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/**", + ], + version=WalletType.WALLET_POLICY_V1 + ) + x.start() + assert client.get_wallet_address(wallet, None, 0, 0, True) == "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm" + x.join() + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) + x.start() + assert client.get_wallet_address(wallet, None, 1, 15, True) == "myFCUBRCKFjV7292HnZtiHqMzzHrApobpT" + x.join() + + +def test_get_wallet_address_multisig_legacy_v1_ui(client: Client, comm: SpeculosClient, is_speculos: + bool, model: str): + # test for a legacy p2sh multisig wallet + + wallet = MultisigWallet( + name="Cold storage", + address_type=AddressType.LEGACY, + threshold=2, + keys_info=[ + f"[5c9e228d/48'/1'/0'/0']tpubDEGquuorgFNb8bjh5kNZQMPtABJzoWwNm78FUmeoPkfRtoPF7JLrtoZeT3J3ybq1HmC3Rn1Q8wFQ8J5usanzups5rj7PJoQLNyvq8QbJruW/**", + f"[f5acc2fd/48'/1'/0'/0']tpubDFAqEGNyad35WQAZMmPD4vgBXnjH16RGciLdWekPe4f4d5JzoHVu1PS86Sy4Tm63vDf8rfV3UjifhrRuSUDfiZj5KPffTPyZ4ZXBKvjD8jm/**", + ], + version=WalletType.WALLET_POLICY_V1 + ) + wallet_hmac = bytes.fromhex( + "1980a07cde99fbdec0d487671d3bb296507e47b3ddfa778600a9d73d501983bc" + ) + + def ux_thread(): + event = comm.wait_for_text_event("Receive") + + # press right until the last screen (will press the "right" button more times than needed) + while "Reject" != event["text"]: + comm.press_and_release("right") + + event = comm.get_next_event() + + # go back to the Accept screen, then accept + comm.press_and_release("left") + comm.press_and_release("both") + + def ux_thread_stax(): + while True: + event = comm.get_next_event() + if "Tap to continue" in event["text"] or "Confirm" in event["text"]: + comm.finger_touch(55, 550) + elif "CONFIRMED" in event["text"]: + break + + if model == "stax": + x = threading.Thread(target=ux_thread_stax) + else: + x = threading.Thread(target=ux_thread) + + x.start() + res = client.get_wallet_address(wallet, wallet_hmac, 0, 0, True) + x.join() + assert res == "2Mx69MjHC4ViZAH1koVXPvVgaazbBCdr89j" diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 00000000..e42bc37e --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,33 @@ +import pytest + +from bitcoin_client.ledger_bitcoin import Client +from bitcoin_client.ledger_bitcoin.client_base import ApduException +from bitcoin_client.ledger_bitcoin.command_builder import BitcoinCommandBuilder, BitcoinInsType, CURRENT_PROTOCOL_VERSION + + +def test_high_p1_allowed(client: Client): + # We reserve p1 for feature flags, so non-zero bits shouldn't be rejected + # for forward-compatibility; this allows graceful degradation for optional features. + + # We can't use the client to send this apdu, so we use raw transport. + # We're only testing that no exception is raised. + client.transport_client.apdu_exchange( + cla=BitcoinCommandBuilder.CLA_BITCOIN, + ins=BitcoinInsType.GET_MASTER_FINGERPRINT, + p1=0xff, + p2=CURRENT_PROTOCOL_VERSION, + data=b'' + ) + + +def test_p2_too_high(client: Client): + # Tests that sending a p2 > CURRENT_PROTOCOL_VERSION fails with 0x6a86 (WRONG_P1P2) + with pytest.raises(ApduException, match="Exception: invalid status 0x6a86"): + # We can't use the client to send this apdu, so we use raw transport + client.transport_client.apdu_exchange( + cla=BitcoinCommandBuilder.CLA_BITCOIN, + ins=BitcoinInsType.GET_MASTER_FINGERPRINT, + p1=0, + p2=CURRENT_PROTOCOL_VERSION + 1, + data=b'' + ) diff --git a/tests/test_register_wallet.py b/tests/test_register_wallet.py index 6901e8c9..a8988e20 100644 --- a/tests/test_register_wallet.py +++ b/tests/test_register_wallet.py @@ -117,6 +117,21 @@ def test_register_wallet_invalid_names(client: Client): client.register_wallet(wallet) +@has_automation("automations/register_wallet_accept.json") +def test_register_wallet_missing_key(client: Client): + wallet = WalletPolicy( + name="Missing a key", + descriptor_template="wsh(multi(2,@0/**,@1/**))", + keys_info=[ + "[f5acc2fd/48'/1'/0'/2']tpubDFAqEGNyad35aBCKUAXbQGDjdVhNueno5ZZVEn3sQbW5ci457gLR7HyTmHBg93oourBssgUxuWz1jX5uhc1qaqFo9VsybY1J5FuedLfm4dK", + # the second key is missing + ], + ) + + with pytest.raises(IncorrectDataError): + client.register_wallet(wallet) + + @has_automation("automations/register_wallet_accept.json") def test_register_wallet_unsupported_policy(client: Client): # valid policies, but not supported (might change in the future) @@ -132,7 +147,7 @@ def test_register_wallet_unsupported_policy(client: Client): @has_automation("automations/register_wallet_accept.json") -def test_register_miniscript_long_policy(client: Client, speculos_globals, model): +def test_register_miniscript_long_policy(client: Client, speculos_globals, model: str): # This test makes sure that policies longer than 256 bytes work as expected on all devices, # except on Nano S that has 196 bytes as a technical limitation. wallet = WalletPolicy( diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index 55e94a25..4eb8aef6 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -54,18 +54,37 @@ def should_go_right(event: dict): return False +def ux_thread_sign_psbt_stax(speculos_client: SpeculosClient, all_events: List[dict]): + """Completes the signing flow always going right and accepting at the appropriate time, while collecting all the events in all_events.""" + + first_approve = True + + while True: + event = speculos_client.get_next_event() + all_events.append(event) + + if "Tap to continue" in event["text"]: + speculos_client.finger_touch(55, 550) + + elif first_approve and "Hold to sign" in event["text"]: + first_approve = False + speculos_client.finger_touch(55, 550, 3) + + elif "SIGNED" in event["text"]: + break + + def ux_thread_sign_psbt(speculos_client: SpeculosClient, all_events: List[dict]): """Completes the signing flow always going right and accepting at the appropriate time, while collecting all the events in all_events.""" # press right until the last screen (will press the "right" button more times than needed) - while True: event = speculos_client.get_next_event() all_events.append(event) if should_go_right(event): speculos_client.press_and_release("right") - elif event["text"] == "Approve": + elif "Approve" in event["text"]: speculos_client.press_and_release("both") elif event["text"] == "Accept": speculos_client.press_and_release("both") @@ -95,18 +114,30 @@ def parse_signing_events(events: List[dict]) -> dict: ret["amounts"].append("") next_step = "" + elif ev["text"].startswith("Tap"): + ret["addresses"].append("") + ret["amounts"].append("") + next_step = "" + continue + elif ev["text"].startswith(keywords): next_step = ev["text"] continue if next_step.startswith("Address"): - ret["addresses"][-1] += ev["text"] + if len(ret["addresses"]) == 0: + ret["addresses"].append("") + + ret["addresses"][-1] += ev["text"].strip().replace("O", "0") # OCR misreads O for 0 elif next_step.startswith("Fees"): - ret["fees"] += ev["text"] + ret["fees"] += ev["text"].strip() elif next_step.startswith("Amount"): - ret["amounts"][-1] += ev["text"] + if len(ret["amounts"]) == 0: + ret["amounts"].append("") + + ret["amounts"][-1] += ev["text"].strip() return ret @@ -304,6 +335,25 @@ def test_sign_psbt_singlesig_wpkh_2to2_missing_nonwitnessutxo(client: Client): )] +@has_automation("automations/sign_with_default_wallet_accept.json") +def test_sign_psbt_singlesig_wpkh_selftransfer(client: Client): + # The only output is a change output. + # A "self-transfer" screen should be shown before the fees. + + wallet = WalletPolicy( + "", + "wpkh(@0/**)", + [ + "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" + ], + ) + + psbt = "cHNidP8BAHECAAAAAfcDVJxLN1tzz5vaIy2onFL/ht/OqwKm2jEWGwMNDE/cAQAAAAD9////As0qAAAAAAAAFgAUJfcXOL7SoYGoDC1n6egGa0OTD9/mtgEAAAAAABYAFDXG4N1tPISxa6iF3Kc6yGPQtZPsTTQlAAABAPYCAAAAAAEBCOcYS1aMP1uQcUKTMJbvlsZXsV4yNnVxynyMfxSX//UAAAAAFxYAFGEWho6AN6qeux0gU3BSWnK+Dw4D/f///wKfJwEAAAAAABepFG1IUtrzpUCfdyFtu46j1ZIxLX7ph0DiAQAAAAAAFgAU4e5IJz0XxNe96ANYDugMQ34E0/cCRzBEAiB1b84pX0QaOUrvCdDxKeB+idM6wYKTLGmqnUU/tL8/lQIgbSinpq4jBlo+SIGyh8XNVrWAeMlKBNmoLenKOBugKzcBIQKXsd8NwO+9naIfeI3nkgYjg6g3QZarGTRDs7SNVZfGPJBJJAABAR9A4gEAAAAAABYAFOHuSCc9F8TXvegDWA7oDEN+BNP3IgYCgffBheEUZI8iAFFfv7b+HNM7j4jolv6lj5/n3j68h3kY9azC/VQAAIABAACAAAAAgAAAAAAHAAAAACICAzQZjNnkwXFEhm1F6oC2nk1ADqH6t/RHBAOblLA4tV5BGPWswv1UAACAAQAAgAAAAIABAAAAEgAAAAAiAgJxtbd5rYcIOFh3l7z28MeuxavnanCdck9I0uJs+HTwoBj1rML9VAAAgAEAAIAAAACAAQAAAAAAAAAA" + result = client.sign_psbt(psbt, wallet, None) + + assert len(result) == 1 + + # def test_sign_psbt_legacy(client: Client): # # legacy address # # PSBT for a legacy 1-input 1-output spend @@ -365,18 +415,72 @@ def test_sign_psbt_multisig_wsh(client: Client): )] -# def test_sign_psbt_legacy_wrong_non_witness_utxo(client: Client): -# # legacy address -# # PSBT for a legacy 1-input 1-output spend -# # The spend is valid, but the non-witness utxo is wrong; therefore, it should fail the hash test -# # TODO: this fails PSBT decoding; need to make a version we can control for this test. +@has_automation("automations/sign_with_wallet_accept.json") +def test_sign_psbt_multisig_sh_wsh(client: Client): + # wrapped segwit multisig ("sh(wsh(sortedmulti(...)))") + wallet = MultisigWallet( + name="Cold storage", + address_type=AddressType.SH_WIT, + threshold=2, + keys_info=[ + "[e24243b4/48'/1'/0'/1']tpubDFY2NoEHyYsp4J98UCMAaRT5LzRYeXjWqh2txK2RsxPAR5YWKWyTeZBBncRJ7z5nL5RUQPEgycbgbbmywbeLaH9yWK6rnFAYQn28HyiYc1Y", + "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY", + ], + sorted=True + ) -# unsigned_raw_psbt_base64 = "cHNidP8BAFQCAAAAAbUlIwxFfIt0fsuFCNtL3dHKcOvUPQu2CNcqc8FrNtTyAAAAAAD+////AaDwGQAAAAAAGKkU2FZEFTTPb1ZpCw2Oa2sc/FxM59GIrAAAAAAAAQD5AgAAAAABATfphYFskBaL7jbWIkU3K7RS5zKr5BvfNHjec1rNieTrAQAAABcWABTkjiMSrvGNi5KFtSy72CSJolzNDv7///8C/y8bAAAAAAAZdqkU2FZEFTTPb1ZpCw2Oa2sc/FxM59GIrDS2GJ0BAAAAF6kUnEFiBqwsbP0pWpazURx45PGdXkWHAkcwRAIgCxWs2+R6UcpQuD6QKydU0irJ7yNe++5eoOly5VgqrEsCIHUD6t4LNW0292vnP+heXZ6Walx8DRW2TB+IOazzDNcaASEDnQS6zdUebuNm7FuOdKonnlNmPPpUyN66w2CIsX5N+pUySC0BAAA=" -# psbt = PSBT() -# psbt.deserialize(unsigned_raw_psbt_base64) + wallet_hmac = bytes.fromhex( + "677ec94c2e1a7446c6cac9db2adde8667b9a746dd63fa1e1863553cdb814a54a" + ) + + psbt = "cHNidP8BAFUCAAAAAS60cHn6kIlm2wk314ZKiOok2xj++cPoa/K5TXzNk4s6AQAAAAD9////AescAAAAAAAAGXapFFnK2lAxTIKeGfWneG+O4NSYf0KdiKwhlRUAAAEAigIAAAABAaNw+E0toKUlohxkK0YmapPS7uToo7RG7DA2YLrmoD8BAAAAFxYAFAppBymwQTPq8lpFfFWMuPRNdbTX/v///wI7rUIBAAAAABepFJMyNbbbdF4o3zxQhWSJ5ZXY5naHh60dAAAAAAAAF6kU9wt/XvakFsqnsR6xlBxP5N9MyyqHbvokAAEBIK0dAAAAAAAAF6kU9wt/XvakFsqnsR6xlBxP5N9MyyqHAQQiACAyIOGl/sIPCRep2F4Bude0ME17U2m2dPAiK96XdDCf7wEFR1IhA0fxhNV0BDkMTLzQjBSpKxSeh39pMEcQ+reqlD2a/D20IQPlOZCX7JMMMjUxBLMNtzR+gcVKZaL4J4sf/VRbo03NfFKuIgYDR/GE1XQEOQxMvNCMFKkrFJ6Hf2kwRxD6t6qUPZr8PbQc4kJDtDAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA+U5kJfskwwyNTEEsw23NH6BxUplovgnix/9VFujTc18HPWswv0wAACAAQAAgAAAAIABAACAAAAAAAAAAAAAAA==" + result = client.sign_psbt(psbt, wallet, wallet_hmac) + + assert result == [( + 0, + PartialSignature( + pubkey=bytes.fromhex("03e5399097ec930c32353104b30db7347e81c54a65a2f8278b1ffd545ba34dcd7c"), + signature=bytes.fromhex( + "30440220689c3ee23b8f52c21abe47ea6f37cf8bc72653cab9cd32658199b1a16db193d802200db5d2157044913d5a60f69e9ce10ab9a9d883d421d3fb0400d948b31c3b7ee201" + ) + ) + )] -# with pytest.raises(IncorrectDataError): -# client.sign_psbt(psbt) + +@has_automation("automations/sign_with_wallet_missing_nonwitnessutxo_accept.json") +def test_sign_psbt_multisig_sh_wsh_missing_nonwitnessutxo(client: Client): + # A transaction spending a wrapped segwit address has a script that appears like a legacy UTXO, but uses + # the segwit sighash algorithm. + # Therefore, if the non-witness-utxo is missing, we should still sign it while giving the warning for unverified inputs, + # for consistency with other segwit input types. + + wallet = MultisigWallet( + name="Cold storage", + address_type=AddressType.SH_WIT, + threshold=2, + keys_info=[ + "[e24243b4/48'/1'/0'/1']tpubDFY2NoEHyYsp4J98UCMAaRT5LzRYeXjWqh2txK2RsxPAR5YWKWyTeZBBncRJ7z5nL5RUQPEgycbgbbmywbeLaH9yWK6rnFAYQn28HyiYc1Y", + "[f5acc2fd/48'/1'/0'/1']tpubDFAqEGNyad35YgH8zxvxFZqNUoPtr5mDojs7wzbXQBHTZ4xHeVXG6w2HvsKvjBpaRpTmjYDjdPg5w2c6Wvu8QBkyMDrmBWdCyqkDM7reSsY", + ], + sorted=True + ) + + wallet_hmac = bytes.fromhex( + "677ec94c2e1a7446c6cac9db2adde8667b9a746dd63fa1e1863553cdb814a54a" + ) + + psbt = "cHNidP8BAFUCAAAAAS60cHn6kIlm2wk314ZKiOok2xj++cPoa/K5TXzNk4s6AQAAAAD9////AescAAAAAAAAGXapFFnK2lAxTIKeGfWneG+O4NSYf0KdiKwhlRUAAAEBIK0dAAAAAAAAF6kU9wt/XvakFsqnsR6xlBxP5N9MyyqHAQQiACAyIOGl/sIPCRep2F4Bude0ME17U2m2dPAiK96XdDCf7wEFR1IhA0fxhNV0BDkMTLzQjBSpKxSeh39pMEcQ+reqlD2a/D20IQPlOZCX7JMMMjUxBLMNtzR+gcVKZaL4J4sf/VRbo03NfFKuIgYDR/GE1XQEOQxMvNCMFKkrFJ6Hf2kwRxD6t6qUPZr8PbQc4kJDtDAAAIABAACAAAAAgAEAAIAAAAAAAAAAACIGA+U5kJfskwwyNTEEsw23NH6BxUplovgnix/9VFujTc18HPWswv0wAACAAQAAgAAAAIABAACAAAAAAAAAAAAAAA==" + result = client.sign_psbt(psbt, wallet, wallet_hmac) + + assert result == [( + 0, + PartialSignature( + pubkey=bytes.fromhex("03e5399097ec930c32353104b30db7347e81c54a65a2f8278b1ffd545ba34dcd7c"), + signature=bytes.fromhex( + "30440220689c3ee23b8f52c21abe47ea6f37cf8bc72653cab9cd32658199b1a16db193d802200db5d2157044913d5a60f69e9ce10ab9a9d883d421d3fb0400d948b31c3b7ee201" + ) + ) + )] @has_automation("automations/sign_with_default_wallet_accept.json") @@ -462,7 +566,8 @@ def test_sign_psbt_taproot_1to2_sighash_default(client: Client): assert bip0340.schnorr_verify(sighash0, partial_sig0.pubkey, partial_sig0.signature) -def test_sign_psbt_singlesig_wpkh_4to3(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_sign_psbt_singlesig_wpkh_4to3(client: Client, comm: SpeculosClient, is_speculos: bool, + model: str): # PSBT for a segwit 4-input 3-output spend (1 change address) # this test also checks that addresses, amounts and fees shown on screen are correct @@ -501,7 +606,11 @@ def test_sign_psbt_singlesig_wpkh_4to3(client: Client, comm: SpeculosClient, is_ all_events: List[dict] = [] - x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + if model == "stax": + x = threading.Thread(target=ux_thread_sign_psbt_stax, args=[comm, all_events]) + else: + x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + x.start() result = client.sign_psbt(psbt, wallet, None) x.join() @@ -520,13 +629,15 @@ def test_sign_psbt_singlesig_wpkh_4to3(client: Client, comm: SpeculosClient, is_ assert ((parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER, out_amt)) or (parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER_ALT, out_amt))) - out_addr = Script(psbt.tx.vout[out_idx].scriptPubKey).address(network=NETWORKS["test"]) + out_addr = Script(psbt.tx.vout[out_idx].scriptPubKey).address( + network=NETWORKS["test"]).replace('O', '0') # OCR misreads O for 0 assert parsed_events["addresses"][shown_out_idx] == out_addr shown_out_idx += 1 -def test_sign_psbt_singlesig_large_amount(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_sign_psbt_singlesig_large_amount(client: Client, comm: SpeculosClient, is_speculos: bool, + model: str): # Test with a transaction with an extremely large amount if not is_speculos: @@ -554,7 +665,10 @@ def test_sign_psbt_singlesig_large_amount(client: Client, comm: SpeculosClient, all_events: List[dict] = [] - x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + if model == "stax": + x = threading.Thread(target=ux_thread_sign_psbt_stax, args=[comm, all_events]) + else: + x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) x.start() result = client.sign_psbt(psbt, wallet, None) x.join() @@ -571,6 +685,7 @@ def test_sign_psbt_singlesig_large_amount(client: Client, comm: SpeculosClient, (parsed_events["amounts"][0] == format_amount(CURRENCY_TICKER_ALT, out_amt))) +@pytest.mark.timeout(0) # disable timeout @has_automation("automations/sign_with_default_wallet_accept.json") def test_sign_psbt_singlesig_wpkh_512to256(client: Client, enable_slow_tests: bool): # PSBT for a transaction with 512 inputs and 256 outputs (maximum currently supported in the app) @@ -602,9 +717,20 @@ def test_sign_psbt_singlesig_wpkh_512to256(client: Client, enable_slow_tests: bo assert len(result) == n_inputs -def test_sign_psbt_fail_11_changes(client: Client): +def ux_thread_acept_prompt_stax(speculos_client: SpeculosClient, all_events: List[dict]): + """Completes the signing flow always going right and accepting at the appropriate time, while collecting all the events in all_events.""" + + while True: + event = speculos_client.get_next_event() + all_events.append(event) + if "Tap to continue" in event["text"]: + speculos_client.finger_touch(55, 550) + break + + +def test_sign_psbt_fail_11_changes(client: Client, comm: SpeculosClient, model: str): # PSBT for transaction with 11 change addresses; the limit is 10, so it must fail with NotSupportedError - # before any user interaction + # before any user interaction on nanos. wallet = WalletPolicy( "", @@ -621,6 +747,12 @@ def test_sign_psbt_fail_11_changes(client: Client): [True] * 11, ) + all_events: List[dict] = [] + + if model == "stax": + x = threading.Thread(target=ux_thread_acept_prompt_stax, args=[comm, all_events]) + + x.start() with pytest.raises(NotSupportedError): client.sign_psbt(psbt, wallet, None) @@ -678,6 +810,27 @@ def test_sign_psbt_with_opreturn(client: Client, comm: SpeculosClient): assert len(hww_sigs) == 1 +def test_sign_psbt_with_naked_opreturn(client: Client, comm: SpeculosClient): + wallet = WalletPolicy( + "", + "wpkh(@0/**)", + [ + "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" + ], + ) + + # Same psbt as in test_sign_psbt_with_opreturn, but the first output is a naked OP_RETURN script (no data). + # Signing such outputs is needed in BIP-0322. + psbt_b64 = "cHNidP8BAFwCAAAAAZ0gZDu3l28lrZWbtsuoIfI07zpsaXXMe6sMHHJn03LPAAAAAAD+////AgAAAAAAAAAAAWrBlZgAAAAAABYAFCuTP2nl6yRKHwS+1J6OyeTsk7yfAAAAAAABAHECAAAAAZ6afPCN0VxFOW9vKyNxhgF2lpJPsNbBKlg1xV3WnCoPAAAAAAD+////AoCWmAAAAAAAFgAUE0foKgN7Xbs4z4xHWfJCsfXH4JrzWm0pAQAAABYAFAgOnmT0kCvYJ6vJ4DkmkNGXT3iFQQAAAAEBH4CWmAAAAAAAFgAUE0foKgN7Xbs4z4xHWfJCsfXH4JoiBgJ8t100sAXE659iu/LEV9djjoE+dX787I+mhnfZULY2Yhj1rML9VAAAgAEAAIAAAACAAAAAAAAAAAAAACICAxmbidg1b1fhzjgKEgXPKGBtvqiYVbEcPf7PuKGlM1aJGPWswv1UAACAAQAAgAAAAIABAAAAAQAAAAA=" + psbt = PSBT() + psbt.deserialize(psbt_b64) + + with automation(comm, "automations/sign_with_default_wallet_accept.json"): + hww_sigs = client.sign_psbt(psbt, wallet, None) + + assert len(hww_sigs) == 1 + + def test_sign_psbt_with_segwit_v16(client: Client, comm: SpeculosClient): # This psbt contains an output with future psbt version 16 (corresponding to address # tb1sqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq4hu3px). diff --git a/tests/test_sign_psbt_v1.py b/tests/test_sign_psbt_v1.py index 16763d5e..ced263fe 100644 --- a/tests/test_sign_psbt_v1.py +++ b/tests/test_sign_psbt_v1.py @@ -74,6 +74,28 @@ def ux_thread_sign_psbt(speculos_client: SpeculosClient, all_events: List[dict]) break +def ux_thread_sign_psbt_stax(speculos_client: SpeculosClient, all_events: List[dict]): + """Completes the signing flow always going right and accepting at the appropriate time, while collecting all the events in all_events.""" + + first_approve = True + while True: + event = speculos_client.get_next_event() + all_events.append(event) + + if event["text"] == "Tap to continue": + speculos_client.finger_touch(55, 550) + + elif first_approve and ("Approve" in event["text"] or "Hold" in event["text"]): + first_approve = False + speculos_client.finger_touch(55, 550, 3) + + elif event["text"] == "TRANSACTION": + break + + elif "CONFIRMED" in event["text"]: + first_approve = True + + def parse_signing_events(events: List[dict]) -> dict: ret = dict() @@ -97,18 +119,29 @@ def parse_signing_events(events: List[dict]) -> dict: ret["amounts"].append("") next_step = "" + elif ev["text"].startswith("Tap"): + ret["addresses"].append("") + ret["amounts"].append("") + next_step = "" + continue + elif ev["text"].startswith(keywords): next_step = ev["text"] continue if next_step.startswith("Address"): - ret["addresses"][-1] += ev["text"] + if len(ret["addresses"]) == 0: + ret["addresses"].append("") + + ret["addresses"][-1] += ev["text"].strip().replace("O", "0") # OCR misreads O for 0 elif next_step.startswith("Fees"): ret["fees"] += ev["text"] elif next_step.startswith("Amount"): - ret["amounts"][-1] += ev["text"] + if len(ret["amounts"]) == 0: + ret["amounts"].append("") + ret["amounts"][-1] += ev["text"].strip() return ret @@ -333,7 +366,8 @@ def test_sign_psbt_taproot_1to2_v1(client: Client): assert bip0340.schnorr_verify(sighash0, pubkey0_psbt, partial_sig0.signature[:-1]) -def test_sign_psbt_singlesig_wpkh_4to3_v1(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_sign_psbt_singlesig_wpkh_4to3_v1(client: Client, comm: SpeculosClient, is_speculos: bool, + model: str): # PSBT for a segwit 4-input 3-output spend (1 change address) # this test also checks that addresses, amounts and fees shown on screen are correct @@ -373,7 +407,11 @@ def test_sign_psbt_singlesig_wpkh_4to3_v1(client: Client, comm: SpeculosClient, all_events: List[dict] = [] - x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + if model == "stax": + x = threading.Thread(target=ux_thread_sign_psbt_stax, args=[comm, all_events]) + else: + x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + x.start() result = client.sign_psbt(psbt, wallet, None) x.join() @@ -382,15 +420,15 @@ def test_sign_psbt_singlesig_wpkh_4to3_v1(client: Client, comm: SpeculosClient, parsed_events = parse_signing_events(all_events) - assert((parsed_events["fees"] == format_amount(CURRENCY_TICKER, fees_amount)) or - (parsed_events["fees"] == format_amount(CURRENCY_TICKER_ALT, fees_amount))) + assert ((parsed_events["fees"] == format_amount(CURRENCY_TICKER, fees_amount)) or + (parsed_events["fees"] == format_amount(CURRENCY_TICKER_ALT, fees_amount))) shown_out_idx = 0 for out_idx in range(n_outs): if out_idx != change_index: out_amt = psbt.tx.vout[out_idx].nValue - assert((parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER, out_amt)) or - (parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER_ALT, out_amt))) + assert ((parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER, out_amt)) or + (parsed_events["amounts"][shown_out_idx] == format_amount(CURRENCY_TICKER_ALT, out_amt))) out_addr = Script(psbt.tx.vout[out_idx].scriptPubKey).address(network=NETWORKS["test"]) assert parsed_events["addresses"][shown_out_idx] == out_addr @@ -398,7 +436,8 @@ def test_sign_psbt_singlesig_wpkh_4to3_v1(client: Client, comm: SpeculosClient, shown_out_idx += 1 -def test_sign_psbt_singlesig_large_amount_v1(client: Client, comm: SpeculosClient, is_speculos: bool): +def test_sign_psbt_singlesig_large_amount_v1(client: Client, comm: SpeculosClient, is_speculos: + bool, model: str): # Test with a transaction with an extremely large amount if not is_speculos: @@ -427,7 +466,11 @@ def test_sign_psbt_singlesig_large_amount_v1(client: Client, comm: SpeculosClien all_events: List[dict] = [] - x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + if model == "stax": + x = threading.Thread(target=ux_thread_sign_psbt_stax, args=[comm, all_events]) + else: + x = threading.Thread(target=ux_thread_sign_psbt, args=[comm, all_events]) + x.start() result = client.sign_psbt(psbt, wallet, None) x.join() @@ -436,14 +479,15 @@ def test_sign_psbt_singlesig_large_amount_v1(client: Client, comm: SpeculosClien parsed_events = parse_signing_events(all_events) - assert((parsed_events["fees"] == format_amount(CURRENCY_TICKER, fees_amount)) or - (parsed_events["fees"] == format_amount(CURRENCY_TICKER_ALT, fees_amount))) + assert ((parsed_events["fees"] == format_amount(CURRENCY_TICKER, fees_amount)) or + (parsed_events["fees"] == format_amount(CURRENCY_TICKER_ALT, fees_amount))) out_amt = psbt.tx.vout[0].nValue - assert((parsed_events["amounts"][0] == format_amount(CURRENCY_TICKER, out_amt)) or - (parsed_events["amounts"][0] == format_amount(CURRENCY_TICKER_ALT, out_amt))) + assert ((parsed_events["amounts"][0] == format_amount(CURRENCY_TICKER, out_amt)) or + (parsed_events["amounts"][0] == format_amount(CURRENCY_TICKER_ALT, out_amt))) +@pytest.mark.timeout(0) # disable timeout @has_automation("automations/sign_with_default_wallet_accept.json") def test_sign_psbt_singlesig_wpkh_512to256_v1(client: Client, enable_slow_tests: bool): # PSBT for a transaction with 512 inputs and 256 outputs (maximum currently supported in the app) @@ -476,7 +520,19 @@ def test_sign_psbt_singlesig_wpkh_512to256_v1(client: Client, enable_slow_tests: assert len(result) == n_inputs -def test_sign_psbt_fail_11_changes_v1(client: Client): +def ux_thread_accept_prompt_stax(speculos_client: SpeculosClient, all_events: List[dict]): + """Completes the signing flow always going right and accepting at the appropriate time, while collecting all the events in all_events.""" + + while True: + event = speculos_client.get_next_event() + all_events.append(event) + if "Tap to continue" in event["text"]: + speculos_client.finger_touch(55, 550) + break + + +def test_sign_psbt_fail_11_changes_v1(client: Client, comm: SpeculosClient, is_speculos: bool, + model: str): # PSBT for transaction with 11 change addresses; the limit is 10, so it must fail with NotSupportedError # before any user interaction @@ -496,6 +552,12 @@ def test_sign_psbt_fail_11_changes_v1(client: Client): [True] * 11, ) + all_events: List[dict] = [] + + if model == "stax": + x = threading.Thread(target=ux_thread_accept_prompt_stax, args=[comm, all_events]) + + x.start() with pytest.raises(NotSupportedError): client.sign_psbt(psbt, wallet, None) diff --git a/tests_mainnet/requirements.txt b/tests_mainnet/requirements.txt index 58945f42..23a57de0 100644 --- a/tests_mainnet/requirements.txt +++ b/tests_mainnet/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/tests_mainnet/test_dashboard.py b/tests_mainnet/test_dashboard.py index 5d2c67f5..f0e8bb5e 100644 --- a/tests_mainnet/test_dashboard.py +++ b/tests_mainnet/test_dashboard.py @@ -9,6 +9,9 @@ def test_dashboard(comm: SpeculosClient, is_speculos: bool, app_version: str, mo if not is_speculos: pytest.skip("Requires speculos") + if model == "stax": + pytest.skip("No dashboard test for stax") + comm.press_and_release("right") comm.wait_for_text_event("Version") comm.wait_for_text_event(app_version) diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt index 059a7f8f..b67f4d35 100644 --- a/unit-tests/CMakeLists.txt +++ b/unit-tests/CMakeLists.txt @@ -1,18 +1,17 @@ cmake_minimum_required(VERSION 3.10) if(${CMAKE_VERSION} VERSION_LESS 3.10) - cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) endif() # project information project(unit_tests - VERSION 0.1 - DESCRIPTION "Unit tests for Ledger Nano application" - LANGUAGES C) - + VERSION 0.1 + DESCRIPTION "Unit tests for Ledger Nano application" + LANGUAGES C) # guard against bad build-type strings -if (NOT CMAKE_BUILD_TYPE) +if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug") endif() @@ -37,6 +36,7 @@ add_compile_definitions(TEST DEBUG=0 SKIP_FOR_CMOCKA PRINTF=printf) include_directories(../src) include_directories(mock_includes) +include_directories(libs) add_executable(test_apdu_parser test_apdu_parser.c) add_executable(test_base58 test_base58.c) @@ -49,8 +49,14 @@ add_executable(test_parser test_parser.c) add_executable(test_script test_script.c) add_executable(test_wallet test_wallet.c) add_executable(test_write test_write.c) -#add_executable(test_crypto test_crypto.c) +# add_executable(test_crypto test_crypto.c) + +# Mock libraries +add_library(crypto_mocks SHARED libs/crypto_mocks.c) +add_library(sha256 SHARED libs/sha-256.c) + +# App's libraries add_library(apdu_parser SHARED ../src/boilerplate/apdu_parser.c) add_library(base58 SHARED ../src/common/base58.c) add_library(bip32 SHARED ../src/common/bip32.c) @@ -63,8 +69,13 @@ add_library(script SHARED ../src/common/script.c) add_library(varint SHARED ../src/common/varint.c) add_library(wallet SHARED ../src/common/wallet.c) add_library(write SHARED ../src/common/write.c) -#add_library(crypto SHARED ../src/crypto.c) +# add_library(crypto SHARED ../src/crypto.c) + +# Mock libraries +target_link_libraries(crypto_mocks PUBLIC sha256) + +# App's libraries target_link_libraries(test_apdu_parser PUBLIC cmocka gcov apdu_parser) target_link_libraries(test_base58 PUBLIC cmocka gcov base58) target_link_libraries(test_bip32 PUBLIC cmocka gcov bip32 read) @@ -74,10 +85,10 @@ target_link_libraries(test_display_utils PUBLIC cmocka gcov display_utils) target_link_libraries(test_format PUBLIC cmocka gcov format) target_link_libraries(test_parser PUBLIC cmocka gcov parser buffer varint read write bip32) target_link_libraries(test_script PUBLIC cmocka gcov script buffer varint read write bip32) -target_link_libraries(test_wallet PUBLIC cmocka gcov wallet script buffer varint read write bip32) +target_link_libraries(test_wallet PUBLIC cmocka gcov wallet script buffer varint read write bip32 base58 crypto_mocks) target_link_libraries(test_write PUBLIC cmocka gcov write) -#target_link_libraries(test_crypto PUBLIC cmocka gcov crypto) +# target_link_libraries(test_crypto PUBLIC cmocka gcov crypto) add_test(test_apdu_parser test_apdu_parser) add_test(test_base58 test_base58) add_test(test_bip32 test_bip32) @@ -89,4 +100,5 @@ add_test(test_parser test_parser) add_test(test_script test_script) add_test(test_wallet test_wallet) add_test(test_write test_write) -#add_test(test_crypto test_crypto) + +# add_test(test_crypto test_crypto) diff --git a/unit-tests/libs/crypto_mocks.c b/unit-tests/libs/crypto_mocks.c new file mode 100644 index 00000000..79d09d06 --- /dev/null +++ b/unit-tests/libs/crypto_mocks.c @@ -0,0 +1,10 @@ +#include +#include "crypto_mocks.h" +#include "sha-256.h" + +void crypto_get_checksum(const uint8_t *in, uint16_t in_len, uint8_t out[static 4]) { + uint8_t buffer[32]; + calc_sha_256(buffer, in, in_len); + calc_sha_256(buffer, buffer, 32); + memmove(out, buffer, 4); +} diff --git a/unit-tests/libs/crypto_mocks.h b/unit-tests/libs/crypto_mocks.h new file mode 100644 index 00000000..436076ea --- /dev/null +++ b/unit-tests/libs/crypto_mocks.h @@ -0,0 +1,7 @@ +// We're currently unable to compile the app's crypto.c in unit tests. +// This library mocks the functions currently used in other modules that are part of +// the unit tests. + +#include + +void crypto_get_checksum(const uint8_t *in, uint16_t in_len, uint8_t out[static 4]); \ No newline at end of file diff --git a/unit-tests/libs/sha-256.c b/unit-tests/libs/sha-256.c new file mode 100644 index 00000000..3ae891b7 --- /dev/null +++ b/unit-tests/libs/sha-256.c @@ -0,0 +1,224 @@ +// from https://github.com/amosnier/sha-2/blob/0be5e1601b487b2aa6869e2fe12bd30ac2ca543c/sha-256.c + +#include "sha-256.h" + +#define TOTAL_LEN_LEN 8 + +/* + * Comments from pseudo-code at https://en.wikipedia.org/wiki/SHA-2 are reproduced here. + * When useful for clarification, portions of the pseudo-code are reproduced here too. + */ + +/* + * @brief Rotate a 32-bit value by a number of bits to the right. + * @param value The value to be rotated. + * @param count The number of bits to rotate by. + * @return The rotated value. + */ +static inline uint32_t right_rot(uint32_t value, unsigned int count) +{ + /* + * Defined behaviour in standard C for all count where 0 < count < 32, which is what we need here. + */ + return value >> count | value << (32 - count); +} + +/* + * @brief Update a hash value under calculation with a new chunk of data. + * @param h Pointer to the first hash item, of a total of eight. + * @param p Pointer to the chunk data, which has a standard length. + * + * @note This is the SHA-256 work horse. + */ +static inline void consume_chunk(uint32_t *h, const uint8_t *p) +{ + unsigned i, j; + uint32_t ah[8]; + + /* Initialize working variables to current hash value: */ + for (i = 0; i < 8; i++) + ah[i] = h[i]; + + /* + * The w-array is really w[64], but since we only need 16 of them at a time, we save stack by + * calculating 16 at a time. + * + * This optimization was not there initially and the rest of the comments about w[64] are kept in their + * initial state. + */ + + /* + * create a 64-entry message schedule array w[0..63] of 32-bit words (The initial values in w[0..63] + * don't matter, so many implementations zero them here) copy chunk into first 16 words w[0..15] of the + * message schedule array + */ + uint32_t w[16]; + + /* Compression function main loop: */ + for (i = 0; i < 4; i++) { + for (j = 0; j < 16; j++) { + if (i == 0) { + w[j] = + (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3]; + p += 4; + } else { + /* Extend the first 16 words into the remaining 48 words w[16..63] of the + * message schedule array: */ + const uint32_t s0 = right_rot(w[(j + 1) & 0xf], 7) ^ right_rot(w[(j + 1) & 0xf], 18) ^ + (w[(j + 1) & 0xf] >> 3); + const uint32_t s1 = right_rot(w[(j + 14) & 0xf], 17) ^ + right_rot(w[(j + 14) & 0xf], 19) ^ (w[(j + 14) & 0xf] >> 10); + w[j] = w[j] + s0 + w[(j + 9) & 0xf] + s1; + } + const uint32_t s1 = right_rot(ah[4], 6) ^ right_rot(ah[4], 11) ^ right_rot(ah[4], 25); + const uint32_t ch = (ah[4] & ah[5]) ^ (~ah[4] & ah[6]); + + /* + * Initialize array of round constants: + * (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311): + */ + static const uint32_t k[] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, + 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, + 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, + 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, + 0xc67178f2}; + + const uint32_t temp1 = ah[7] + s1 + ch + k[i << 4 | j] + w[j]; + const uint32_t s0 = right_rot(ah[0], 2) ^ right_rot(ah[0], 13) ^ right_rot(ah[0], 22); + const uint32_t maj = (ah[0] & ah[1]) ^ (ah[0] & ah[2]) ^ (ah[1] & ah[2]); + const uint32_t temp2 = s0 + maj; + + ah[7] = ah[6]; + ah[6] = ah[5]; + ah[5] = ah[4]; + ah[4] = ah[3] + temp1; + ah[3] = ah[2]; + ah[2] = ah[1]; + ah[1] = ah[0]; + ah[0] = temp1 + temp2; + } + } + + /* Add the compressed chunk to the current hash value: */ + for (i = 0; i < 8; i++) + h[i] += ah[i]; +} + +/* + * Public functions. See header file for documentation. + */ + +void sha_256_init(struct Sha_256 *sha_256, uint8_t hash[SIZE_OF_SHA_256_HASH]) +{ + sha_256->hash = hash; + sha_256->chunk_pos = sha_256->chunk; + sha_256->space_left = SIZE_OF_SHA_256_CHUNK; + sha_256->total_len = 0; + /* + * Initialize hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes + * 2..19): + */ + sha_256->h[0] = 0x6a09e667; + sha_256->h[1] = 0xbb67ae85; + sha_256->h[2] = 0x3c6ef372; + sha_256->h[3] = 0xa54ff53a; + sha_256->h[4] = 0x510e527f; + sha_256->h[5] = 0x9b05688c; + sha_256->h[6] = 0x1f83d9ab; + sha_256->h[7] = 0x5be0cd19; +} + +void sha_256_write(struct Sha_256 *sha_256, const void *data, size_t len) +{ + sha_256->total_len += len; + + const uint8_t *p = data; + + while (len > 0) { + /* + * If the input chunks have sizes that are multiples of the calculation chunk size, no copies are + * necessary. We operate directly on the input data instead. + */ + if (sha_256->space_left == SIZE_OF_SHA_256_CHUNK && len >= SIZE_OF_SHA_256_CHUNK) { + consume_chunk(sha_256->h, p); + len -= SIZE_OF_SHA_256_CHUNK; + p += SIZE_OF_SHA_256_CHUNK; + continue; + } + /* General case, no particular optimization. */ + const size_t consumed_len = len < sha_256->space_left ? len : sha_256->space_left; + memcpy(sha_256->chunk_pos, p, consumed_len); + sha_256->space_left -= consumed_len; + len -= consumed_len; + p += consumed_len; + if (sha_256->space_left == 0) { + consume_chunk(sha_256->h, sha_256->chunk); + sha_256->chunk_pos = sha_256->chunk; + sha_256->space_left = SIZE_OF_SHA_256_CHUNK; + } else { + sha_256->chunk_pos += consumed_len; + } + } +} + +uint8_t *sha_256_close(struct Sha_256 *sha_256) +{ + uint8_t *pos = sha_256->chunk_pos; + size_t space_left = sha_256->space_left; + uint32_t *const h = sha_256->h; + + /* + * The current chunk cannot be full. Otherwise, it would already have been consumed. I.e. there is space left for + * at least one byte. The next step in the calculation is to add a single one-bit to the data. + */ + *pos++ = 0x80; + --space_left; + + /* + * Now, the last step is to add the total data length at the end of the last chunk, and zero padding before + * that. But we do not necessarily have enough space left. If not, we pad the current chunk with zeroes, and add + * an extra chunk at the end. + */ + if (space_left < TOTAL_LEN_LEN) { + memset(pos, 0x00, space_left); + consume_chunk(h, sha_256->chunk); + pos = sha_256->chunk; + space_left = SIZE_OF_SHA_256_CHUNK; + } + const size_t left = space_left - TOTAL_LEN_LEN; + memset(pos, 0x00, left); + pos += left; + size_t len = sha_256->total_len; + pos[7] = (uint8_t)(len << 3); + len >>= 5; + int i; + for (i = 6; i >= 0; --i) { + pos[i] = (uint8_t)len; + len >>= 8; + } + consume_chunk(h, sha_256->chunk); + /* Produce the final hash value (big-endian): */ + int j; + uint8_t *const hash = sha_256->hash; + for (i = 0, j = 0; i < 8; i++) { + hash[j++] = (uint8_t)(h[i] >> 24); + hash[j++] = (uint8_t)(h[i] >> 16); + hash[j++] = (uint8_t)(h[i] >> 8); + hash[j++] = (uint8_t)h[i]; + } + return sha_256->hash; +} + +void calc_sha_256(uint8_t hash[SIZE_OF_SHA_256_HASH], const void *input, size_t len) +{ + struct Sha_256 sha_256; + sha_256_init(&sha_256, hash); + sha_256_write(&sha_256, input, len); + (void)sha_256_close(&sha_256); +} \ No newline at end of file diff --git a/unit-tests/libs/sha-256.h b/unit-tests/libs/sha-256.h new file mode 100644 index 00000000..2c1c8047 --- /dev/null +++ b/unit-tests/libs/sha-256.h @@ -0,0 +1,105 @@ +// from https://github.com/amosnier/sha-2/blob/0be5e1601b487b2aa6869e2fe12bd30ac2ca543c/sha-256.h + +#ifndef SHA_256_H +#define SHA_256_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * @brief Size of the SHA-256 sum. This times eight is 256 bits. + */ +#define SIZE_OF_SHA_256_HASH 32 + +/* + * @brief Size of the chunks used for the calculations. + * + * @note This should mostly be ignored by the user, although when using the streaming API, it has an impact for + * performance. Add chunks whose size is a multiple of this, and you will avoid a lot of superfluous copying in RAM! + */ +#define SIZE_OF_SHA_256_CHUNK 64 + +/* + * @brief The opaque SHA-256 type, that should be instantiated when using the streaming API. + * + * @note Although the details are exposed here, in order to make instantiation easy, you should refrain from directly + * accessing the fields, as they may change in the future. + */ +struct Sha_256 { + uint8_t *hash; + uint8_t chunk[SIZE_OF_SHA_256_CHUNK]; + uint8_t *chunk_pos; + size_t space_left; + size_t total_len; + uint32_t h[8]; +}; + +/* + * @brief The simple SHA-256 calculation function. + * @param hash Hash array, where the result is delivered. + * @param input Pointer to the data the hash shall be calculated on. + * @param len Length of the input data, in byte. + * + * @note If all of the data you are calculating the hash value on is available in a contiguous buffer in memory, this is + * the function you should use. + * + * @note If either of the passed pointers is NULL, the results are unpredictable. + */ +void calc_sha_256(uint8_t hash[SIZE_OF_SHA_256_HASH], const void *input, size_t len); + +/* + * @brief Initialize a SHA-256 streaming calculation. + * @param sha_256 A pointer to a SHA-256 structure. + * @param hash Hash array, where the result will be delivered. + * + * @note If all of the data you are calculating the hash value on is not available in a contiguous buffer in memory, this is + * where you should start. Instantiate a SHA-256 structure, for instance by simply declaring it locally, make your hash + * buffer available, and invoke this function. Once a SHA-256 hash has been calculated (see further below) a SHA-256 + * structure can be initialized again for the next calculation. + * + * @note If either of the passed pointers is NULL, the results are unpredictable. + */ +void sha_256_init(struct Sha_256 *sha_256, uint8_t hash[SIZE_OF_SHA_256_HASH]); + +/* + * @brief Stream more input data for an on-going SHA-256 calculation. + * @param sha_256 A pointer to a previously initialized SHA-256 structure. + * @param data Pointer to the data to be added to the calculation. + * @param len Length of the data to add, in byte. + * + * @note This function may be invoked an arbitrary number of times between initialization and closing, but the maximum + * data length is limited by the SHA-256 algorithm: the total number of bits (i.e. the total number of bytes times + * eight) must be representable by a 64-bit unsigned integer. While that is not a practical limitation, the results are + * unpredictable if that limit is exceeded. + * + * @note This function may be invoked on empty data (zero length), although that obviously will not add any data. + * + * @note If either of the passed pointers is NULL, the results are unpredictable. + */ +void sha_256_write(struct Sha_256 *sha_256, const void *data, size_t len); + +/* + * @brief Conclude a SHA-256 streaming calculation, making the hash value available. + * @param sha_256 A pointer to a previously initialized SHA-256 structure. + * @return Pointer to the hash array, where the result is delivered. + * + * @note After this function has been invoked, the result is available in the hash buffer that initially was provided. A + * pointer to the hash value is returned for convenience, but you should feel free to ignore it: it is simply a pointer + * to the first byte of your initially provided hash array. + * + * @note If the passed pointer is NULL, the results are unpredictable. + * + * @note Invoking this function for a calculation with no data (the writing function has never been invoked, or it only + * has been invoked with empty data) is legal. It will calculate the SHA-256 value of the empty string. + */ +uint8_t *sha_256_close(struct Sha_256 *sha_256); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/unit-tests/test_bip32.c b/unit-tests/test_bip32.c index 65cf52f1..1e406a4f 100644 --- a/unit-tests/test_bip32.c +++ b/unit-tests/test_bip32.c @@ -146,82 +146,6 @@ static void test_is_pubkey_path_standard_false(void **state) { } -static void test_is_address_path_standard_true(void **state) { - (void) state; - - const uint32_t valid_purposes[] = {44, 49, 84}; - const uint32_t coin_types[] = {0, 8}; - - for (int i_p = 0; i_p < sizeof(valid_purposes)/sizeof(valid_purposes[0]); i_p++) { - uint32_t purpose = valid_purposes[i_p]; - - // any coin type will do, if coin_types is not given - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, 12345^H, 42^H, 0, 0}, 5, purpose, NULL, 0, 0)); - - for (int i_c = 0; i_c < sizeof(coin_types)/sizeof(coin_types[0]); i_c++) { - uint32_t coin_type = coin_types[i_c]; - - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, 0^H, 0, 0}, 5, purpose, coin_types, 2, 0)); - - // Change address - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, 0^H, 1, 0}, 5, purpose, coin_types, 2, 1)); - - // Change or not with expected_change == -1 - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, 0^H, 0, 0}, 5, purpose, coin_types, 2, -1)); - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, 0^H, 1, 0}, 5, purpose, coin_types, 2, -1)); - - // Largest valid account - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, MAX_BIP44_ACCOUNT_RECOMMENDED^H, 0, 0}, 5, purpose, coin_types, 2, 0)); - - // Largest valid address index - assert_true(is_address_path_standard((const uint32_t[]){purpose^H, coin_type^H, 0^H, 0, MAX_BIP44_ADDRESS_INDEX_RECOMMENDED}, 5, purpose, coin_types, 2, 0)); - } - } -} - -static void test_is_address_path_standard_false(void **state) { - (void) state; - - const uint32_t coin_types[] = {0, 8}; - - // purpose not matching expected one - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 0, 0}, 5, 84, coin_types, 2, 0)); - // non-hardened purpose - assert_false(is_address_path_standard((const uint32_t[]){44, 0^H, 0^H, 0, 0}, 5, 44, coin_types, 2, 0)); - - // invalid coin type - assert_false(is_address_path_standard((const uint32_t[]){44^H, 100^H, 0^H, 0, 0}, 44, 5, coin_types, 2, 0)); - // non-hardened coin type (but otherwise in coin_types) - assert_false(is_address_path_standard((const uint32_t[]){44^H, 8, 0^H, 0, 0}, 44, 5, coin_types, 2, 0)); - // should still check that coin type is hardened, even if coin_types is not given - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0, 0^H, 0, 0}, 44, 5, NULL, 0, 0)); - - // account too big - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, (1 + MAX_BIP44_ACCOUNT_RECOMMENDED)^H, 0, 0}, 44, 5, coin_types, 2, 0)); - // account not hardened - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0, 0, 0}, 44, 5, coin_types, 2, 0)); - - // got change when is_change = 0 - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 1, 0}, 44, 5, coin_types, 2, 0)); - // didn't get change despite is_change = 1 - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 0, 0}, 44, 5, coin_types, 2, 1)); - - // invalid change value, even if expected_change == -1 - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 2, 0}, 44, 5, coin_types, 2, -1)); - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 0^H, 0}, 44, 5, coin_types, 2, -1)); - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 1^H, 0}, 44, 5, coin_types, 2, -1)); - - // change is hardened, but it shouldn't be - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 0^H, 0}, 44, 5, coin_types, 2, 0)); - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0^H, 1^H, 0}, 44, 5, coin_types, 2, 1)); - - // account too big - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0, 0, 1 + MAX_BIP44_ADDRESS_INDEX_RECOMMENDED}, 44, 5, coin_types, 2, 0)); - // account is hardened - assert_false(is_address_path_standard((const uint32_t[]){44^H, 0^H, 0, 0, 0^H}, 44, 5, coin_types, 2, 0)); -} - - int main() { const struct CMUnitTest tests[] = { cmocka_unit_test(test_bip32_format), @@ -229,9 +153,7 @@ int main() { cmocka_unit_test(test_bip32_read), cmocka_unit_test(test_bad_bip32_read), cmocka_unit_test(test_is_pubkey_path_standard_true), - cmocka_unit_test(test_is_pubkey_path_standard_false), - cmocka_unit_test(test_is_address_path_standard_true), - cmocka_unit_test(test_is_address_path_standard_false) + cmocka_unit_test(test_is_pubkey_path_standard_false) }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/unit-tests/test_script.c b/unit-tests/test_script.c index 93ff6f55..1361d49e 100644 --- a/unit-tests/test_script.c +++ b/unit-tests/test_script.c @@ -235,6 +235,9 @@ static void test_format_opscript_script_valid(void **state) { uint8_t input22[] = {OP_RETURN, OP_1NEGATE}; CHECK_VALID_TESTCASE(input22, "OP_RETURN -1"); + + uint8_t input_23[] = {OP_RETURN}; + CHECK_VALID_TESTCASE(input_23, "OP_RETURN"); } static void test_format_opscript_script_invalid(void **state) { @@ -244,9 +247,6 @@ static void test_format_opscript_script_invalid(void **state) { char out[MAX_OPRETURN_OUTPUT_DESC_SIZE]; assert_int_equal(format_opscript_script(input_empty, 0, out), -1); - uint8_t input_no_push[] = {OP_RETURN}; - CHECK_INVALID_TESTCASE(input_no_push); - uint8_t input_not_opreturn[] = {OP_DUP}; CHECK_INVALID_TESTCASE(input_not_opreturn); diff --git a/unit-tests/test_wallet.c b/unit-tests/test_wallet.c index ca5fa060..eb95ae15 100644 --- a/unit-tests/test_wallet.c +++ b/unit-tests/test_wallet.c @@ -268,6 +268,36 @@ static void test_parse_policy_tr_multisig(void **state) { check_key_placeholder(&tapscript_right->key_placeholders[2], 5, 0, 1); } +static void test_get_policy_segwit_version(void **state) { + (void) state; + + uint8_t out[MAX_WALLET_POLICY_MEMORY_SIZE]; + policy_node_t *policy = out; + + // legacy policies (returning -1) + parse_policy("pkh(@0/**)", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == -1); + + parse_policy("sh(multi(2,@0/**,@1/**))", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == -1); + + // segwit v0 policies + parse_policy("wpkh(@0/**)", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 0); + parse_policy("wsh(multi(2,@0/**,@1/**))", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 0); + parse_policy("sh(wpkh(@0/**))", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 0); + parse_policy("sh(wsh(multi(2,@0/**,@1/**)))", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 0); + + // segwit v1 policies + parse_policy("tr(@0/**)", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 1); + parse_policy("tr(@0/**,{pk(@1/**,multi(1,@2/**,@3/**)})", out, sizeof(out)); + assert(get_policy_segwit_version(policy) == 1); +} + static void test_failures(void **state) { (void) state; @@ -511,6 +541,7 @@ int main() { cmocka_unit_test(test_parse_policy_map_multisig_3), cmocka_unit_test(test_parse_policy_tr), cmocka_unit_test(test_parse_policy_tr_multisig), + cmocka_unit_test(test_get_policy_segwit_version), cmocka_unit_test(test_failures), cmocka_unit_test(test_miniscript_types), }; From f6608401d8d8a823e241a407d2d7e7b1d72b18b0 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Tue, 19 Sep 2023 00:06:32 +0800 Subject: [PATCH 02/10] chore: update icons and logo --- Makefile | 6 +++--- src/ui/menu_bagl.c | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 0718019f..f20cac90 100644 --- a/Makefile +++ b/Makefile @@ -88,9 +88,9 @@ endif # Application icons following guidelines: # https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon -ICON_NANOS = icons/nanos_app_bitcoin.gif -ICON_NANOX = icons/nanox_app_bitcoin.gif -ICON_NANOSP = icons/nanox_app_bitcoin.gif +ICON_NANOS = icons/nanos_app_syscoin.gif +ICON_NANOX = icons/nanox_app_syscoin.gif +ICON_NANOSP = icons/nanox_app_syscoin.gif ICON_STAX = icons/stax_app_bitcoin.gif ######################################## diff --git a/src/ui/menu_bagl.c b/src/ui/menu_bagl.c index 59d8d22f..e3ee46e2 100644 --- a/src/ui/menu_bagl.c +++ b/src/ui/menu_bagl.c @@ -24,10 +24,10 @@ // We have a screen with the icon and "Syscoin is ready" for Syscoin, // "Syscoin Testnet is ready" for Syscoin Testnet. -UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_bitcoin_logo, "Syscoin", "is ready"}); +UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_syscoin_logo, "Syscoin", "is ready"}); UX_STEP_NOCB(ux_menu_ready_step_bitcoin_testnet, pnn, - {&C_bitcoin_logo, "Syscoin Testnet", "is ready"}); + {&C_syscoin_logo, "Syscoin Testnet", "is ready"}); UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); UX_STEP_CB(ux_menu_about_step, pb, ui_menu_about(), {&C_icon_certificate, "About"}); From c2ba8aa023617c0b5c1dd6c6b29a9988e7952cd6 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Tue, 19 Sep 2023 00:17:35 +0800 Subject: [PATCH 03/10] fix: check app name --- .doxygen/Doxyfile | 2 +- bitcoin_client_js/src/__tests__/appClient.test.ts | 2 +- bitcoin_client_js/src/lib/appClient.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.doxygen/Doxyfile b/.doxygen/Doxyfile index 7728a3c6..e39eac21 100644 --- a/.doxygen/Doxyfile +++ b/.doxygen/Doxyfile @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "Bitcoin" +PROJECT_NAME = "Syscoin" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/bitcoin_client_js/src/__tests__/appClient.test.ts b/bitcoin_client_js/src/__tests__/appClient.test.ts index fcf69441..4c4a3d4b 100644 --- a/bitcoin_client_js/src/__tests__/appClient.test.ts +++ b/bitcoin_client_js/src/__tests__/appClient.test.ts @@ -130,7 +130,7 @@ describe("test AppClient", () => { it("can retrieve the app's version", async () => { const result = await app.getAppAndVersion(); - expect(result.name).toEqual("Bitcoin Test"); + expect(result.name).toEqual('Syscoin Test'); expect(result.version.split(".")[0]).toEqual("2") }); diff --git a/bitcoin_client_js/src/lib/appClient.ts b/bitcoin_client_js/src/lib/appClient.ts index bb369779..e121b39e 100644 --- a/bitcoin_client_js/src/lib/appClient.ts +++ b/bitcoin_client_js/src/lib/appClient.ts @@ -421,13 +421,13 @@ export class AppClient { throw new Error('Invalid address index'); const appAndVer = await this.getAppAndVersion(); let network; - if (appAndVer.name === 'Bitcoin Test') { + if (appAndVer.name === 'Syscoin Test') { network = networks.testnet; - } else if (appAndVer.name === 'Bitcoin') { + } else if (appAndVer.name === 'Syscoin') { network = networks.bitcoin; } else { throw new Error( - `Invalid network: ${appAndVer.name}. Expected 'Bitcoin Test' or 'Bitcoin'.` + `Invalid network: ${appAndVer.name}. Expected 'Syscoin Test' or 'Syscoin'.` ); } let expression = walletPolicy.descriptorTemplate; From add834605bfaf64a787c1f6df17edb720845d135 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Thu, 21 Sep 2023 19:29:26 +0800 Subject: [PATCH 04/10] temporary use bitcoin logo --- Makefile | 2 +- icons/nanox_app_bitcoin.gif | Bin 0 -> 1122 bytes src/ui/menu_bagl.c | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 icons/nanox_app_bitcoin.gif diff --git a/Makefile b/Makefile index f20cac90..8a5ff3c4 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ endif # Application icons following guidelines: # https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon ICON_NANOS = icons/nanos_app_syscoin.gif -ICON_NANOX = icons/nanox_app_syscoin.gif +ICON_NANOX = icons/nanox_app_bitcoin.gif ICON_NANOSP = icons/nanox_app_syscoin.gif ICON_STAX = icons/stax_app_bitcoin.gif diff --git a/icons/nanox_app_bitcoin.gif b/icons/nanox_app_bitcoin.gif new file mode 100644 index 0000000000000000000000000000000000000000..7a92ac33e4cc5b089a6645ee648d2f102391c448 GIT binary patch literal 1122 zcmZ?wbhEHbtP)JEENd(gW?JEirle1Gx6p~WY zGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&%T$P<{nWAKGr(jcI1=O2clBiIT zo0C^;Rbi`?n3A8AY6WEHrj{h?D=C0glw{i~If5hXl~0)0b01CWOxKFuxg^~J9=Hy5lL z7!<`NL8%DWVl}roq_QAYKPa_0zqBYh6{uVpWK)5ab5UwyNq$jCetr%t2m>EvqSYHn<1 zXl!WcYGz?-Xy#;TU~X*eW^Vf5%*ha@*(E=@G&eP`1g1F!q1ghe8AUHhD=001f&>`A zMVV!(DQ-pixe8!^TV=xCg6@{ z(ZdJ#@7=v~`_|1H*RNf@a{1E53+K6ZM9qnzc zEzM1h4fS=kHPuy>73F26CB;RB1^IcoIoVm68R==MDalER3Gs2UG0{A;Cd` z0selzKHgrQ9`0_gF3wJl4)%7oHr7^_7UpKACdNjF2KsusI@(&A8tQ7QD#}WV3i5KY zGSX6#65?W_BEmv~0{ncuJUrZ7oE+?ItSrn-z=A>tR6u}A4F)FJmj0Eq8P7G%dE+Rl YA2L}lCzm6mc2-8oz3(CKx Date: Thu, 21 Sep 2023 19:39:48 +0800 Subject: [PATCH 05/10] update syscoinjs-lib --- desktop-wallet/package.json | 2 +- desktop-wallet/yarn.lock | 189 ++++++++++++++++++++---------------- 2 files changed, 105 insertions(+), 86 deletions(-) diff --git a/desktop-wallet/package.json b/desktop-wallet/package.json index f6a43f33..a686d5f2 100644 --- a/desktop-wallet/package.json +++ b/desktop-wallet/package.json @@ -59,6 +59,6 @@ "sass": "^1.60.0", "sass-loader": "^13.2.1", "satoshi-bitcoin": "^1.0.5", - "syscoinjs-lib": "^1.0.214" + "syscoinjs-lib": "^1.0.218" } } diff --git a/desktop-wallet/yarn.lock b/desktop-wallet/yarn.lock index 15862c11..73535895 100644 --- a/desktop-wallet/yarn.lock +++ b/desktop-wallet/yarn.lock @@ -853,31 +853,34 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@trezor/utxo-lib@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@trezor/utxo-lib/-/utxo-lib-0.1.2.tgz#19319a424b0d0c648b0df456f1849eb08697fb44" - integrity sha512-ONAsg8LAyZY9g6X/qgqloUHwWsqtRmrx2Wt3wkz+7LO2VdE69HktAHWUjJLK9drskNwly3Fp0GKK6ZbbtX+Ghw== +"@trezor/utils@9.0.13": + version "9.0.13" + resolved "https://registry.yarnpkg.com/@trezor/utils/-/utils-9.0.13.tgz#20665620e194648dc10150cfdbde46f8b698e45b" + integrity sha512-DvUKEC/Pc5/xOJT6UmQgc29AXakB1tftNo1XMMaDlRKnbDsofuSBiGnxK4pf/Emp5eem4D+9bdnrhHMmLBTQTQ== + +"@trezor/utxo-lib@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@trezor/utxo-lib/-/utxo-lib-1.0.11.tgz#6b47a12c7fd251de05c14d1c6ebb4ad59cc44e90" + integrity sha512-21UpKcwLyGGLfACNrg1vrGAmZ8ZSk+h2jtjzCVAOAktSu9fmsLXVBAK9cXfBwWTWjbLacykOvwW/V259vKLGaw== dependencies: - bech32 "0.0.3" - bigi "^1.4.0" - bip66 "^1.1.0" - bitcoin-ops "^1.3.0" - blake2b "https://github.com/BitGo/blake2b#6268e6dd678661e0acc4359e9171b97eb1ebf8ac" - bs58check "^2.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.3" - debug "~3.1.0" - ecurve "^1.0.0" - int64-buffer "0.99.1007" - merkle-lib "^2.0.10" + "@trezor/utils" "9.0.13" + bchaddrjs "^0.5.2" + bech32 "^2.0.0" + bip66 "^1.1.5" + bitcoin-ops "^1.4.1" + blake-hash "^2.0.0" + blakejs "^1.2.1" + bn.js "^5.2.1" + bs58 "^5.0.0" + bs58check "^3.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + int64-buffer "^1.0.1" pushdata-bitcoin "^1.0.1" - randombytes "^2.0.1" - safe-buffer "^5.0.1" - typeforce "^1.11.3" - varuint-bitcoin "^1.0.4" - wif "^2.0.1" - optionalDependencies: - secp256k1 "^3.5.2" + tiny-secp256k1 "^1.1.6" + typeforce "^1.18.0" + varuint-bitcoin "^1.1.2" + wif "^2.0.6" "@tsconfig/node10@^1.0.7": version "1.0.9" @@ -1766,6 +1769,16 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== +bchaddrjs@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/bchaddrjs/-/bchaddrjs-0.5.2.tgz#1f52b5077329774e7c82d4882964628106bb11a0" + integrity sha512-OO7gIn3m7ea4FVx4cT8gdlWQR2+++EquhdpWQJH9BQjK63tJJ6ngB3QMZDO6DiBoXiIGUsTPHjlrHVxPGcGxLQ== + dependencies: + bs58check "2.1.2" + buffer "^6.0.3" + cashaddrjs "0.4.4" + stream-browserify "^3.0.0" + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -1773,11 +1786,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bech32@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/bech32/-/bech32-0.0.3.tgz#736747c4a6531c5d8937d0400498de30e93b2f9c" - integrity sha512-O+K1w8P/aAOLcYwwQ4sbiPYZ51ZIW95lnS4/6nE8Aib/z+OOddQIIPdu2qi94qGDp4HhYy/wJotttXKkak1lXg== - bech32@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" @@ -1788,6 +1796,11 @@ bech32@^2.0.0: resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== +big-integer@1.6.36: + version "1.6.36" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" + integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== + big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -1798,11 +1811,6 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== -bigi@^1.1.0, bigi@^1.4.0: - version "1.4.2" - resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" - integrity sha512-ddkU+dFIuEIW8lE7ZwdIAf2UPoM90eaprg5m3YXAVVTmKlqV/9BX4A2M8BOK2yOq6/VgZFVhK6QAxJebhlbhzw== - bignumber.js@^9.0.0: version "9.1.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" @@ -1910,20 +1918,16 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -"blake2b-wasm@https://github.com/BitGo/blake2b-wasm#193cdb71656c1a6c7f89b05d0327bb9b758d071b": +blake-hash@^2.0.0: version "2.0.0" - resolved "https://github.com/BitGo/blake2b-wasm#193cdb71656c1a6c7f89b05d0327bb9b758d071b" - dependencies: - nanoassert "^1.0.0" - -"blake2b@https://github.com/BitGo/blake2b#6268e6dd678661e0acc4359e9171b97eb1ebf8ac": - version "2.1.3" - resolved "https://github.com/BitGo/blake2b#6268e6dd678661e0acc4359e9171b97eb1ebf8ac" + resolved "https://registry.yarnpkg.com/blake-hash/-/blake-hash-2.0.0.tgz#af184dce641951126d05b7d1c3de3224f538d66e" + integrity sha512-Igj8YowDu1PRkRsxZA7NVkdFNxH5rKv5cpLxQ0CVXSIA77pVYwCPRQJ2sMew/oneUpfuYRyjG6r8SmmmnbZb1w== dependencies: - blake2b-wasm "https://github.com/BitGo/blake2b-wasm#193cdb71656c1a6c7f89b05d0327bb9b758d071b" - nanoassert "^1.0.0" + node-addon-api "^3.0.0" + node-gyp-build "^4.2.2" + readable-stream "^3.6.0" -blakejs@^1.1.0: +blakejs@^1.1.0, blakejs@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== @@ -2072,7 +2076,7 @@ bs58@^5.0.0: dependencies: base-x "^4.0.0" -bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: +bs58check@2.1.2, bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== @@ -2081,6 +2085,14 @@ bs58check@<3.0.0, bs58check@^2.0.0, bs58check@^2.1.1, bs58check@^2.1.2: create-hash "^1.1.0" safe-buffer "^5.1.2" +bs58check@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-3.0.1.tgz#2094d13720a28593de1cba1d8c4e48602fdd841c" + integrity sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ== + dependencies: + "@noble/hashes" "^1.2.0" + bs58 "^5.0.0" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -2222,6 +2234,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +cashaddrjs@0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/cashaddrjs/-/cashaddrjs-0.4.4.tgz#169f1ae620d325db77700273d972282adeeee331" + integrity sha512-xZkuWdNOh0uq/mxJIng6vYWfTowZLd9F4GMAlp2DwFHlcCqCm91NtuAc47RuV4L7r4PYcY5p6Cr2OKNb4hnkWA== + dependencies: + big-integer "1.6.36" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -2708,13 +2727,6 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2955,14 +2967,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecurve@^1.0.0: - version "1.0.6" - resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797" - integrity sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w== - dependencies: - bigi "^1.1.0" - safe-buffer "^5.0.1" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -3461,12 +3465,19 @@ eth-lib@^0.1.26: ws "^3.0.0" xhr-request-promise "^0.1.2" -eth-object@^1.0.3, "eth-object@https://github.com/syscoin/eth-object.git": +eth-object@^1.0.3: version "1.0.3" resolved "https://github.com/syscoin/eth-object.git#b5ff300f57c136138b31d6e570c816a147e0f1c9" dependencies: eth-util-lite "^1.0.1" +"eth-object@git+https://github.com/syscoin/eth-object.git": + version "1.0.3" + uid b5ff300f57c136138b31d6e570c816a147e0f1c9 + resolved "git+https://github.com/syscoin/eth-object.git#b5ff300f57c136138b31d6e570c816a147e0f1c9" + dependencies: + eth-util-lite "^1.0.1" + eth-proof@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/eth-proof/-/eth-proof-2.1.6.tgz#6c8a468f82334d9c79347324e6eb237f1ecc965f" @@ -4627,7 +4638,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4642,10 +4653,10 @@ ini@^1.3.4, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -int64-buffer@0.99.1007: - version "0.99.1007" - resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-0.99.1007.tgz#211ea089a2fdb960070a2e77cd6d17dc456a5220" - integrity sha512-XDBEu44oSTqlvCSiOZ/0FoUkpWu/vwjJLGSKDabNISPQNZ5wub1FodGHBljRsrR0IXRPq7SslshZYMuA55CgTQ== +int64-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/int64-buffer/-/int64-buffer-1.0.1.tgz#c78d841b444cadf036cd04f8683696c740f15dca" + integrity sha512-+3azY4pXrjAupJHU1V9uGERWlhoqNswJNji6aD/02xac7oxol508AsMC5lxKhEqyZeDFy3enq5OGWXF4u75hiw== internal-slot@^1.0.5: version "1.0.5" @@ -5712,11 +5723,6 @@ nano-json-stream-parser@^0.1.2: resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" integrity sha512-9MqxMH/BSJC7dnLsEMPyfN5Dvoo49IsPFYMcHw3Bcfc2kN0lpHRBSzlMSVx4HGyJ7s9B31CyBTVehWJoQ8Ctew== -nanoassert@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d" - integrity sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ== - nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" @@ -5789,7 +5795,7 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-addon-api@^3.0.2, node-addon-api@^3.1.0: +node-addon-api@^3.0.0, node-addon-api@^3.0.2, node-addon-api@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== @@ -5838,6 +5844,11 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.2.1, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== +node-gyp-build@^4.2.2: + version "4.6.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" + integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== + node-gyp@^9.0.0: version "9.3.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4" @@ -6675,7 +6686,7 @@ readable-stream@^2.0.1, readable-stream@^2.2.8, readable-stream@^2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -6984,7 +6995,7 @@ scrypt-js@^3.0.0, scrypt-js@^3.0.1: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -secp256k1@^3.5.2, secp256k1@^3.8.0: +secp256k1@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d" integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw== @@ -7377,6 +7388,14 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +stream-browserify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" + integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA== + dependencies: + inherits "~2.0.4" + readable-stream "^3.5.0" + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -7538,12 +7557,12 @@ swarm-js@^0.1.40: tar "^4.0.2" xhr-request "^1.0.1" -syscoinjs-lib@^1.0.214: - version "1.0.214" - resolved "https://registry.yarnpkg.com/syscoinjs-lib/-/syscoinjs-lib-1.0.214.tgz#aaa2e2fc8355b60738f9dea3807ad9526286abcf" - integrity sha512-RURwpNoVpZsq0cs7wl/tjTLfydBA8/eJwmvz/YqNNI7a3WIDPa4Ayj3LtkezWIGEfAIe2KTcVaRKJ93232yLCg== +syscoinjs-lib@^1.0.218: + version "1.0.218" + resolved "https://registry.yarnpkg.com/syscoinjs-lib/-/syscoinjs-lib-1.0.218.tgz#a45fe2e70d8d26c12b0b924ee012cee959c6a599" + integrity sha512-9kpPvOcDmhK0KedmS1sgaKdbmwLayinxQ+dWcr5FBzIjdFuWB90gYSaQ4hY7sHj4SH3X4rIrTX7ULzrCwb2Tzg== dependencies: - "@trezor/utxo-lib" "^0.1.2" + "@trezor/utxo-lib" "^1.0.7" axios "^0.21.1" bip84 "^0.2.7" bitcoin-ops "^1.4.1" @@ -7553,15 +7572,15 @@ syscoinjs-lib@^1.0.214: eth-object "https://github.com/syscoin/eth-object.git" eth-proof "^2.1.6" node-localstorage "^2.1.6" - syscointx-js "^1.0.102" + syscointx-js "^1.0.106" trezor-connect "^8.1.29" varuint-bitcoin "^1.1.2" web3 "^1.4.0" -syscointx-js@^1.0.102: - version "1.0.105" - resolved "https://registry.yarnpkg.com/syscointx-js/-/syscointx-js-1.0.105.tgz#21d81b2bbd08a708d2570d28ea498581d598691a" - integrity sha512-CFdnU/eYXQwvDUqRMnTtlg/frSY8cVi9Zkop8WyjRO9x1ZDoC9JF/0H0zRULzBBW6YXksXQ/u6xSJ/512WRw8w== +syscointx-js@^1.0.106: + version "1.0.106" + resolved "https://registry.yarnpkg.com/syscointx-js/-/syscointx-js-1.0.106.tgz#ffd365de9bd99fcabe829517d38e1e849092b342" + integrity sha512-ckc+kyuyge6ct+LjdeLZkVdqAfylUQxULcOpWF9v7nL2WriIjETRRAHqSzc4WebaNnWOTd5NKkRp8ZFNO+1IeQ== dependencies: bitcoin-ops "^1.4.1" bitcoinjs-lib "^5.2.0" @@ -7676,7 +7695,7 @@ tiny-each-async@2.0.3: resolved "https://registry.yarnpkg.com/tiny-each-async/-/tiny-each-async-2.0.3.tgz#8ebbbfd6d6295f1370003fbb37162afe5a0a51d1" integrity sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA== -tiny-secp256k1@^1.1.1, tiny-secp256k1@^1.1.3: +tiny-secp256k1@^1.1.1, tiny-secp256k1@^1.1.3, tiny-secp256k1@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz#7e224d2bee8ab8283f284e40e6b4acb74ffe047c" integrity sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA== From a1070a1ef43496ac508645de0628ff47c8fddbb9 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Thu, 21 Sep 2023 20:40:53 +0800 Subject: [PATCH 06/10] use updated syscoin icon --- Makefile | 2 +- icons/nanox_app_bitcoin.gif | Bin 1122 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 icons/nanox_app_bitcoin.gif diff --git a/Makefile b/Makefile index 8a5ff3c4..f20cac90 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ endif # Application icons following guidelines: # https://developers.ledger.com/docs/embedded-app/design-requirements/#device-icon ICON_NANOS = icons/nanos_app_syscoin.gif -ICON_NANOX = icons/nanox_app_bitcoin.gif +ICON_NANOX = icons/nanox_app_syscoin.gif ICON_NANOSP = icons/nanox_app_syscoin.gif ICON_STAX = icons/stax_app_bitcoin.gif diff --git a/icons/nanox_app_bitcoin.gif b/icons/nanox_app_bitcoin.gif deleted file mode 100644 index 7a92ac33e4cc5b089a6645ee648d2f102391c448..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1122 zcmZ?wbhEHbtP)JEENd(gW?JEirle1Gx6p~WY zGxKbf-tXS8q>!0ns}yePYv5bpoSKp8QB{;0T;&&%T$P<{nWAKGr(jcI1=O2clBiIT zo0C^;Rbi`?n3A8AY6WEHrj{h?D=C0glw{i~If5hXl~0)0b01CWOxKFuxg^~J9=Hy5lL z7!<`NL8%DWVl}roq_QAYKPa_0zqBYh6{uVpWK)5ab5UwyNq$jCetr%t2m>EvqSYHn<1 zXl!WcYGz?-Xy#;TU~X*eW^Vf5%*ha@*(E=@G&eP`1g1F!q1ghe8AUHhD=001f&>`A zMVV!(DQ-pixe8!^TV=xCg6@{ z(ZdJ#@7=v~`_|1H*RNf@a{1E53+K6ZM9qnzc zEzM1h4fS=kHPuy>73F26CB;RB1^IcoIoVm68R==MDalER3Gs2UG0{A;Cd` z0selzKHgrQ9`0_gF3wJl4)%7oHr7^_7UpKACdNjF2KsusI@(&A8tQ7QD#}WV3i5KY zGSX6#65?W_BEmv~0{ncuJUrZ7oE+?ItSrn-z=A>tR6u}A4F)FJmj0Eq8P7G%dE+Rl YA2L}lCzm6mc2-8oz3(CKx Date: Fri, 22 Sep 2023 23:06:25 +0800 Subject: [PATCH 07/10] nanox logo --- Makefile | 2 +- glyphs/nanos_badge_syscoin.gif | Bin 0 -> 881 bytes glyphs/nanos_badge_syscoin_testnet.gif | Bin 0 -> 881 bytes glyphs/syscoin_logo.gif | Bin 92 -> 881 bytes src/ui/menu_bagl.c | 4 ++-- 5 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 glyphs/nanos_badge_syscoin.gif create mode 100644 glyphs/nanos_badge_syscoin_testnet.gif diff --git a/Makefile b/Makefile index f20cac90..040d642c 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ VARIANT_VALUES = syscoin_regtest syscoin # simplify for tests ifndef COIN -COIN=syscoin_regtest +COIN=syscoin endif ######################################## diff --git a/glyphs/nanos_badge_syscoin.gif b/glyphs/nanos_badge_syscoin.gif new file mode 100644 index 0000000000000000000000000000000000000000..bd6054f49062c02a772e74bb3e0d147f28037448 GIT binary patch literal 881 zcmZ?wbhEHbRZaks1Pu|GE8KLxPwv-pluZ~otQnX&L^u{GG&FMXYMIQKpm?x}SyqXMVdA5M?cBO?YgPm*9_x^_FPjoE Y@u5?ns=l8}XV9Y)6SQO5I5-%r0ZNGRZaks1Pu|GE8KLxPwv-pluZ~otQnX&L^u{GG&FMXYMIQKpm?x}SyqXMVdA5M?cBO?YgPm*9_x^_FPjoE Y@u5?ns=l8}XV9Y)6SQO5I5-%r0ZNGRZaks1Pu|GE8KLxPwv-pluZ~otQnX&L^u{GG&FMXYMIQKpm?x}SyqXMVdA5M?cBO?YgPm*9_x^_FPjoE Y@u5?ns=l8}XV9Y)6SQO5I5-%r0ZNGG0H o3PD;Jn50_zS8BeNNGf@FQ2DxvW$A{>#MHtmF4JDU5@E0g0D$Kk8~^|S diff --git a/src/ui/menu_bagl.c b/src/ui/menu_bagl.c index 59d8d22f..e3ee46e2 100644 --- a/src/ui/menu_bagl.c +++ b/src/ui/menu_bagl.c @@ -24,10 +24,10 @@ // We have a screen with the icon and "Syscoin is ready" for Syscoin, // "Syscoin Testnet is ready" for Syscoin Testnet. -UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_bitcoin_logo, "Syscoin", "is ready"}); +UX_STEP_NOCB(ux_menu_ready_step_bitcoin, pnn, {&C_syscoin_logo, "Syscoin", "is ready"}); UX_STEP_NOCB(ux_menu_ready_step_bitcoin_testnet, pnn, - {&C_bitcoin_logo, "Syscoin Testnet", "is ready"}); + {&C_syscoin_logo, "Syscoin Testnet", "is ready"}); UX_STEP_NOCB(ux_menu_version_step, bn, {"Version", APPVERSION}); UX_STEP_CB(ux_menu_about_step, pb, ui_menu_about(), {&C_icon_certificate, "About"}); From b9da31c1716303df9317aaf0d8489926cfd85747 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Fri, 22 Sep 2023 23:07:15 +0800 Subject: [PATCH 08/10] revert default coin to regtest --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 040d642c..f20cac90 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ VARIANT_VALUES = syscoin_regtest syscoin # simplify for tests ifndef COIN -COIN=syscoin +COIN=syscoin_regtest endif ######################################## From b5644eccd97295e268eca6df81bdf3dd3f496020 Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Fri, 22 Sep 2023 23:27:07 +0800 Subject: [PATCH 09/10] test nanox remove temp fix --- tests/test_get_extended_pubkey.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_get_extended_pubkey.py b/tests/test_get_extended_pubkey.py index 48b2ba9b..4da4fc8a 100644 --- a/tests/test_get_extended_pubkey.py +++ b/tests/test_get_extended_pubkey.py @@ -166,10 +166,10 @@ def ux_thread(): comm.wait_for_text_event("Confirm public key") comm.press_and_release("right") # Temporary fix for broken OCR - if (model == "nanox"): - comm.wait_for_text_event("111-/222-/333-") - else: - comm.wait_for_text_event("111'/222'/333'") + # if (model == "nanox"): + # comm.wait_for_text_event("111-/222-/333-") + # else: + comm.wait_for_text_event("111'/222'/333'") comm.press_and_release("right") comm.wait_for_text_event("not sure") # second line of "Reject if you're not sure" From f3f3714d05e08f974c2a8ecd98d45d7899532a9f Mon Sep 17 00:00:00 2001 From: Ted Ian Osias Date: Tue, 26 Sep 2023 21:03:29 +0800 Subject: [PATCH 10/10] Add stax icons --- .github/workflows/Dockerfile | 2 +- Makefile | 2 +- docker-compose.yml | 13 +++++++++++++ glyphs/Syscoin_64px.png | Bin 0 -> 720 bytes icons/stax_app_syscoin.gif | Bin 0 -> 1138 bytes src/ui/display_nbgl.c | 26 +++++++++++++------------- src/ui/menu_nbgl.c | 4 ++-- 7 files changed, 30 insertions(+), 17 deletions(-) create mode 100644 glyphs/Syscoin_64px.png create mode 100644 icons/stax_app_syscoin.gif diff --git a/.github/workflows/Dockerfile b/.github/workflows/Dockerfile index e4841bcd..136cfac3 100644 --- a/.github/workflows/Dockerfile +++ b/.github/workflows/Dockerfile @@ -41,4 +41,4 @@ ENV PATH=/syscoin-${SYSCOIN_VERSION}/bin:$PATH EXPOSE 8369 8370 18369 18370 18443 18444 38332 38333 -RUN syscoind -version | grep "Syscoin Core version v${SYSCOIN_VERSION}" \ No newline at end of file +# RUN syscoind -version | grep "Syscoin Core version v${SYSCOIN_VERSION}" \ No newline at end of file diff --git a/Makefile b/Makefile index f20cac90..12ecfd67 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ endif ICON_NANOS = icons/nanos_app_syscoin.gif ICON_NANOX = icons/nanox_app_syscoin.gif ICON_NANOSP = icons/nanox_app_syscoin.gif -ICON_STAX = icons/stax_app_bitcoin.gif +ICON_STAX = icons/stax_app_syscoin.gif ######################################## # Application communication interfaces # diff --git a/docker-compose.yml b/docker-compose.yml index b4568678..2f202dec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,3 +13,16 @@ services: - "41000:41000" # vnc command: "--model nanox ./apps/syscoin.elf --seed secret --display headless --apdu-port 40000 --vnc-port 41000 --api-port 5002" # Add `--vnc-password ""` for macos users to use built-in vnc client. + + stax: + build: .github/workflows + # image: syscoin-speculos + volumes: + - ./src:/speculos/src + - ./build/stax/bin/app.elf:/speculos/apps/syscoin.elf + ports: + - "5004:5000" # api + - "40004:40004" # apdu + entrypoint: ["python", "./speculos.py"] + command: "--model stax ./apps/syscoin.elf --seed secret --display headless --api-port 5000" + # Add `--vnc-password ""` for macos users to use built-in vnc client. diff --git a/glyphs/Syscoin_64px.png b/glyphs/Syscoin_64px.png new file mode 100644 index 0000000000000000000000000000000000000000..a294c614dc9449026e9226b7c150b3611f4313d2 GIT binary patch literal 720 zcmV;>0x$iEP)JA&VE@+?)H500HY4@8FeEfv-keDr;8oQj+MqF@ct z(;~?4+Y_aP$7u;7xQcm8AzJWPB&br$GOl>?00otLo%6IuSe59mS@;DCswKo8DvZES zkUMWse_#|D(pHWpw6=2}6OP$N$XFw7ouhy!wIC-(jUw7oWAUF_@tO3kpNB}Gj?$J< zW)8+0fn+Tx_(d!9tfGk?AJJx93G}S()ZL6;&?~zy-2}j?5;WOku#~SA?-*Ulu|W4s z3P3;kA6sBANX?p*@P$NCFt0vN(j(DZ)OVXM@J=(Scw_%>C0HQ>?M$Tb7X@dwAgjG) z-VrJi61S3;MU`IR-U34T*t}+T)J=X`{;JIWKzb|`rn?!S;}C2B0000KYBDaZy;RmKm>AJ7PJhnI{?8**RK3;Wz;q5gh&IVr3vV})ZGRiop z=j|!G(!#{G*k