From 068566c2a298b5fa50546eb2f28d85f4eb4b97b4 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:28:27 +0200 Subject: [PATCH 01/53] Check bound on address index in get_wallet_address --- src/handler/get_wallet_address.c | 4 ++++ tests/test_get_wallet_address.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/handler/get_wallet_address.c b/src/handler/get_wallet_address.c index 93dc7c648..752ef471d 100644 --- a/src/handler/get_wallet_address.c +++ b/src/handler/get_wallet_address.c @@ -93,6 +93,10 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi 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]; diff --git a/tests/test_get_wallet_address.py b/tests/test_get_wallet_address.py index 008e0cb50..526637cb8 100644 --- a/tests/test_get_wallet_address.py +++ b/tests/test_get_wallet_address.py @@ -231,3 +231,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) From 6e80438eb4708c0fa0e2947b7820e95364beeaf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Wed, 19 Apr 2023 16:05:30 +0200 Subject: [PATCH 02/53] docs: Add official npm package information to bitcoin_client_js/README.md --- bitcoin_client_js/README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bitcoin_client_js/README.md b/bitcoin_client_js/README.md index 1069b5e1d..efd4ab53c 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 From c8de9256c21d0157689caa07f489e051d680174c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Wed, 19 Apr 2023 16:07:07 +0200 Subject: [PATCH 03/53] Remove unused dependency tiny-secp256k1 from package.json --- bitcoin_client_js/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bitcoin_client_js/package.json b/bitcoin_client_js/package.json index 3399aaa34..2f1e5b070 100644 --- a/bitcoin_client_js/package.json +++ b/bitcoin_client_js/package.json @@ -31,8 +31,7 @@ "dependencies": { "@ledgerhq/hw-transport": "^6.20.0", "bip32-path": "^0.4.2", - "bitcoinjs-lib": "^6.0.1", - "tiny-secp256k1": "^2.1.2" + "bitcoinjs-lib": "^6.0.1" }, "devDependencies": { "@ledgerhq/hw-transport-node-speculos-http": "^6.24.1", @@ -71,4 +70,4 @@ "prettier": { "singleQuote": true } -} \ No newline at end of file +} From 22298b8710b03e5de6abee41fb06f95d640941e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Luis=20Landabaso=20D=C3=ADaz?= Date: Wed, 19 Apr 2023 16:27:41 +0200 Subject: [PATCH 04/53] Expose PartialSignature for usage by 3rd parties --- bitcoin_client_js/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bitcoin_client_js/src/index.ts b/bitcoin_client_js/src/index.ts index 929a27583..01a2be10c 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 }; From a2f1632102cd94c0556e85719f5a3da866322b06 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 21 Apr 2023 16:39:05 +0200 Subject: [PATCH 05/53] Fix get_extended_pubkey and get_wallet_address not returning to dashboard when done --- src/ui/display.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/display.c b/src/ui/display.c index e1ed85589..4d32ac746 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -72,7 +72,7 @@ bool ui_display_pubkey(dispatcher_context_t *context, ui_display_pubkey_suspicious_flow(); } - return io_ui_process(context, false); + return io_ui_process(context, true); } bool ui_display_message_hash(dispatcher_context_t *context, @@ -145,7 +145,7 @@ bool ui_display_wallet_address(dispatcher_context_t *context, ui_display_receive_in_wallet_flow(); } - return io_ui_process(context, false); + return io_ui_process(context, true); } bool ui_authorize_wallet_spend(dispatcher_context_t *context, const char *wallet_name) { From bdc48a2baef0c79d8d44ae5b33fa01512bc07a64 Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Tue, 25 Apr 2023 17:39:15 +0200 Subject: [PATCH 06/53] Add a workflow triggering the Exchange tests with the current Bitcoin branch --- .github/workflows/swap-ci-workflow.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/swap-ci-workflow.yml diff --git a/.github/workflows/swap-ci-workflow.yml b/.github/workflows/swap-ci-workflow.yml new file mode 100644 index 000000000..8702214ba --- /dev/null +++ b/.github/workflows/swap-ci-workflow.yml @@ -0,0 +1,15 @@ +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 }} From 63d2ee0d5d4b05a68f73484567d216161d4b2bbe Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Wed, 26 Apr 2023 11:23:28 +0200 Subject: [PATCH 07/53] Only run Swap tests related to Bitcoin --- .github/workflows/swap-ci-workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/swap-ci-workflow.yml b/.github/workflows/swap-ci-workflow.yml index 8702214ba..85225ecac 100644 --- a/.github/workflows/swap-ci-workflow.yml +++ b/.github/workflows/swap-ci-workflow.yml @@ -13,3 +13,4 @@ jobs: 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"' From 05014fa02457e7e9dcb99f21a127f05bb24c7d2d Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Tue, 2 May 2023 13:37:10 +0200 Subject: [PATCH 08/53] Reset BSS at start in swap mode --- src/swap/handle_swap_sign_transaction.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index a5eb96134..0fc7f1aee 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" @@ -41,6 +42,7 @@ 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_state.amount = read_u64_be(amount, 0); G_swap_state.fees = read_u64_be(fees, 0); memcpy(G_swap_state.destination_address, From dc59e4bae77a9bc9f29b4f0dd8734fe0d3475641 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 4 May 2023 12:02:19 +0200 Subject: [PATCH 09/53] Add guidelines enforcer to CI --- .github/workflows/guidelines-enforcer.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/guidelines-enforcer.yml diff --git a/.github/workflows/guidelines-enforcer.yml b/.github/workflows/guidelines-enforcer.yml new file mode 100644 index 000000000..c154d6cf5 --- /dev/null +++ b/.github/workflows/guidelines-enforcer.yml @@ -0,0 +1,22 @@ +name: Ensure compliance with Ledger guidelines + +# This workflow is mandatory in all applications +# It calls a reusable workflow guidelines_enforcer developed by Ledger's internal developer team. +# The successful completion of the reusable workflow is a mandatory step for an app to be available on the Ledger +# application store. +# +# More information on the guidelines can be found in the repository: +# LedgerHQ/ledger-app-workflows/ + +on: + workflow_dispatch: + push: + branches: + - master + - develop + pull_request: + +jobs: + guidelines_enforcer: + name: Call Ledger guidelines_enforcer + uses: LedgerHQ/ledger-app-workflows/.github/workflows/reusable_guidelines_enforcer.yml@v1 From bc4bf10d172b70f09bd9c39be53bcbfd787bc349 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 4 May 2023 13:49:19 +0200 Subject: [PATCH 10/53] Fix clang-analyzer issues --- src/boilerplate/dispatcher.h | 2 +- src/boilerplate/io.c | 2 +- src/common/script.c | 3 ++- src/crypto.c | 6 ++---- src/debug-helpers/debug.c | 11 +++++------ src/handler/lib/get_preimage.c | 3 +-- src/handler/lib/stream_preimage.c | 3 +-- src/swap/btchip_bcd.c | 1 - src/ui/display_nbgl.c | 6 +++--- src/ui/display_utils.c | 2 +- 10 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/boilerplate/dispatcher.h b/src/boilerplate/dispatcher.h index 22d5b5186..b95450947 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 d01bf9990..9a4af0835 100644 --- a/src/boilerplate/io.c +++ b/src/boilerplate/io.c @@ -99,7 +99,7 @@ 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({}); diff --git a/src/common/script.c b/src/common/script.c index 0c645b7fc..6190bbc37 100644 --- a/src/common/script.c +++ b/src/common/script.c @@ -1,5 +1,6 @@ #include #include +#include #include #include "../common/bip32.h" @@ -122,7 +123,7 @@ 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 diff --git a/src/crypto.c b/src/crypto.c index fd051d09a..381fad858 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -276,8 +276,7 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], uint8_t pubkey[static 33], uint8_t chain_code[]) { struct { - uint8_t prefix; - uint8_t raw_public_key[64]; + uint8_t raw_public_key[65]; uint8_t chain_code[32]; } keydata; @@ -287,7 +286,6 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], 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); @@ -298,7 +296,7 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], // 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); + memmove(keydata.raw_public_key, public_key.W, 65); // compute compressed public key if (crypto_get_compressed_pubkey((uint8_t *) &keydata, pubkey) < 0) { diff --git a/src/debug-helpers/debug.c b/src/debug-helpers/debug.c index 3a9dc5be4..d01f24363 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/lib/get_preimage.c b/src/handler/lib/get_preimage.c index ea0d0f759..b5f5f5cd7 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/stream_preimage.c b/src/handler/lib/stream_preimage.c index bea9a1082..cd6a0c795 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/swap/btchip_bcd.c b/src/swap/btchip_bcd.c index 36f3e47f3..20e993838 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/ui/display_nbgl.c b/src/ui/display_nbgl.c index 0b0abc350..a02c2ee04 100644 --- a/src/ui/display_nbgl.c +++ b/src/ui/display_nbgl.c @@ -8,9 +8,9 @@ #include "io.h" typedef struct { - char *confirm; // text displayed in last transaction page - char *confirmed_status; // text displayed in confirmation page (after long press) - char *rejected_status; // text displayed in rejection page (after reject confirmed) + 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; diff --git a/src/ui/display_utils.c b/src/ui/display_utils.c index f19d8f5ce..30ce98c14 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; From d6e1ba0c2069786d43bdde47789e0464909b27ac Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 5 May 2023 09:55:26 +0200 Subject: [PATCH 11/53] Add --curve and --path_slip21 params --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f7af6867a..045bddfe5 100644 --- a/Makefile +++ b/Makefile @@ -22,9 +22,11 @@ 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 = "" +APP_LOAD_PARAMS += --curve secp256k1 +APP_LOAD_PARAMS += --path "" +APP_LOAD_PARAMS += --path_slip21 "LEDGER-Wallet policy" APPVERSION_M = 2 APPVERSION_N = 1 From cd83dafbe2cb2ed3bac27a141f2a399daa9c7477 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 10 May 2023 16:28:43 +0200 Subject: [PATCH 12/53] Update CHANGELOG for version 2.1.2 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69d129c01..9b1215dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Dates are in `dd-mm-yyyy` format. ### 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 From 16a2999c68cde7bc16927c97f1df401a6cd63f01 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 24 May 2023 11:49:10 +0200 Subject: [PATCH 13/53] Refactor state related to outputs; remove unused variable; slightly simplify read_outputs --- src/handler/sign_psbt.c | 75 ++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 7ed08c2da..c8640c8a6 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -130,12 +130,15 @@ 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 inputs_total_amount; - uint64_t internal_inputs_total_value; - - 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; @@ -155,10 +158,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 - int external_outputs_total_count; // Total count of external outputs in the transaction } sign_psbt_state_t; /* BIP0341 tags for computing the tagged hashes when computing he sighash */ @@ -931,7 +930,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) { @@ -963,7 +962,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; @@ -984,7 +983,6 @@ 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_policy_segwit_version(&st->wallet_policy_map); @@ -1147,6 +1145,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; @@ -1189,8 +1188,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, - st->external_outputs_total_count, + external_outputs_count, + st->outputs.n_external, output_address, COIN_COINID_SHORT, output->value)) { @@ -1205,6 +1204,10 @@ 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)); @@ -1249,7 +1252,7 @@ static bool read_outputs(dispatcher_context_t *dc, uint64_t value = read_u64_le(raw_result, 0); output.value = value; - st->outputs_total_value += value; + st->outputs.total_amount += value; } // Read the output's scriptPubKey @@ -1274,21 +1277,22 @@ static bool read_outputs(dispatcher_context_t *dc, SEND_SW(dc, SW_INCORRECT_DATA); return false; } else if (is_internal == 0) { - if (dry_run) { - ++st->external_outputs_total_count; - } else { - // external output, user needs to validate - ++st->external_outputs_count; - } + // external output, user needs to validate + ++external_outputs_count; - if (!dry_run && !display_output(dc, st, cur_output_index, &output)) return false; + 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; } @@ -1307,12 +1311,13 @@ process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { if (!find_first_internal_key_placeholder(dc, st, &placeholder_info)) return false; - // Dry run to get the total amount of external outputs - st->external_outputs_total_count = 0; + memset(&st->outputs, 0, sizeof(st->outputs)); + + // Dry run to get the total count of external outputs if (!read_outputs(dc, st, &placeholder_info, true)) return false; #ifdef HAVE_NBGL - if (!ui_transaction_prompt(dc, st->external_outputs_total_count)) { + if (!ui_transaction_prompt(dc, st->outputs.n_external)) { SEND_SW(dc, SW_DENY); return false; } @@ -1327,39 +1332,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); From e8f053e932f89b584440f50f1c84ca12fdf8787a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 24 May 2023 13:50:47 +0200 Subject: [PATCH 14/53] Update (c) notice, and improved dashboard wording for testnet app on Stax --- src/ui/menu_bagl.c | 2 +- src/ui/menu_nbgl.c | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ui/menu_bagl.c b/src/ui/menu_bagl.c index 65654198e..b0f1b835e 100644 --- a/src/ui/menu_bagl.c +++ b/src/ui/menu_bagl.c @@ -57,7 +57,7 @@ UX_FLOW(ux_menu_main_flow_bitcoin_testnet, &ux_menu_exit_step, FLOW_LOOP); -UX_STEP_NOCB(ux_menu_info_step, bn, {"Bitcoin App", "(c) 2022 Ledger"}); +UX_STEP_NOCB(ux_menu_info_step, bn, {"Bitcoin App", "(c) 2023 Ledger"}); UX_STEP_CB(ux_menu_back_step, pb, ui_menu_main(), {&C_icon_back, "Back"}); // FLOW for the about submenu: diff --git a/src/ui/menu_nbgl.c b/src/ui/menu_nbgl.c index 2ff32066f..b87af5078 100644 --- a/src/ui/menu_nbgl.c +++ b/src/ui/menu_nbgl.c @@ -22,7 +22,7 @@ #include "menu.h" static const char* const infoTypes[] = {"Version", "Developer", "Copyright"}; -static const char* const infoContents[] = {APPVERSION, "Ledger", "(c) 2022 Ledger"}; +static const char* const infoContents[] = {APPVERSION, "Ledger", "(c) 2023 Ledger"}; static bool navigation_cb(uint8_t page, nbgl_pageContent_t* content) { UNUSED(page); @@ -42,7 +42,12 @@ void ui_menu_main_flow_bitcoin(void) { } void ui_menu_main_flow_bitcoin_testnet(void) { - nbgl_useCaseHome("Bitcoin Testnet", &C_Bitcoin_64px, NULL, false, ui_menu_about, exit); + nbgl_useCaseHome("Bitcoin Test", + &C_Bitcoin_64px, + "This app enables signing\ntransactions on all the Bitcoin\ntest networks.", + false, + ui_menu_about, + exit); } void ui_menu_about(void) { @@ -50,6 +55,6 @@ void ui_menu_about(void) { } void ui_menu_about_testnet(void) { - nbgl_useCaseSettings("Bitcoin Testnet", 0, 1, false, ui_menu_main, navigation_cb, NULL); + nbgl_useCaseSettings("Bitcoin Test", 0, 1, false, ui_menu_main, navigation_cb, NULL); } #endif // HAVE_NBGL From 1f1c6c99445406903d6d3b651a98bebebdb78cf8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 24 May 2023 14:41:38 +0200 Subject: [PATCH 15/53] Only perform the dry-run for the outputs on Stax --- src/handler/sign_psbt.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index c8640c8a6..5743dc5ab 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1313,10 +1313,14 @@ process_outputs(dispatcher_context_t *dc, sign_psbt_state_t *st) { memset(&st->outputs, 0, sizeof(st->outputs)); - // Dry run to get the total count of external 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; -#ifdef HAVE_NBGL if (!ui_transaction_prompt(dc, st->outputs.n_external)) { SEND_SW(dc, SW_DENY); return false; From e5701a54c41a78575c4d99fd720088eb66ff2696 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 26 May 2023 09:38:18 +0200 Subject: [PATCH 16/53] Add test for a complicated vault setup --- tests/test_e2e_miniscript.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_e2e_miniscript.py b/tests/test_e2e_miniscript.py index f23bd6b53..a7bb09d87 100644 --- a/tests/test_e2e_miniscript.py +++ b/tests/test_e2e_miniscript.py @@ -275,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 From 960c2ab7d9c89e48a592d65cb1ad2dadb40db654 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 26 May 2023 09:40:21 +0200 Subject: [PATCH 17/53] thresh has the 'd' and 'u' properties --- src/common/wallet.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/wallet.c b/src/common/wallet.c index 73c3c9da9..9f7817079 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -1343,8 +1343,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; From 141e58b0be5139657a5419bc0e1ab82f41df673a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 26 May 2023 10:07:14 +0200 Subject: [PATCH 18/53] Fix encoding of thresh when n == 1 --- src/handler/lib/policy.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 5ea589338..0f023b3f2 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -675,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 @@ -700,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); From 3e66f6e16558fd93991f0fd9f1fc2e9715a85c07 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 26 May 2023 17:12:21 +0200 Subject: [PATCH 19/53] Check return value of crypto_derive_private_key --- src/crypto.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 381fad858..652344b1c 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -283,11 +283,16 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], cx_ecfp_private_key_t private_key = {0}; cx_ecfp_public_key_t public_key; - bool result = true; + bool result = false; BEGIN_TRY { TRY { // derive private key according to BIP32 path - crypto_derive_private_key(&private_key, keydata.chain_code, bip32_path, bip32_path_len); + if (crypto_derive_private_key(&private_key, + keydata.chain_code, + bip32_path, + bip32_path_len) < 0) { + goto end; + } if (chain_code != NULL) { memmove(chain_code, keydata.chain_code, 32); @@ -300,13 +305,15 @@ bool crypto_get_compressed_pubkey_at_path(const uint32_t bip32_path[], // compute compressed public key if (crypto_get_compressed_pubkey((uint8_t *) &keydata, pubkey) < 0) { - result = false; + goto end; } + result = true; } CATCH_ALL { result = false; } FINALLY { + end: // delete sensitive data explicit_bzero(keydata.chain_code, 32); explicit_bzero(&private_key, sizeof(private_key)); From 6846a91cabbaaadd13f98347a6271d93163a7277 Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 12:08:27 +0200 Subject: [PATCH 20/53] Makefile: Revamp the Makefile using SDK Makefile.standard_app features This commit doesn't affect generated `bin/app.apdu` (same sha256sum). --- Makefile | 163 +++++++++++++++++-------------------------------------- 1 file changed, 50 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index 045bddfe5..be5ee8f48 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # **************************************************************************** # Ledger App for Bitcoin -# (c) 2021 Ledger SAS. +# (c) 2023 Ledger SAS. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,26 +23,37 @@ include $(BOLOS_SDK)/Makefile.defines # TODO: compile with the right path restrictions -APP_LOAD_PARAMS = $(COMMON_LOAD_PARAMS) -APP_LOAD_PARAMS += --curve secp256k1 -APP_LOAD_PARAMS += --path "" -APP_LOAD_PARAMS += --path_slip21 "LEDGER-Wallet policy" +# 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 = 1 APPVERSION_P = 2 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" - APP_STACK_SIZE = 3072 +# Setting to allow building variant applications +VARIANT_PARAM = COIN +VARIANT_VALUES = bitcoin_testnet bitcoin + # simplify for tests ifndef COIN COIN=bitcoin_testnet 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),bitcoin_testnet) @@ -76,33 +87,39 @@ $(error Unsupported COIN - use bitcoin_testnet, bitcoin) endif endif -APP_LOAD_PARAMS += $(APP_LOAD_FLAGS) +# 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 -ifeq ($(TARGET_NAME),TARGET_NANOS) -ICONNAME=icons/nanos_app_bitcoin.gif -else ifeq ($(TARGET_NAME),TARGET_STAX) -ICONNAME=icons/stax_app_bitcoin.gif -else -ICONNAME=icons/nanox_app_bitcoin.gif -endif +######################################## +# Application communication interfaces # +######################################## +ENABLE_BLUETOOTH = 1 -all: default +######################################## +# NBGL custom features # +######################################## +ENABLE_NBGL_QRCODE = 1 -# TODO: double check if all those flags are still relevant/needed (was copied from legacy app-bitcoin) +######################################## +# Features disablers # +######################################## +# Don't use standard app file to avoid conflicts for now +DISABLE_STANDARD_APP_FILES = 1 -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_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 +# Don't use default IO_SEPROXY_BUFFER_SIZE to use another +# value for NANOS for an unknown reason. +DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE = 1 -DEFINES += HAVE_WEBUSB WEBUSB_URL_SIZE_B=0 WEBUSB_URL="" +# Don't use STANDARD_USB as we want IO_USB_MAX_ENDPOINTS=4 +# and the default is 6 +DISABLE_STANDARD_USB = 1 +DEFINES += HAVE_IO_USB HAVE_L4_USBLIB IO_USB_MAX_ENDPOINTS=4 IO_HID_EP_LENGTH=64 HAVE_USB_APDU DEFINES += UNUSED\(x\)=\(void\)x -DEFINES += APPVERSION=\"$(APPVERSION)\" - DEFINES += HAVE_BOLOS_APP_STACK_CANARY @@ -113,24 +130,6 @@ else DEFINES += IO_SEPROXYHAL_BUFFER_SIZE_B=300 endif -ifeq ($(TARGET_NAME),TARGET_STAX) - DEFINES += NBGL_QRCODE -else - DEFINES += HAVE_BAGL HAVE_UX_FLOW - ifneq ($(TARGET_NAME),TARGET_NANOS) - 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 -endif - -ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOX TARGET_STAX)) -DEFINES += HAVE_BLE BLE_COMMAND_TIMEOUT_MS=2000 -DEFINES += HAVE_BLE_APDU # basic ledger apdu transport over BLE -endif - ifeq ($(TARGET_NAME),TARGET_NANOS) # enables optimizations using the shared 1K CXRAM region DEFINES += USE_CXRAM_SECTION @@ -141,81 +140,19 @@ CFLAGS += -include debug-helpers/debug.h # DEFINES += HAVE_PRINT_STACK_POINTER -ifndef DEBUG - DEBUG = 0 +ifeq ($(DEBUG),10) + $(warning Using semihosted PRINTF. Only run with speculos!) + DEFINES += HAVE_PRINTF HAVE_SEMIHOSTED_PRINTF PRINTF=semihosted_printf 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 -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 -ifneq ($(TARGET_NAME),TARGET_STAX) -SDK_SOURCE_PATH += lib_ux -endif - -ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOX TARGET_STAX)) - 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 bitcoin_testnet bitcoin - +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 From 54b2831c6cb5f724aedab952dca5da59606d8097 Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 16:36:00 +0200 Subject: [PATCH 21/53] Makefile: Enable lib_standard_app crypto_helpers.c usage --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index be5ee8f48..9c423ed00 100644 --- a/Makefile +++ b/Makefile @@ -152,6 +152,10 @@ INCLUDES_PATH += $(BOLOS_SDK)/lib_cxng/src APP_SOURCE_PATH += src SDK_SOURCE_PATH += lib_stusb lib_stusb_impl +# Allow usage of function from lib_standard_app/crypto_helpers.c +INCLUDES_PATH += ${BOLOS_SDK} +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 From daa18285edaa8815d4fd19f4d3ea4f1a6769af19 Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 16:36:21 +0200 Subject: [PATCH 22/53] src: crypto.c: Simplify crypto_get_compressed_pubkey_at_path() --- src/crypto.c | 59 +++++++++++++--------------------------------------- 1 file changed, 15 insertions(+), 44 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 652344b1c..93d7f4bab 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" @@ -275,52 +276,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 raw_public_key[65]; - uint8_t chain_code[32]; - } keydata; - - cx_ecfp_private_key_t private_key = {0}; - cx_ecfp_public_key_t public_key; - - bool result = false; - BEGIN_TRY { - TRY { - // derive private key according to BIP32 path - if (crypto_derive_private_key(&private_key, - keydata.chain_code, - bip32_path, - bip32_path_len) < 0) { - goto end; - } - - 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, 65); + 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) { - goto end; - } - result = true; - } - CATCH_ALL { - result = false; - } - FINALLY { - end: - // 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]) { From 4bc0dcb475ccf2703dd0ef2fcf2a9134dd3f50bc Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 17:15:13 +0200 Subject: [PATCH 23/53] src: crypto.c: Simplify crypto_ecdsa_sign_sha256_hash_with_key() --- src/crypto.c | 65 +++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 93d7f4bab..03022907d 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -412,38 +412,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 From 2ca624822e63a2625a9c2977b7fece26b28e0632 Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 17:22:08 +0200 Subject: [PATCH 24/53] src: crypto.c: Replace crypto_derive_private_key by equivalent bip32_derive_init_privkey_256 --- src/crypto.c | 35 ----------------------------------- src/crypto.h | 20 -------------------- src/handler/sign_psbt.c | 8 +++++++- 3 files changed, 7 insertions(+), 56 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 03022907d..a5fece132 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -90,41 +90,6 @@ static int secp256k1_point(const uint8_t k[static 32], uint8_t out[static 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; -} - int bip32_CKDpub(const serialized_extended_pubkey_t *parent, uint32_t index, serialized_extended_pubkey_t *child) { diff --git a/src/crypto.h b/src/crypto.h index a5cf0e532..a04ae39a6 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. diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 5743dc5ab..2502b11ac 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" @@ -1963,7 +1965,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; } From d23603fbb301e45bcaa8bc93e84c8f749ee96c4f Mon Sep 17 00:00:00 2001 From: Xavier Chapron Date: Mon, 29 May 2023 17:34:47 +0200 Subject: [PATCH 25/53] src: crypto.c: Remove the need for TRY/FINALLY block when using crypto_derive_symmetric_key --- src/crypto.c | 24 ++++++++++++--------- src/crypto.h | 3 +-- src/handler/lib/policy.c | 45 +++++++++++++++++++--------------------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index a5fece132..4bb60f30c 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -273,23 +273,27 @@ 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? diff --git a/src/crypto.h b/src/crypto.h index a04ae39a6..a961aef3c 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -276,7 +276,6 @@ int get_serialized_extended_pubkey_at_path(const uint32_t bip32_path[], /** * 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. @@ -285,7 +284,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/handler/lib/policy.c b/src/handler/lib/policy.c index 5ea589338..27c8fac72 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -1270,18 +1270,17 @@ bool compute_wallet_hmac(const uint8_t wallet_id[static 32], uint8_t wallet_hmac 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; } @@ -1291,22 +1290,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; } From 1894450e433b8bee42718efbf0689e074cee7fed Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 30 May 2023 10:38:26 +0200 Subject: [PATCH 26/53] Rewrite functions in crypto.c using throwing functions with their _no_throw equivalent --- src/crypto.c | 127 +++++++++++++++++++++++++-------------------------- src/crypto.h | 12 ++--- 2 files changed, 67 insertions(+), 72 deletions(-) diff --git a/src/crypto.c b/src/crypto.c index 4bb60f30c..37c0a194a 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -79,15 +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); + 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, @@ -100,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]; @@ -118,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; } @@ -127,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) } } @@ -214,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; @@ -457,24 +457,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 @@ -543,7 +546,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; } @@ -554,13 +558,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; @@ -575,45 +579,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 a961aef3c..89ac3adfd 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -50,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. @@ -59,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 @@ -76,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); } /** From 2b6b8423fb2c125a95093a7daa419670f0541735 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 26 May 2023 14:17:06 +0200 Subject: [PATCH 27/53] Fix comment in and_n specs; fix calculation for 'o' for or_c --- src/common/wallet.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/wallet.c b/src/common/wallet.c index 9f7817079..96ac0f4b5 100644 --- a/src/common/wallet.c +++ b/src/common/wallet.c @@ -988,7 +988,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 +1118,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; From 92d482258bfd2df55011a0b37786c26476007708 Mon Sep 17 00:00:00 2001 From: edouard Date: Mon, 3 Apr 2023 15:36:35 +0200 Subject: [PATCH 28/53] Add tapscript signature to rust client --- bitcoin_client_rs/Cargo.toml | 2 +- .../examples/ledger_hwi/src/main.rs | 20 +- bitcoin_client_rs/src/async_client.rs | 44 +- bitcoin_client_rs/src/client.rs | 50 +- bitcoin_client_rs/src/lib.rs | 2 +- bitcoin_client_rs/src/psbt.rs | 70 ++- bitcoin_client_rs/tests/client.rs | 41 +- bitcoin_client_rs/tests/data/sign_psbt.json | 444 +++++++++++++++++- 8 files changed, 595 insertions(+), 78 deletions(-) diff --git a/bitcoin_client_rs/Cargo.toml b/bitcoin_client_rs/Cargo.toml index b53d20c23..71d1025b9 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.3" +version = "0.2.0" authors = ["Edouard Paris "] edition = "2018" description = "Ledger Bitcoin application client" diff --git a/bitcoin_client_rs/examples/ledger_hwi/src/main.rs b/bitcoin_client_rs/examples/ledger_hwi/src/main.rs index 5326b4afe..6e62ac999 100644 --- a/bitcoin_client_rs/examples/ledger_hwi/src/main.rs +++ b/bitcoin_client_rs/examples/ledger_hwi/src/main.rs @@ -14,6 +14,7 @@ use regex::Regex; use ledger_bitcoin_client::{ async_client::{BitcoinClient, Transport}, + psbt::PartialSignature, wallet::{Version, WalletPolicy, WalletPubKey}, }; @@ -178,8 +179,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| h.to_hex()) + .unwrap_or("none".to_string()), + sig.to_vec().to_hex() + ); + } + } } Ok(()) } diff --git a/bitcoin_client_rs/src/async_client.rs b/bitcoin_client_rs/src/async_client.rs index 5edd9c598..72b112f9e 100644 --- a/bitcoin_client_rs/src/async_client.rs +++ b/bitcoin_client_rs/src/async_client.rs @@ -8,10 +8,8 @@ use bitcoin::{ secp256k1::ecdsa::Signature, util::{ bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, - ecdsa::EcdsaSig, psbt::PartiallySignedTransaction as Psbt, }, - PublicKey, }; use crate::{ @@ -219,9 +217,8 @@ impl BitcoinClient { psbt: &Psbt, wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, - ) -> Result, BitcoinClientError> { + ) -> Result, BitcoinClientError> { self.validate_policy(&wallet).await?; - let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -293,40 +290,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 32c935b3d..bcf521dba 100644 --- a/bitcoin_client_rs/src/client.rs +++ b/bitcoin_client_rs/src/client.rs @@ -3,13 +3,11 @@ use core::str::FromStr; use bitcoin::{ consensus::encode::{deserialize_partial, VarInt}, - secp256k1::ecdsa::Signature, + secp256k1::ecdsa, util::{ bip32::{DerivationPath, ExtendedPubKey, Fingerprint}, - ecdsa::EcdsaSig, psbt::PartiallySignedTransaction as Psbt, }, - PublicKey, }; use crate::{ @@ -203,9 +201,8 @@ impl BitcoinClient { psbt: &Psbt, wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, - ) -> Result, BitcoinClientError> { + ) -> Result, BitcoinClientError> { self.validate_policy(&wallet)?; - let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -277,40 +274,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) @@ -322,7 +300,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); @@ -330,7 +308,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/lib.rs b/bitcoin_client_rs/src/lib.rs index 8c7fbd420..dc751c4cd 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/psbt.rs b/bitcoin_client_rs/src/psbt.rs index d82a393d4..6345ce265 100644 --- a/bitcoin_client_rs/src/psbt.rs +++ b/bitcoin_client_rs/src/psbt.rs @@ -5,8 +5,16 @@ /// rust-bitcoin currently support V0. use bitcoin::{ blockdata::transaction::{TxIn, TxOut}, - consensus::encode::{deserialize, serialize, VarInt}, - util::psbt::{raw, Input, Output, Psbt}, + consensus::encode::{deserialize, serialize, Error, VarInt}, + secp256k1, + util::{ + ecdsa::{EcdsaSig, EcdsaSigError}, + key::Error as KeyError, + psbt::{raw, serialize::Deserialize, Input, Output, Psbt}, + schnorr::{SchnorrSig, SchnorrSigError}, + taproot::TapLeafHash, + }, + PublicKey, XOnlyPublicKey, }; #[rustfmt::skip] @@ -377,3 +385,61 @@ pub fn get_v2_output_pairs(output: &Output, txout: &TxOut) -> Vec { pub fn deserialize_pairs(pair: raw::Pair) -> (Vec, Vec) { (deserialize(&serialize(&pair.key)).unwrap(), pair.value) } + +pub enum PartialSignature { + /// signature stored in pbst.partial_sigs + Sig(PublicKey, EcdsaSig), + /// signature stored in pbst.tap_script_sigs + TapScriptSig(XOnlyPublicKey, Option, SchnorrSig), +} + +impl PartialSignature { + pub fn from_slice(slice: &[u8]) -> Result { + let key_augment_byte = slice + .get(0) + .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::deserialize(&slice[33..65]).map_err(PartialSignatureError::TapLeaf)?; + let sig = SchnorrSig::from_slice(&slice[65..])?; + 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 = SchnorrSig::from_slice(&slice[65..])?; + Ok(Self::TapScriptSig(key, None, sig)) + } else { + let key = PublicKey::from_slice(&slice[1..key_augment_len + 1]) + .map_err(PartialSignatureError::PubKey)?; + let sig = EcdsaSig::from_slice(&slice[key_augment_len + 1..])?; + Ok(Self::Sig(key, sig)) + } + } +} + +pub enum PartialSignatureError { + BadKeyAugmentLength, + XOnlyPubKey(secp256k1::Error), + PubKey(KeyError), + EcdsaSig(EcdsaSigError), + SchnorrSig(SchnorrSigError), + TapLeaf(Error), +} + +impl From for PartialSignatureError { + fn from(e: SchnorrSigError) -> PartialSignatureError { + PartialSignatureError::SchnorrSig(e) + } +} + +impl From for PartialSignatureError { + fn from(e: EcdsaSigError) -> PartialSignatureError { + PartialSignatureError::EcdsaSig(e) + } +} diff --git a/bitcoin_client_rs/tests/client.rs b/bitcoin_client_rs/tests/client.rs index 04f6175dc..e5863523c 100644 --- a/bitcoin_client_rs/tests/client.rs +++ b/bitcoin_client_rs/tests/client.rs @@ -6,7 +6,7 @@ use bitcoin::{ hashes::hex::{FromHex, ToHex}, util::{bip32::DerivationPath, 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"); @@ -300,6 +300,11 @@ 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()) @@ -314,9 +319,41 @@ async fn test_sign_psbt() { .sign_psbt(&psbt, &wallet, hmac.as_ref()) .unwrap(); - let _res = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) + let res = async_client::BitcoinClient::new(utils::TransportReplayer::new(store.clone())) .sign_psbt(&psbt, &wallet, hmac.as_ref()) .await .unwrap(); + + 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_hex() + ); + 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, tapleaf_hash.unwrap().to_hex()); + } + assert_eq!( + res_sig + .get("sig") + .map(|v| serde_json::from_value::(v.clone()).unwrap()) + .unwrap(), + sig.to_vec().to_hex() + ); + } + _ => {} + } + } + } + } } } diff --git a/bitcoin_client_rs/tests/data/sign_psbt.json b/bitcoin_client_rs/tests/data/sign_psbt.json index a1d286f60..593afcff4 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" + } ] } ] From 34e1d77799d350eafc063be499fa7b0e707dbfd6 Mon Sep 17 00:00:00 2001 From: edouard Date: Tue, 30 May 2023 12:37:40 +0200 Subject: [PATCH 29/53] fix: clippy warnings --- bitcoin_client_rs/src/async_client.rs | 8 ++++---- bitcoin_client_rs/src/client.rs | 8 ++++---- bitcoin_client_rs/src/psbt.rs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitcoin_client_rs/src/async_client.rs b/bitcoin_client_rs/src/async_client.rs index 72b112f9e..549b41fff 100644 --- a/bitcoin_client_rs/src/async_client.rs +++ b/bitcoin_client_rs/src/async_client.rs @@ -90,7 +90,7 @@ impl BitcoinClient { if data.is_empty() || data[0] != 0x01 { return Err(BitcoinClientError::UnexpectedResult { command: cmd.ins, - data: data.clone(), + data, }); } @@ -151,7 +151,7 @@ impl BitcoinClient { &self, wallet: &WalletPolicy, ) -> Result<([u8; 32], [u8; 32]), BitcoinClientError> { - self.validate_policy(&wallet).await?; + self.validate_policy(wallet).await?; let cmd = command::register_wallet(wallet); let mut intpr = ClientCommandInterpreter::new(); @@ -188,7 +188,7 @@ impl BitcoinClient { address_index: u32, display: bool, ) -> Result> { - self.validate_policy(&wallet).await?; + self.validate_policy(wallet).await?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); @@ -218,7 +218,7 @@ impl BitcoinClient { wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, ) -> Result, BitcoinClientError> { - self.validate_policy(&wallet).await?; + self.validate_policy(wallet).await?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); diff --git a/bitcoin_client_rs/src/client.rs b/bitcoin_client_rs/src/client.rs index bcf521dba..eed052125 100644 --- a/bitcoin_client_rs/src/client.rs +++ b/bitcoin_client_rs/src/client.rs @@ -80,7 +80,7 @@ impl BitcoinClient { if data.is_empty() || data[0] != 0x01 { return Err(BitcoinClientError::UnexpectedResult { command: cmd.ins, - data: data.clone(), + data, }); } @@ -139,7 +139,7 @@ impl BitcoinClient { &self, wallet: &WalletPolicy, ) -> Result<([u8; 32], [u8; 32]), BitcoinClientError> { - self.validate_policy(&wallet)?; + self.validate_policy(wallet)?; let cmd = command::register_wallet(wallet); let mut intpr = ClientCommandInterpreter::new(); @@ -174,7 +174,7 @@ impl BitcoinClient { address_index: u32, display: bool, ) -> Result> { - self.validate_policy(&wallet)?; + self.validate_policy(wallet)?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); @@ -202,7 +202,7 @@ impl BitcoinClient { wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, ) -> Result, BitcoinClientError> { - self.validate_policy(&wallet)?; + self.validate_policy(wallet)?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); diff --git a/bitcoin_client_rs/src/psbt.rs b/bitcoin_client_rs/src/psbt.rs index 6345ce265..924b47cca 100644 --- a/bitcoin_client_rs/src/psbt.rs +++ b/bitcoin_client_rs/src/psbt.rs @@ -74,7 +74,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 }, }); @@ -396,7 +396,7 @@ pub enum PartialSignature { impl PartialSignature { pub fn from_slice(slice: &[u8]) -> Result { let key_augment_byte = slice - .get(0) + .first() .ok_or(PartialSignatureError::BadKeyAugmentLength)?; let key_augment_len = u8::from_le_bytes([*key_augment_byte]) as usize; From a866e3f1a71be7d16d7c2328d4189f693463f953 Mon Sep 17 00:00:00 2001 From: Fionn Fitzmaurice Date: Fri, 9 Jun 2023 19:02:04 +0800 Subject: [PATCH 30/53] Declare type annotations The Python module is statically typed, but does not export the type information. This adds the py.typed marker as per PEP 561. --- bitcoin_client/ledger_bitcoin/py.typed | 0 bitcoin_client/setup.cfg | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 bitcoin_client/ledger_bitcoin/py.typed diff --git a/bitcoin_client/ledger_bitcoin/py.typed b/bitcoin_client/ledger_bitcoin/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/bitcoin_client/setup.cfg b/bitcoin_client/setup.cfg index 235d722b5..9d44335f8 100644 --- a/bitcoin_client/setup.cfg +++ b/bitcoin_client/setup.cfg @@ -22,6 +22,9 @@ install_requires= ledgercomm>=1.1.0 packaging>=21.3 +[options.package_data] +* = py.typed + [options.extras_require] hid = hidapi>=0.9.0.post3 From dc5abbede15aed38840246cb1865ea30b947f8dd Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Tue, 13 Jun 2023 15:35:44 +0200 Subject: [PATCH 31/53] Update Exchange API file --- src/main.c | 18 ++-------- src/swap/swap_lib_calls.h | 72 +++++++++++++++++++++++++++------------ 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/main.c b/src/main.c index 7cf5c1c2a..49bacff0b 100644 --- a/src/main.c +++ b/src/main.c @@ -45,18 +45,6 @@ #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_BOLOS_APP_STACK_CANARY extern unsigned int app_stack_canary; #endif @@ -259,7 +247,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) { @@ -310,7 +298,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 */ @@ -344,7 +332,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/swap_lib_calls.h b/src/swap/swap_lib_calls.h index d41d37ab9..dc88417ae 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; From 356ad4715bd876467a91f879ad403f4ffca37d12 Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Tue, 13 Jun 2023 15:56:03 +0200 Subject: [PATCH 32/53] Return the Swap status to Exchange --- src/main.c | 3 ++- src/swap/handle_swap_sign_transaction.c | 10 ++++++++++ src/swap/handle_swap_sign_transaction.h | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 49bacff0b..abf912a3a 100644 --- a/src/main.c +++ b/src/main.c @@ -151,7 +151,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); } } } diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index 0fc7f1aee..61e1d2203 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -11,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]; @@ -43,6 +46,8 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti 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, @@ -50,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 d961b94cf..bbd82b24b 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); From dc8fd4ff0ea2a88f6835dfa4ca4f3fed014b53ee Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 29 May 2023 14:29:41 +0200 Subject: [PATCH 33/53] Compare generated addresses with the expected ones in the client library --- 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 +++++++ bitcoin_client/ledger_bitcoin/client.py | 52 +- bitcoin_client/ledger_bitcoin/segwit_addr.py | 137 ++ bitcoin_client/pyproject.toml | 2 + bitcoin_client/setup.cfg | 2 + 23 files changed, 4048 insertions(+), 21 deletions(-) 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 create mode 100644 bitcoin_client/ledger_bitcoin/segwit_addr.py diff --git a/bitcoin_client/ledger_bitcoin/bip380/README b/bitcoin_client/ledger_bitcoin/bip380/README new file mode 100644 index 000000000..3609525a4 --- /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 000000000..27fdca497 --- /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 000000000..bc4eaac4d --- /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 000000000..9f3e01326 --- /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 000000000..f7b58483a --- /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 000000000..1d18bffdd --- /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 000000000..25dbfe94f --- /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 000000000..3e05b61d5 --- /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 000000000..b0de1f9c7 --- /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 000000000..7ccd98f4e --- /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 000000000..d0e572eeb --- /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 000000000..2058b7b6b --- /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 000000000..5cff50b79 --- /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 000000000..67e878060 --- /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 000000000..e69de29bb 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 000000000..138493918 --- /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 000000000..1124dc57a --- /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 000000000..a4043de9b --- /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 000000000..9ff0e703d --- /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/client.py b/bitcoin_client/ledger_bitcoin/client.py index bb058aa55..60ca5eec0 100644 --- a/bitcoin_client/ledger_bitcoin/client.py +++ b/bitcoin_client/ledger_bitcoin/client.py @@ -2,7 +2,8 @@ from typing import Tuple, List, Mapping, Optional, Union import base64 from io import BytesIO, BufferedReader -import re + +from .bip380.descriptors import Descriptor from .command_builder import BitcoinCommandBuilder, BitcoinInsType from .common import Chain, read_uint, read_varint @@ -14,6 +15,7 @@ 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 @@ -52,11 +54,6 @@ def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSign return PartialSignature(signature=signature, pubkey=pubkey_augm) -def _contains_a_fragment(desc_tmp: str) -> bool: - """Returns true if the given descriptor template contains the `a:` fragment.""" - return any('a' in match for match in re.findall(r'[asctdvjnlu]+:', desc_tmp)) - - class NewClient(Client): # internal use for testing: if set to True, sign_psbt will not clone the psbt before converting to psbt version 2 _no_clone_psbt: bool = False @@ -94,8 +91,6 @@ def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]: if wallet.version not in [WalletType.WALLET_POLICY_V1, WalletType.WALLET_POLICY_V2]: raise ValueError("invalid wallet policy version") - self._validate_policy(wallet) - client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_preimage(wallet.serialize()) client_intepreter.add_known_list([k.encode() for k in wallet.keys_info]) @@ -116,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( @@ -133,8 +135,6 @@ def get_wallet_address( if change != 0 and change != 1: raise ValueError("Invalid change") - self._validate_policy(wallet) - client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_list([k.encode() for k in wallet.keys_info]) client_intepreter.add_known_preimage(wallet.serialize()) @@ -152,11 +152,17 @@ def get_wallet_address( if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS) - return response.decode() + result = response.decode() - def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]: + if self._should_validate_address(wallet): + # sanity check: for miniscripts, derive the address independently with python-bip380 - self._validate_policy(wallet) + 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) @@ -265,14 +271,18 @@ def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: return base64.b64encode(response).decode('utf-8') - def _validate_policy(self, wallet_policy: WalletPolicy): - """Performs any additional checks before we use a wallet policy""" - if _contains_a_fragment(wallet_policy.descriptor_template): - _, app_version, _ = self.get_version() - if app_version in ["2.1.0", "2.1.1"]: - # Versions 2.1.0 and 2.1.1 produced incorrect scripts for policies containing - # the `a:` fragment. - raise RuntimeError("Please update your Ledger Bitcoin app.") + 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]: diff --git a/bitcoin_client/ledger_bitcoin/segwit_addr.py b/bitcoin_client/ledger_bitcoin/segwit_addr.py new file mode 100644 index 000000000..ef4174773 --- /dev/null +++ b/bitcoin_client/ledger_bitcoin/segwit_addr.py @@ -0,0 +1,137 @@ +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" + + +from enum import Enum + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret \ No newline at end of file diff --git a/bitcoin_client/pyproject.toml b/bitcoin_client/pyproject.toml index f7473453b..894551620 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 9d44335f8..21656c888 100644 --- a/bitcoin_client/setup.cfg +++ b/bitcoin_client/setup.cfg @@ -18,6 +18,8 @@ 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 From 278637ecf34ace8d81cd14aefed53b7dd29c640a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 29 May 2023 15:46:23 +0200 Subject: [PATCH 34/53] Update embit and bip32 in test suites --- bitcoin_client/tests/requirements.txt | 4 ++-- test_utils/requirements.txt | 2 +- tests/requirements.txt | 4 ++-- tests_mainnet/requirements.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bitcoin_client/tests/requirements.txt b/bitcoin_client/tests/requirements.txt index 4127c1405..23a57de0e 100644 --- a/bitcoin_client/tests/requirements.txt +++ b/bitcoin_client/tests/requirements.txt @@ -3,6 +3,6 @@ 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/test_utils/requirements.txt b/test_utils/requirements.txt index cd006bce6..c06bc9eb3 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/requirements.txt b/tests/requirements.txt index 4127c1405..23a57de0e 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -3,6 +3,6 @@ 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/requirements.txt b/tests_mainnet/requirements.txt index 4127c1405..23a57de0e 100644 --- a/tests_mainnet/requirements.txt +++ b/tests_mainnet/requirements.txt @@ -3,6 +3,6 @@ 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 From 601695d828c4fcdbcdf31c0636c601ccbdd7c386 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 29 May 2023 14:50:52 +0200 Subject: [PATCH 35/53] Fix typing issue with bs58check --- bitcoin_client_js/src/lib/bip32.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitcoin_client_js/src/lib/bip32.ts b/bitcoin_client_js/src/lib/bip32.ts index 0275bb172..091cf66f1 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), From 5b6c1414aeea9d6cc0bd2ae6ccc19723e86e4340 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 13 Jun 2023 15:53:04 +0200 Subject: [PATCH 36/53] Compare addresses with rust-miniscript in the Rust client library --- .github/workflows/ci-workflow.yml | 2 +- bitcoin_client_rs/Cargo.toml | 4 +- bitcoin_client_rs/src/async_client.rs | 74 +++++++++++++++++++-------- bitcoin_client_rs/src/client.rs | 71 +++++++++++++++++-------- bitcoin_client_rs/src/error.rs | 2 + bitcoin_client_rs/src/wallet.rs | 64 +++++++++++++---------- 6 files changed, 147 insertions(+), 70 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 27f0c526b..cf9b663eb 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -264,4 +264,4 @@ jobs: - name: Run tests run: | cd bitcoin_client_rs/ - cargo test + cargo test --no-default-features --features="async" diff --git a/bitcoin_client_rs/Cargo.toml b/bitcoin_client_rs/Cargo.toml index 71d1025b9..dc139ff2f 100644 --- a/bitcoin_client_rs/Cargo.toml +++ b/bitcoin_client_rs/Cargo.toml @@ -9,12 +9,14 @@ license = "Apache-2.0" documentation = "https://docs.rs/ledger_bitcoin_client/" [features] -default = ["async"] +default = ["async", "paranoid_client"] async = ["async-trait"] +paranoid_client = ["miniscript"] [dependencies] async-trait = { version = "0.1", optional = true } bitcoin = { version = "0.29.1", default-features = false, features = ["no-std"] } +miniscript = { version = "9.0.1", optional = true, default-features = false, features = ["no-std"] } [workspace] members = ["examples/ledger_hwi"] diff --git a/bitcoin_client_rs/src/async_client.rs b/bitcoin_client_rs/src/async_client.rs index 549b41fff..52bb42e3c 100644 --- a/bitcoin_client_rs/src/async_client.rs +++ b/bitcoin_client_rs/src/async_client.rs @@ -12,13 +12,16 @@ use bitcoin::{ }, }; +#[cfg(feature = "paranoid_client")] +use miniscript::{Descriptor, DescriptorPublicKey}; + use crate::{ apdu::{APDUCommand, StatusWord}, command, error::BitcoinClientError, interpreter::{get_merkleized_map_commitment, ClientCommandInterpreter}, psbt::*, - wallet::{contains_a, WalletPolicy}, + wallet::WalletPolicy, }; /// BitcoinClient calls and interprets commands with the Ledger Device. @@ -66,18 +69,31 @@ impl BitcoinClient { } } - async fn validate_policy( + // 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> { - if contains_a(&wallet.descriptor_template) { - let (_, version, _) = self.get_version().await?; - if version == "2.1.0" || version == "2.1.1" { - // Versions 2.1.0 and 2.1.1 produced incorrect scripts for policies containing - // the `a:` fragment. - return Err(BitcoinClientError::UnsupportedAppVersion); - } + 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) + .script_pubkey() + != expected_address.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(()) } @@ -151,8 +167,6 @@ impl BitcoinClient { &self, wallet: &WalletPolicy, ) -> Result<([u8; 32], [u8; 32]), BitcoinClientError> { - self.validate_policy(wallet).await?; - let cmd = command::register_wallet(wallet); let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); @@ -160,7 +174,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 { @@ -171,11 +186,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), @@ -188,8 +214,6 @@ impl BitcoinClient { address_index: u32, display: bool, ) -> Result> { - self.validate_policy(wallet).await?; - 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 +221,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(|_| { @@ -206,7 +231,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). @@ -218,7 +251,6 @@ impl BitcoinClient { wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, ) -> Result, BitcoinClientError> { - self.validate_policy(wallet).await?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); diff --git a/bitcoin_client_rs/src/client.rs b/bitcoin_client_rs/src/client.rs index eed052125..91b712244 100644 --- a/bitcoin_client_rs/src/client.rs +++ b/bitcoin_client_rs/src/client.rs @@ -10,13 +10,16 @@ use bitcoin::{ }, }; +#[cfg(feature = "paranoid_client")] +use miniscript::{Descriptor, DescriptorPublicKey}; + use crate::{ apdu::{APDUCommand, StatusWord}, command, error::BitcoinClientError, interpreter::{get_merkleized_map_commitment, ClientCommandInterpreter}, psbt::*, - wallet::{contains_a, WalletPolicy}, + wallet::WalletPolicy, }; /// BitcoinClient calls and interprets commands with the Ledger Device. @@ -61,15 +64,31 @@ impl BitcoinClient { } } - fn validate_policy(&self, wallet: &WalletPolicy) -> Result<(), BitcoinClientError> { - if contains_a(&wallet.descriptor_template) { - let (_, version, _) = self.get_version()?; - if version == "2.1.0" || version == "2.1.1" { - // Versions 2.1.0 and 2.1.1 produced incorrect scripts for policies containing - // the `a:` fragment. - return Err(BitcoinClientError::UnsupportedAppVersion); - } + // 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) + .script_pubkey() + != expected_address.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(()) } @@ -139,8 +158,6 @@ impl BitcoinClient { &self, wallet: &WalletPolicy, ) -> Result<([u8; 32], [u8; 32]), BitcoinClientError> { - self.validate_policy(wallet)?; - let cmd = command::register_wallet(wallet); let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); @@ -148,7 +165,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, @@ -157,11 +174,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), @@ -174,8 +199,6 @@ impl BitcoinClient { address_index: u32, display: bool, ) -> Result> { - self.validate_policy(wallet)?; - let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); @@ -183,14 +206,21 @@ 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| { + 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). @@ -202,7 +232,6 @@ impl BitcoinClient { wallet: &WalletPolicy, wallet_hmac: Option<&[u8; 32]>, ) -> Result, BitcoinClientError> { - self.validate_policy(wallet)?; let mut intpr = ClientCommandInterpreter::new(); intpr.add_known_preimage(wallet.serialize()); let keys: Vec = wallet.keys.iter().map(|k| k.to_string()).collect(); diff --git a/bitcoin_client_rs/src/error.rs b/bitcoin_client_rs/src/error.rs index 9b0f5521e..fce642c62 100644 --- a/bitcoin_client_rs/src/error.rs +++ b/bitcoin_client_rs/src/error.rs @@ -4,11 +4,13 @@ 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, } diff --git a/bitcoin_client_rs/src/wallet.rs b/bitcoin_client_rs/src/wallet.rs index 04009d266..523306539 100644 --- a/bitcoin_client_rs/src/wallet.rs +++ b/bitcoin_client_rs/src/wallet.rs @@ -140,6 +140,31 @@ 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 + if 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()); @@ -151,6 +176,7 @@ impl WalletPolicy { pub enum WalletError { InvalidThreshold, UnsupportedAddressType, + InvalidPolicy, } pub struct WalletPubKey { @@ -252,25 +278,6 @@ impl core::fmt::Display for WalletPubKey { } } -/// Returns true if `descriptor_template` contains an 'a:' fragment -pub fn contains_a(descriptor_template: &str) -> bool { - const MINISCRIPT_WRAPPERS: &[char] = &['a', 's', 'c', 't', 'd', 'v', 'j', 'n', 'l', 'u']; - let mut sequence = String::new(); - - for ch in descriptor_template.chars() { - if MINISCRIPT_WRAPPERS.contains(&ch) { - sequence.push(ch); - } else { - if ch == ':' && sequence.contains('a') { - return true; - } - sequence.clear(); - } - } - - false -} - #[cfg(test)] mod tests { use super::*; @@ -328,13 +335,18 @@ mod tests { } #[test] - fn test_contains_a() { - assert_eq!(contains_a("wsh(pkh(@0))"), false); - assert_eq!( - contains_a("wsh(c:andor(pk(@0/**),pk_k(@1/**),and_v(v:pk(@2/**),pk_k(@3/**))))"), - false + 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!(contains_a("wsh(a:pkh(@0))"), true); - assert_eq!(contains_a("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)))))"), true); + + 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/*))"); } } From 5feaa063ba97cbd26786c1115144a9f2e15a5b86 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 13 Jun 2023 16:49:23 +0200 Subject: [PATCH 37/53] Document paranoid_client feature --- bitcoin_client_rs/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bitcoin_client_rs/Cargo.toml b/bitcoin_client_rs/Cargo.toml index dc139ff2f..9b09e41d6 100644 --- a/bitcoin_client_rs/Cargo.toml +++ b/bitcoin_client_rs/Cargo.toml @@ -11,6 +11,12 @@ documentation = "https://docs.rs/ledger_bitcoin_client/" [features] 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] From 375277b1a85bbc705b326c75a220d5216991ae40 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:04:41 +0200 Subject: [PATCH 38/53] Reject policies with "thresh(1," in the JS client --- bitcoin_client_js/src/lib/appClient.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/bitcoin_client_js/src/lib/appClient.ts b/bitcoin_client_js/src/lib/appClient.ts index 9392a797f..ddfa169b6 100644 --- a/bitcoin_client_js/src/lib/appClient.ts +++ b/bitcoin_client_js/src/lib/appClient.ts @@ -404,14 +404,32 @@ export class AppClient { /* Performs any additional checks on the policy before using it.*/ private async validatePolicy(walletPolicy: WalletPolicy) { + // TODO: Once an independent implementation of miniscript in JavaScript is available, + // we will replace the checks in this section with a generic comparison between the + // address produced by the app and the one computed locally (like the python and Rust + // clients). Until then, we filter for any known bug. + + let appAndVer = undefined; + if (containsA(walletPolicy.descriptorTemplate)) { - const appAndVer = await this.getAppAndVersion(); + appAndVer = appAndVer || await this.getAppAndVersion(); if (["2.1.0", "2.1.1"].includes(appAndVer.version)) { // Versions 2.1.0 and 2.1.1 produced incorrect scripts for policies containing // the `a:` fragment. throw new Error("Please update your Ledger Bitcoin app.") } } + + if (walletPolicy.descriptorTemplate.includes("thresh(1,")) { + appAndVer = appAndVer || await this.getAppAndVersion(); + if (["2.1.0", "2.1.1", "2.1.2"].includes(appAndVer.version)) { + // Versions 2.1.0 and 2.1.1 and "2.1.2" produced incorrect scripts for policies + // containing an unusual thresh fragment with k = n = 1, that is "thresh(1,X)". + // (The check above has false positives, as it also matches "thresh" fragments + // where n > 1; however, better to be overzealous). + throw new Error("Please update your Ledger Bitcoin app.") + } + } } } From 10bd3732d1837e5bfc3866cefd9d9ad937d54ea2 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:22:14 +0200 Subject: [PATCH 39/53] Allow naked OP_RETURN outputs --- src/common/script.c | 104 +++++++++++++++++++++------------------ tests/test_sign_psbt.py | 21 ++++++++ unit-tests/test_script.c | 6 +-- 3 files changed, 81 insertions(+), 50 deletions(-) diff --git a/src/common/script.c b/src/common/script.c index 6190bbc37..aeb35144c 100644 --- a/src/common/script.c +++ b/src/common/script.c @@ -126,57 +126,67 @@ int format_opscript_script(const uint8_t script[], 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/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index a6d630516..8f9ab440f 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -791,6 +791,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/unit-tests/test_script.c b/unit-tests/test_script.c index 93ff6f55b..1361d49ea 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); From b22692b7cc2c8d743ea211886bceedb17010344a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 19 Jun 2023 13:28:25 +0200 Subject: [PATCH 40/53] Add special wording for self-transfers --- src/handler/sign_psbt.c | 19 ++++++++++++----- src/ui/display.c | 11 ++++++++++ src/ui/display.h | 4 ++++ src/ui/display_bagl.c | 18 ++++++++++++++++- src/ui/display_nbgl.c | 45 ++++++++++++++++++++++++++++++++++++++--- tests/test_sign_psbt.py | 19 +++++++++++++++++ 6 files changed, 107 insertions(+), 9 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 2502b11ac..154b0f82a 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1378,11 +1378,20 @@ 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)) { - SEND_SW(dc, SW_DENY); - ui_post_processing_confirm_transaction(dc, false); - return false; - }; + if (st->outputs.n_external == 0) { + // All outputs are change; show the user it's a self transfer + if (!ui_validate_selftransfer(dc, COIN_COINID_SHORT, fee)) { + SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_transaction(dc, false); + return false; + } + } else { + if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee)) { + SEND_SW(dc, SW_DENY); + ui_post_processing_confirm_transaction(dc, false); + return false; + } + } } return true; diff --git a/src/ui/display.c b/src/ui/display.c index 4d32ac746..63742c5d1 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -209,6 +209,17 @@ bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_nam return io_ui_process(context, true); } +// Special case when all the outputs are change: show a "Self-transfer" screen in the flow +bool ui_validate_selftransfer(dispatcher_context_t *context, const char *coin_name, uint64_t fee) { + ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state; + + format_sats_amount(coin_name, fee, state->fee); + + ui_accept_selftransfer_flow(); + + return io_ui_process(context, true); +} + #ifdef HAVE_BAGL bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success) { (void) context; diff --git a/src/ui/display.h b/src/ui/display.h index cda6ea5d9..9026b4cfd 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -134,6 +134,8 @@ bool ui_validate_output(dispatcher_context_t *context, bool ui_validate_transaction(dispatcher_context_t *context, const char *coin_name, uint64_t fee); +bool ui_validate_selftransfer(dispatcher_context_t *context, const char *coin_name, uint64_t fee); + void set_ux_flow_response(bool approved); void ui_display_pubkey_flow(void); @@ -164,6 +166,8 @@ void ui_display_output_address_amount_no_index_flow(int index); void ui_accept_transaction_flow(void); +void ui_accept_selftransfer_flow(void); + void ui_display_transaction_prompt(const int external_outputs_total_count); bool ui_post_processing_confirm_wallet_registration(dispatcher_context_t *context, bool success); diff --git a/src/ui/display_bagl.c b/src/ui/display_bagl.c index 9d8e5e808..38486e2c4 100644 --- a/src/ui/display_bagl.c +++ b/src/ui/display_bagl.c @@ -168,6 +168,7 @@ UX_STEP_NOCB(ux_validate_address_step, }); 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, { @@ -386,7 +387,7 @@ UX_FLOW(ux_display_output_address_amount_flow, &ux_display_reject_step); // Finalize see the transaction fees and finally accept signing -// #1 screen: eye icon + "Confirm Transaction" +// #1 screen: eye icon + "Confirm transaction" // #2 screen: fee amount // #3 screen: "Accept and send", with approve button // #4 screen: reject button @@ -396,6 +397,17 @@ UX_FLOW(ux_accept_transaction_flow, &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); } @@ -458,4 +470,8 @@ void ui_accept_transaction_flow(void) { ux_flow_init(0, ux_accept_transaction_flow, NULL); } +void ui_accept_selftransfer_flow(void) { + ux_flow_init(0, ux_accept_selftransfer_flow, NULL); +} + #endif // HAVE_BAGL diff --git a/src/ui/display_nbgl.c b/src/ui/display_nbgl.c index a02c2ee04..1a423c0f1 100644 --- a/src/ui/display_nbgl.c +++ b/src/ui/display_nbgl.c @@ -22,7 +22,8 @@ enum { CANCEL_TOKEN = 0, CONFIRM_TOKEN, SILENT_CONFIRM_TOKEN, - BACK_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; @@ -91,9 +92,12 @@ static void transaction_confirm_callback(int token, uint8_t index) { case SILENT_CONFIRM_TOKEN: ux_flow_response(true); break; - case BACK_TOKEN: + case BACK_TOKEN_TRANSACTION: ui_accept_transaction_flow(); break; + case BACK_TOKEN_SELFTRANSFER: + ui_accept_selftransfer_flow(); + break; default: PRINTF("Unhandled token : %d", token); } @@ -149,13 +153,17 @@ static void continue_callback(void) { 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 = BACK_TOKEN, + .navWithTap.backToken = backToken, .navWithTap.nextPageText = NULL, .navWithTap.quitText = "Reject transaction", .quitToken = CANCEL_TOKEN, @@ -204,6 +212,37 @@ void ui_accept_transaction_flow(void) { nbgl_refresh(); } +void ui_accept_selftransfer_flow(void) { + 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; diff --git a/tests/test_sign_psbt.py b/tests/test_sign_psbt.py index 8f9ab440f..4eb8aef61 100644 --- a/tests/test_sign_psbt.py +++ b/tests/test_sign_psbt.py @@ -335,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 From ea2e273154028f349a2613425ea142c72af0d223 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 21 Jun 2023 10:03:13 +0200 Subject: [PATCH 41/53] Avoid code duplication for self-transfers UI --- src/handler/sign_psbt.c | 18 +++++---------- src/ui/display.c | 18 +++++---------- src/ui/display.h | 11 +++++---- src/ui/display_bagl.c | 10 ++++----- src/ui/display_nbgl.c | 50 ++++++++++++----------------------------- 5 files changed, 33 insertions(+), 74 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 154b0f82a..90d42f2b1 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1378,19 +1378,11 @@ confirm_transaction(dispatcher_context_t *dc, sign_psbt_state_t *st) { } } else { // Show final user validation UI - if (st->outputs.n_external == 0) { - // All outputs are change; show the user it's a self transfer - if (!ui_validate_selftransfer(dc, COIN_COINID_SHORT, fee)) { - SEND_SW(dc, SW_DENY); - ui_post_processing_confirm_transaction(dc, false); - return false; - } - } else { - if (!ui_validate_transaction(dc, COIN_COINID_SHORT, fee)) { - SEND_SW(dc, SW_DENY); - ui_post_processing_confirm_transaction(dc, false); - return false; - } + 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; } } diff --git a/src/ui/display.c b/src/ui/display.c index 63742c5d1..57db7a691 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -199,23 +199,15 @@ bool ui_validate_output(dispatcher_context_t *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); - ui_accept_transaction_flow(); - - return io_ui_process(context, true); -} - -// Special case when all the outputs are change: show a "Self-transfer" screen in the flow -bool ui_validate_selftransfer(dispatcher_context_t *context, const char *coin_name, uint64_t fee) { - ui_validate_transaction_state_t *state = (ui_validate_transaction_state_t *) &g_ui_state; - - format_sats_amount(coin_name, fee, state->fee); - - ui_accept_selftransfer_flow(); + ui_accept_transaction_flow(is_self_transfer); return io_ui_process(context, true); } diff --git a/src/ui/display.h b/src/ui/display.h index 9026b4cfd..21eb450cc 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -132,9 +132,10 @@ bool ui_validate_output(dispatcher_context_t *context, 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_selftransfer(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); @@ -164,9 +165,7 @@ 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(void); - -void ui_accept_selftransfer_flow(void); +void ui_accept_transaction_flow(bool is_self_transfer); void ui_display_transaction_prompt(const int external_outputs_total_count); diff --git a/src/ui/display_bagl.c b/src/ui/display_bagl.c index 38486e2c4..5bbfd709c 100644 --- a/src/ui/display_bagl.c +++ b/src/ui/display_bagl.c @@ -466,12 +466,10 @@ void ui_display_output_address_amount_no_index_flow(int index) { ui_display_output_address_amount_flow(index); } -void ui_accept_transaction_flow(void) { - ux_flow_init(0, ux_accept_transaction_flow, NULL); -} - -void ui_accept_selftransfer_flow(void) { - ux_flow_init(0, ux_accept_selftransfer_flow, NULL); +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 index 1a423c0f1..591dc4dd5 100644 --- a/src/ui/display_nbgl.c +++ b/src/ui/display_nbgl.c @@ -93,10 +93,10 @@ static void transaction_confirm_callback(int token, uint8_t index) { ux_flow_response(true); break; case BACK_TOKEN_TRANSACTION: - ui_accept_transaction_flow(); + ui_accept_transaction_flow(false); break; case BACK_TOKEN_SELFTRANSFER: - ui_accept_selftransfer_flow(); + ui_accept_transaction_flow(true); break; default: PRINTF("Unhandled token : %d", token); @@ -183,42 +183,20 @@ static void transaction_confirm(int token, uint8_t index) { } } -void ui_accept_transaction_flow(void) { - transactionContext.tagValuePair[0].item = "Fees"; - transactionContext.tagValuePair[0].value = g_ui_state.validate_transaction.fee; +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; - - 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_accept_selftransfer_flow(void) { - 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 = 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.tagValueList.nbPairs = 2; + } transactionContext.confirm = "Sign transaction\nto send Bitcoin?"; transactionContext.confirmed_status = "TRANSACTION\nSIGNED"; From c585930450853d792c2d3e529925418a4fa50f73 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:55:18 +0200 Subject: [PATCH 42/53] Bump version 2.1.3; update CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ Makefile | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b1215dee..741b30a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ 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 diff --git a/Makefile b/Makefile index 9c423ed00..52f0235fe 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ APP_LOAD_PARAMS += --path_slip21 "LEDGER-Wallet policy" # Application version APPVERSION_M = 2 APPVERSION_N = 1 -APPVERSION_P = 2 +APPVERSION_P = 3 APPVERSION = "$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P)" APP_STACK_SIZE = 3072 From 695b0194bb823168804d074441d4e253514ef961 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Wed, 21 Jun 2023 13:54:00 +0200 Subject: [PATCH 43/53] Revert "Return in Exchange after a valid swap" --- src/main.c | 21 ++++++-- src/swap/handle_swap_sign_transaction.c | 10 ---- src/swap/handle_swap_sign_transaction.h | 2 - src/swap/swap_lib_calls.h | 72 ++++++++----------------- 4 files changed, 37 insertions(+), 68 deletions(-) diff --git a/src/main.c b/src/main.c index abf912a3a..7cf5c1c2a 100644 --- a/src/main.c +++ b/src/main.c @@ -45,6 +45,18 @@ #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_BOLOS_APP_STACK_CANARY extern unsigned int app_stack_canary; #endif @@ -151,8 +163,7 @@ void app_main() { &cmd); if (G_swap_state.called_from_swap && G_swap_state.should_exit) { - // Bitcoin app will keep listening as long as it does not receive a valid TX - finalize_exchange_sign_transaction(true); + os_sched_exit(0); } } } @@ -248,7 +259,7 @@ void coin_main() { app_exit(); } -static void swap_library_main_helper(libargs_t *args) { +static void swap_library_main_helper(struct libargs_s *args) { check_api_level(CX_COMPAT_APILEVEL); PRINTF("Inside a library \n"); switch (args->command) { @@ -299,7 +310,7 @@ static void swap_library_main_helper(libargs_t *args) { } } -void swap_library_main(libargs_t *args) { +void swap_library_main(struct libargs_s *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 */ @@ -333,7 +344,7 @@ __attribute__((section(".boot"))) int main(int arg0) { } // Application launched as library (for swap support) - libargs_t *args = (libargs_t *) arg0; + struct libargs_s *args = (struct libargs_s *) arg0; if (args->id != 0x100) { app_exit(); return 0; diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index 61e1d2203..0fc7f1aee 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -11,9 +11,6 @@ #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]; @@ -46,8 +43,6 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti 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, @@ -55,8 +50,3 @@ 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 bbd82b24b..d961b94cf 100644 --- a/src/swap/handle_swap_sign_transaction.h +++ b/src/swap/handle_swap_sign_transaction.h @@ -3,5 +3,3 @@ #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 dc88417ae..d41d37ab9 100644 --- a/src/swap/swap_lib_calls.h +++ b/src/swap/swap_lib_calls.h @@ -1,13 +1,6 @@ #pragma once -/* 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" +#include #define RUN_APPLICATION 1 @@ -17,27 +10,17 @@ #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 - uint8_t *coin_configuration; - uint8_t coin_configuration_length; + unsigned char* coin_configuration; + unsigned char coin_configuration_length; // serialized path, segwit, version prefix, hash used, dictionary etc. // fields and serialization format depends on spesific coin app - uint8_t *address_parameters; - uint8_t address_parameters_length; - char *address_to_check; - char *extra_id_to_check; + unsigned char* address_parameters; + unsigned char address_parameters_length; + char* address_to_check; + char* extra_id_to_check; // OUT int result; } check_address_parameters_t; @@ -45,36 +28,23 @@ 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 - uint8_t *coin_configuration; - uint8_t coin_configuration_length; - uint8_t *amount; - uint8_t amount_length; + unsigned char* coin_configuration; + unsigned char coin_configuration_length; + unsigned char* amount; + unsigned char amount_length; bool is_fee; // OUT - char printable_amount[MAX_PRINTABLE_AMOUNT_SIZE]; + char printable_amount[30]; + // int result; } get_printable_amount_parameters_t; typedef struct create_transaction_parameters_s { - // 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; + 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; } 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; From b2055a521c696d64a9b8d55f5fa6a51ef88236e6 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 26 Jun 2023 10:26:30 +0200 Subject: [PATCH 44/53] Revert "Revert "Return in Exchange after a valid swap"" --- src/main.c | 21 ++------ src/swap/handle_swap_sign_transaction.c | 10 ++++ src/swap/handle_swap_sign_transaction.h | 2 + src/swap/swap_lib_calls.h | 72 +++++++++++++++++-------- 4 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/main.c b/src/main.c index 7cf5c1c2a..abf912a3a 100644 --- a/src/main.c +++ b/src/main.c @@ -45,18 +45,6 @@ #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_BOLOS_APP_STACK_CANARY extern unsigned int app_stack_canary; #endif @@ -163,7 +151,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); } } } @@ -259,7 +248,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) { @@ -310,7 +299,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 */ @@ -344,7 +333,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/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index 0fc7f1aee..61e1d2203 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -11,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]; @@ -43,6 +46,8 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti 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, @@ -50,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 d961b94cf..bbd82b24b 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 d41d37ab9..dc88417ae 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; From e70166b5493d5e907e2a8b12523c1b4022afdc0a Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:59:39 +0200 Subject: [PATCH 45/53] Bump version 0.2.2 for both the Python and JS client libraries --- bitcoin_client/ledger_bitcoin/__init__.py | 2 +- bitcoin_client_js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bitcoin_client/ledger_bitcoin/__init__.py b/bitcoin_client/ledger_bitcoin/__init__.py index 2c5bef411..4bbd22e14 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.1' +__version__ = '0.2.2' __all__ = [ "Client", diff --git a/bitcoin_client_js/package.json b/bitcoin_client_js/package.json index 2f1e5b070..a99ea6782 100644 --- a/bitcoin_client_js/package.json +++ b/bitcoin_client_js/package.json @@ -1,6 +1,6 @@ { "name": "ledger-bitcoin", - "version": "0.2.1", + "version": "0.2.2", "description": "Ledger Hardware Wallet Bitcoin Application Client", "main": "build/main/index.js", "typings": "build/main/index.d.ts", From c30d0d7f0ee87511d657339ff524c785f5e64130 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:07:20 +0200 Subject: [PATCH 46/53] Always check return value of state_stack_push --- src/handler/lib/policy.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index 053ada479..d1b3734df 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -598,21 +598,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: From 07504f4eb3df3e8f309cb9136576326c31fafd2c Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:38:14 +0200 Subject: [PATCH 47/53] Add __attribute__((warn_unused_result)) to most functions in policy.c/policy.h; added an additional missing return value check --- src/handler/lib/policy.c | 84 +++++++++++++++++++++++----------------- src/handler/lib/policy.h | 49 ++++++++++++----------- 2 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index d1b3734df..da30c3cf7 100644 --- a/src/handler/lib/policy.c +++ b/src/handler/lib/policy.c @@ -326,9 +326,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) { @@ -348,7 +348,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) { @@ -363,9 +363,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 @@ -382,10 +381,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; @@ -428,10 +428,11 @@ static int __attribute__((noinline)) get_extended_pubkey(dispatcher_context_t *d 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; @@ -513,7 +514,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; @@ -632,7 +634,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(); @@ -675,7 +678,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(); @@ -737,7 +741,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(); @@ -813,7 +819,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(); @@ -884,10 +892,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); @@ -919,11 +928,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; @@ -932,7 +941,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]) { @@ -1072,7 +1081,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; } @@ -1086,11 +1097,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) { diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 9ffe880db..5bbcf3306 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. @@ -163,10 +165,11 @@ 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); /** * Checks if a wallet policy is sane, verifying that pubkeys are never repeated and (if miniscript) @@ -183,8 +186,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 From 58016296852af3512090ed6aecca219bfeeb4f48 Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Mon, 3 Jul 2023 18:57:28 +0200 Subject: [PATCH 48/53] Add a spinner when starting in Exchange mode on Stax --- src/main.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.c b/src/main.c index abf912a3a..fd772f218 100644 --- a/src/main.c +++ b/src/main.c @@ -45,6 +45,10 @@ #include "swap/handle_get_printable_amount.h" #include "swap/handle_check_address.h" +#ifdef HAVE_NBGL +#include "nbgl_use_case.h" +#endif + #ifdef HAVE_BOLOS_APP_STACK_CANARY extern unsigned int app_stack_canary; #endif @@ -207,9 +211,7 @@ void coin_main() { // Process the incoming APDUs for (;;) { -#ifdef HAVE_BAGL UX_INIT(); -#endif // HAVE_BAGL BEGIN_TRY { TRY { io_seproxyhal_init(); @@ -268,9 +270,11 @@ static void swap_library_main_helper(libargs_t *args) { G_swap_state.called_from_swap = 1; io_seproxyhal_init(); -#ifdef HAVE_BAGL UX_INIT(); +#ifdef HAVE_BAGL ux_stack_push(); +#elif defined(HAVE_NBGL) + nbgl_useCaseSpinner("Signing"); #endif // HAVE_BAGL USB_power(0); From 203b5d67cc67c06dd96c5213694cadd0fd39fc96 Mon Sep 17 00:00:00 2001 From: Kewde Date: Fri, 7 Jul 2023 17:38:25 +0200 Subject: [PATCH 49/53] fix: repo in package.json --- bitcoin_client_js/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitcoin_client_js/package.json b/bitcoin_client_js/package.json index a99ea6782..2099ada4c 100644 --- a/bitcoin_client_js/package.json +++ b/bitcoin_client_js/package.json @@ -4,7 +4,7 @@ "description": "Ledger Hardware Wallet Bitcoin Application Client", "main": "build/main/index.js", "typings": "build/main/index.d.ts", - "repository": "https://github.com/LedgerHW/app-bitcoin-new", + "repository": "https://github.com/LedgerHQ/app-bitcoin-new", "license": "Apache-2.0", "keywords": [ "Ledger", From 2eb138c2fafa2a9e84049f9cb4bb269dcee9c856 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:58:57 +0200 Subject: [PATCH 50/53] Add additional tests for non-standard policies in get_wallet_address --- tests/test_get_wallet_address.py | 42 +++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/test_get_wallet_address.py b/tests/test_get_wallet_address.py index 526637cb8..9206db585 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 From b9595360bd0b1aecd1a1067149fe6be8b0f97cc4 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:38:33 +0200 Subject: [PATCH 51/53] Rename 'canonical wallet' to 'default wallet' for consistency with the docs --- src/handler/get_wallet_address.c | 18 +++++++++--------- src/handler/sign_psbt.c | 31 ++++++++++++++++--------------- src/ui/display.c | 2 +- src/ui/display.h | 2 +- src/ui/display_bagl.c | 8 ++++---- src/ui/display_nbgl.c | 2 +- 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/handler/get_wallet_address.c b/src/handler/get_wallet_address.c index 752ef471d..62b64ae6f 100644 --- a/src/handler/get_wallet_address.c +++ b/src/handler/get_wallet_address.c @@ -61,7 +61,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi uint8_t wallet_id[32]; uint8_t wallet_hmac[32]; - bool is_wallet_canonical; + bool is_wallet_default; // whether the wallet policy can be used without being registered int address_type; policy_map_wallet_header_t wallet_header; @@ -133,7 +133,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi } if (hmac_or == 0) { - // No hmac, verify that the policy is a canonical one that is allowed by default + // No hmac, verify that the policy is a default one address_type = get_policy_address_type(&wallet_policy_map.parsed); if (address_type == -1) { PRINTF("Non-standard policy, and no hmac provided\n"); @@ -197,7 +197,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi // check if derivation path is indeed standard - // Based on the address type, we set the expected bip44 purpose for this canonical wallet + // Based on the address type, we set the expected bip44 purpose int bip44_purpose = get_bip44_purpose(address_type); if (key_info.master_key_derivation_len != 3) { @@ -219,7 +219,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi return; } - is_wallet_canonical = true; + is_wallet_default = true; } else { // Verify hmac @@ -229,12 +229,12 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi 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; } @@ -282,7 +282,7 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi 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/sign_psbt.c b/src/handler/sign_psbt.c index 90d42f2b1..219fbcce6 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -142,7 +142,7 @@ typedef struct { int n_external; // count of external outputs } outputs; - bool is_wallet_canonical; + bool is_wallet_default; uint8_t protocol_version; @@ -590,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; } { @@ -627,8 +627,8 @@ 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->is_wallet_default) { + // verify that the policy is indeed a default one that is allowed by default if (st->wallet_header_n_keys != 1) { PRINTF("Non-standard policy, it should only have 1 key\n"); @@ -643,8 +643,8 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { return false; } - // Based on the address type, we set the expected bip44 purpose for this canonical - // wallet + // Based on the address type, we set the expected bip44 purpose for this default + // wallet policy int bip44_purpose = get_bip44_purpose(address_type); if (bip44_purpose < 0) { SEND_SW(dc, SW_BAD_STATE); @@ -652,8 +652,9 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { } // 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). + // standards. + // We skip checking that we can indeed derive the same pubkey (no security + // risk here, as the xpub itself isn't really used for the default wallet policies). policy_map_key_info_t key_info; { char key_info_str[MAX_POLICY_KEY_INFO_LEN]; @@ -693,15 +694,15 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { } } - // 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; @@ -709,7 +710,7 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { st->master_key_fingerprint = crypto_get_master_key_fingerprint(); - if (!st->is_wallet_canonical) { + if (!st->is_wallet_default) { ui_post_processing_confirm_wallet_spend(dc, true); } return true; diff --git a/src/ui/display.c b/src/ui/display.c index 57db7a691..a4b00178c 100644 --- a/src/ui/display.c +++ b/src/ui/display.c @@ -139,7 +139,7 @@ bool ui_display_wallet_address(dispatcher_context_t *context, strncpy(state->address, address, sizeof(state->address)); if (wallet_name == NULL) { - ui_display_canonical_wallet_address_flow(); + ui_display_default_wallet_address_flow(); } else { strncpy(state->wallet_name, wallet_name, sizeof(state->wallet_name)); ui_display_receive_in_wallet_flow(); diff --git a/src/ui/display.h b/src/ui/display.h index 21eb450cc..d35321396 100644 --- a/src/ui/display.h +++ b/src/ui/display.h @@ -151,7 +151,7 @@ void ui_display_policy_map_cosigner_pubkey_flow(void); void ui_display_receive_in_wallet_flow(void); -void ui_display_canonical_wallet_address_flow(void); +void ui_display_default_wallet_address_flow(void); void ui_display_spend_from_wallet_flow(void); diff --git a/src/ui/display_bagl.c b/src/ui/display_bagl.c index 5bbfd709c..17edf831c 100644 --- a/src/ui/display_bagl.c +++ b/src/ui/display_bagl.c @@ -320,11 +320,11 @@ UX_FLOW(ux_display_receive_in_wallet_flow, &ux_display_approve_step, &ux_display_reject_step); -// FLOW to display an address of a canonical wallet: +// 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_canonical_wallet_address_flow, +UX_FLOW(ux_display_default_wallet_address_flow, &ux_display_wallet_address_step, &ux_display_approve_step, &ux_display_reject_step); @@ -432,8 +432,8 @@ void ui_display_receive_in_wallet_flow(void) { ux_flow_init(0, ux_display_receive_in_wallet_flow, NULL); } -void ui_display_canonical_wallet_address_flow(void) { - ux_flow_init(0, ux_display_canonical_wallet_address_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) { diff --git a/src/ui/display_nbgl.c b/src/ui/display_nbgl.c index 591dc4dd5..e77b30f27 100644 --- a/src/ui/display_nbgl.c +++ b/src/ui/display_nbgl.c @@ -454,7 +454,7 @@ static void address_display(void) { nbgl_useCaseAddressConfirmation(g_ui_state.wallet.address, status_confirmation_callback); } -void ui_display_canonical_wallet_address_flow(void) { +void ui_display_default_wallet_address_flow(void) { transactionContext.confirm = "Confirm address"; transactionContext.confirmed_status = "ADDRESS\nVERIFIED"; transactionContext.rejected_status = "Address verification\ncancelled"; From 8318c44ea9176179957da2d73337d4e6fe81d223 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 7 Jul 2023 17:39:30 +0200 Subject: [PATCH 52/53] Improve and refactor standardness check for wallet policies --- Makefile | 2 - src/common/bip32.c | 55 ------------- src/common/bip32.h | 49 ------------ src/handler/get_extended_pubkey.c | 17 +--- src/handler/get_wallet_address.c | 80 ++----------------- src/handler/lib/policy.c | 127 +++++++++++++++++++++++++++--- src/handler/lib/policy.h | 24 ++++++ src/handler/sign_psbt.c | 62 ++------------- unit-tests/test_bip32.c | 80 +------------------ 9 files changed, 156 insertions(+), 340 deletions(-) diff --git a/Makefile b/Makefile index 52f0235fe..3553ac717 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,6 @@ ifeq ($(COIN),bitcoin_testnet) # Bitcoin 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\" @@ -73,7 +72,6 @@ else ifeq ($(COIN),bitcoin) # Bitcoin mainnet, no legacy support DEFINES += BIP32_PUBKEY_VERSION=0x0488B21E DEFINES += BIP44_COIN_TYPE=0 -DEFINES += BIP44_COIN_TYPE_2=0 DEFINES += COIN_P2PKH_VERSION=0 DEFINES += COIN_P2SH_VERSION=5 DEFINES += COIN_NATIVE_SEGWIT_PREFIX=\"bc\" diff --git a/src/common/bip32.c b/src/common/bip32.c index ecae86f98..e86580891 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 3edc5e551..cd6707e14 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/handler/get_extended_pubkey.c b/src/handler/get_extended_pubkey.c index 4ac765416..5ab2475a2 100644 --- a/src/handler/get_extended_pubkey.c +++ b/src/handler/get_extended_pubkey.c @@ -29,10 +29,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 +82,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; } @@ -144,8 +134,7 @@ void handler_get_extended_pubkey(dispatcher_context_t *dc, uint8_t protocol_vers 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); diff --git a/src/handler/get_wallet_address.c b/src/handler/get_wallet_address.c index 62b64ae6f..d1f1744ad 100644 --- a/src/handler/get_wallet_address.c +++ b/src/handler/get_wallet_address.c @@ -62,7 +62,6 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi uint8_t wallet_hmac[32]; bool is_wallet_default; // whether the wallet policy can be used without being registered - int address_type; policy_map_wallet_header_t wallet_header; @@ -133,88 +132,21 @@ void handler_get_wallet_address(dispatcher_context_t *dc, uint8_t protocol_versi } if (hmac_or == 0) { - // No hmac, verify that the policy is a default one - 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; - } - - if (read_u32_be(key_info.master_key_fingerprint, 0) != master_key_fingerprint) { - 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; - } + // No hmac, verify that the policy is indeed a default one - if (strncmp(key_info.ext_pubkey, pubkey_derived, MAX_SERIALIZED_PUBKEY_LENGTH) != 0) { + if (!is_wallet_policy_standard(dc, &wallet_header, &wallet_policy_map.parsed)) { 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 - int bip44_purpose = get_bip44_purpose(address_type); - - if (key_info.master_key_derivation_len != 3) { + 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; } - 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; } diff --git a/src/handler/lib/policy.c b/src/handler/lib/policy.c index da30c3cf7..5a77e9199 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" @@ -1285,25 +1286,127 @@ __attribute__((noinline)) int get_wallet_internal_script_hash( #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 + 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"); + return false; + } + + if (strncmp(key_info.ext_pubkey, pubkey_derived, MAX_SERIALIZED_PUBKEY_LENGTH) != 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]) { diff --git a/src/handler/lib/policy.h b/src/handler/lib/policy.h index 5bbcf3306..4db34ae0e 100644 --- a/src/handler/lib/policy.h +++ b/src/handler/lib/policy.h @@ -126,6 +126,30 @@ __attribute__((warn_unused_result)) int get_wallet_internal_script_hash( */ 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. diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 219fbcce6..a89fccf64 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -628,69 +628,21 @@ init_global_state(dispatcher_context_t *dc, sign_psbt_state_t *st) { st->wallet_header_n_keys = wallet_header.n_keys; if (st->is_wallet_default) { - // verify that the policy is indeed a default 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) { + // 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 default - // wallet policy - 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 derive the same pubkey (no security - // risk here, as the xpub itself isn't really used for the default 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. } } diff --git a/unit-tests/test_bip32.c b/unit-tests/test_bip32.c index 65cf52f10..1e406a4fc 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); From 17b84ab78d07922281f06818bb7dc0c2b65833ca Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 10 Jul 2023 10:42:49 +0200 Subject: [PATCH 53/53] Update docs for wallet policies, particularly for taproot scripts --- doc/wallet.md | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/doc/wallet.md b/doc/wallet.md index 85b0c2920..044266cd0 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.