From 19d6c5ec5893c20a881cd778dfba6cecd5888464 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:52:33 +0200 Subject: [PATCH 1/7] Add payin_extra_id to G_swap_state; process destination_address_extra_id during copy_transaction_parameters --- src/swap/handle_swap_sign_transaction.c | 24 ++++++++++++++++++++++++ src/swap/swap_globals.h | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index 61e1d220..8e33ec99 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -53,6 +53,30 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti memcpy(G_swap_state.destination_address, destination_address, sizeof(G_swap_state.destination_address)); + + // if destination_address_extra_id is given, we use the first byte to determine if we use the + // normal swap protocol, or the one for cross-chain swaps + if (sign_transaction_params->destination_address_extra_id == NULL || + sign_transaction_params->destination_address_extra_id[0] == 0) { + G_swap_state.mode = SWAP_MODE_STANDARD; + + // we don't use the payin_extra_id field in this mode + explicit_bzero(G_swap_state.payin_extra_id, sizeof(G_swap_state.payin_extra_id)); + } else if (sign_transaction_params->destination_address_extra_id[0] == 2) { + G_swap_state.mode = SWAP_MODE_CROSSCHAIN; + + // we expect exactly 33 bytes. Guard against future protocol changes, as the following + // code might need to be revised in that case + LEDGER_ASSERT(sizeof(G_swap_state.payin_extra_id) == 33, "Unexpected payin_extra_id size"); + + memcpy(G_swap_state.payin_extra_id, + sign_transaction_params->destination_address_extra_id, + sizeof(G_swap_state.payin_extra_id)); + } else { + PRINTF("Invalid or unknown swap protocol\n"); + return false; + } + return true; } diff --git a/src/swap/swap_globals.h b/src/swap/swap_globals.h index 8358b88a..715e65d5 100644 --- a/src/swap/swap_globals.h +++ b/src/swap/swap_globals.h @@ -2,6 +2,11 @@ #include +enum { + SWAP_MODE_STANDARD = 0, + SWAP_MODE_CROSSCHAIN = 1, +}; + typedef struct swap_globals_s { uint64_t amount; uint64_t fees; @@ -9,6 +14,8 @@ typedef struct swap_globals_s { /*Is swap mode*/ unsigned char called_from_swap; unsigned char should_exit; + unsigned char mode; + uint8_t payin_extra_id[1 + 32]; } swap_globals_t; extern swap_globals_t G_swap_state; From af3c336f1c7a4b9d9d80631d350346980bba2338 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:47:32 +0200 Subject: [PATCH 2/7] Extend Swap protocol for cross-chain swaps --- src/handler/sign_psbt.c | 88 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 4193ff4f..f63cd8e9 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1202,13 +1202,91 @@ execute_swap_checks(dispatcher_context_t *dc, sign_psbt_state_t *st) { uint64_t fee = st->inputs_total_amount - st->outputs.total_amount; - // There must be only one external output - if (st->n_external_outputs != 1) { - PRINTF("Swap transaction must have exactly 1 external output\n"); + // The index of the swap destination address in the cache of external outputs. + // NB: this is _not_ the output index in the transaction, as change outputs are skipped. + int swap_dest_idx = -1; + + if (G_swap_state.mode == SWAP_MODE_STANDARD) { + swap_dest_idx = 0; + + // There must be only one external output + if (st->n_external_outputs != 1) { + PRINTF("Standard swap transaction must have exactly 1 external output\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + } else if (G_swap_state.mode == SWAP_MODE_CROSSCHAIN) { + // There must be exactly 2 external outputs; the first is the OP_RETURN + + swap_dest_idx = 1; + + if (st->n_external_outputs != 2) { + PRINTF("Cross-chain swap transaction must have exactly 2 external outputs\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + + uint8_t *opreturn_script = st->outputs.output_scripts[0]; + size_t opreturn_script_len = st->outputs.output_script_lengths[0]; + size_t opreturn_amount = st->outputs.output_amounts[0]; + if (opreturn_script_len < 4 || opreturn_script[0] != OP_RETURN) { + PRINTF("The first output must be OP_RETURN for a cross-chain swap\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + + uint8_t second_byte = opreturn_script[1]; + size_t push_opcode_size; // the length of the push opcode (1 or 2 bytes) + size_t data_size; // the length of the actual data embedded in the OP_RETURN output + if (2 <= second_byte && second_byte <= 75) { + push_opcode_size = 1; + data_size = second_byte; + } else if (second_byte == OP_PUSHDATA1) { + // pushing more than 75 bytes requires using OP_PUSHDATA1 + // insted of a single-byte opcode + push_opcode_size = 2; + data_size = opreturn_script[2]; + } else { + // there are other valid OP_RETURN Scripts that we never expect here, + // so we don't bother parsing. + PRINTF("Unsupported or invalid OP_RETURN Script in cross-chain swap\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + + // Make sure there is a singla data push + if (opreturn_script_len != 1 + push_opcode_size + data_size) { + PRINTF("Invalid OP_RETURN Script length in cross-chain swap\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + + // Make sure the output's value is 0 + if (opreturn_amount != 0) { + PRINTF("OP_RETURN with non-zero value during cross-chain swap\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + + // verify the hash in the data payload is the expected one + uint8_t expected_payin_hash[32]; + cx_hash_sha256(&opreturn_script[1 + push_opcode_size], data_size, expected_payin_hash, 32); + if (memcmp(G_swap_state.payin_extra_id + 1, + expected_payin_hash, + sizeof(expected_payin_hash)) != 0) { + PRINTF("Mismatching payin hash in cross-chain swap\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); + } + } else { + PRINTF("Unknown swap mode: %d\n", G_swap_state.mode); SEND_SW(dc, SW_FAIL_SWAP); finalize_exchange_sign_transaction(false); } + LEDGER_ASSERT(0 <= swap_dest_idx && swap_dest_idx < N_CACHED_EXTERNAL_OUTPUTS, + "External output index out of range for swap\n"); + // Check that total amount and fees are as expected if (fee != G_swap_state.fees) { PRINTF("Mismatching fee for swap\n"); @@ -1226,8 +1304,8 @@ execute_swap_checks(dispatcher_context_t *dc, sign_psbt_state_t *st) { // Compute this output's address char output_description[MAX_OUTPUT_SCRIPT_DESC_SIZE]; - if (!format_script(st->outputs.output_scripts[0], - st->outputs.output_script_lengths[0], + if (!format_script(st->outputs.output_scripts[swap_dest_idx], + st->outputs.output_script_lengths[swap_dest_idx], output_description)) { PRINTF("Invalid or unsupported script for external output\n"); SEND_SW(dc, SW_FAIL_SWAP); From 629d42322f7b1fb190b199d9e4d99ec5319b0951 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:54:21 +0200 Subject: [PATCH 3/7] Add more debug code in case of address mismatch during swap --- src/handler/sign_psbt.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index f63cd8e9..8ff05505 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1321,6 +1321,16 @@ execute_swap_checks(dispatcher_context_t *dc, sign_psbt_state_t *st) { strncmp(G_swap_state.destination_address, output_description, output_description_len)) { // address did not match PRINTF("Mismatching address for swap\n"); + PRINTF("Expected: "); + for (int i = 0; i < swap_addr_len; i++) { + PRINTF("%c", G_swap_state.destination_address[i]); + } + PRINTF("\n"); + PRINTF("Found: "); + for (int i = 0; i < output_description_len; i++) { + PRINTF("%c", output_description[i]); + } + PRINTF("\n"); SEND_SW(dc, SW_FAIL_SWAP); finalize_exchange_sign_transaction(false); } From e89e67c5ba660f1c586810484f73e83a8f98ef30 Mon Sep 17 00:00:00 2001 From: Francois Beutin Date: Mon, 24 Jun 2024 17:58:50 +0200 Subject: [PATCH 4/7] Read sign_transaction_params->destination_address_extra_id before wiping BSS --- src/swap/handle_swap_sign_transaction.c | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index 8e33ec99..fd501c11 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -16,18 +16,26 @@ 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 destination_address_extra_data[33]; uint8_t amount[8]; uint8_t fees[8]; // first copy parameters to stack, and then to global data. // We need this "trick" as the input data position can overlap with btc-app globals memset(destination_address, 0, sizeof(destination_address)); + memset(destination_address_extra_data, 0, sizeof(destination_address_extra_data)); memset(amount, 0, sizeof(amount)); memset(fees, 0, sizeof(fees)); strncpy(destination_address, sign_transaction_params->destination_address, sizeof(destination_address) - 1); + if (sign_transaction_params->destination_address_extra_id != NULL) { + memcpy(destination_address_extra_data, + sign_transaction_params->destination_address_extra_id, + sizeof(destination_address_extra_data)); + } + // sanity checks if ((destination_address[sizeof(destination_address) - 1] != '\0') || (sign_transaction_params->amount_length > 8) || @@ -56,13 +64,12 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti // if destination_address_extra_id is given, we use the first byte to determine if we use the // normal swap protocol, or the one for cross-chain swaps - if (sign_transaction_params->destination_address_extra_id == NULL || - sign_transaction_params->destination_address_extra_id[0] == 0) { + if (destination_address_extra_data[0] == 0) { G_swap_state.mode = SWAP_MODE_STANDARD; // we don't use the payin_extra_id field in this mode explicit_bzero(G_swap_state.payin_extra_id, sizeof(G_swap_state.payin_extra_id)); - } else if (sign_transaction_params->destination_address_extra_id[0] == 2) { + } else if (destination_address_extra_data[0] == 2) { G_swap_state.mode = SWAP_MODE_CROSSCHAIN; // we expect exactly 33 bytes. Guard against future protocol changes, as the following @@ -70,7 +77,7 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti LEDGER_ASSERT(sizeof(G_swap_state.payin_extra_id) == 33, "Unexpected payin_extra_id size"); memcpy(G_swap_state.payin_extra_id, - sign_transaction_params->destination_address_extra_id, + destination_address_extra_data, sizeof(G_swap_state.payin_extra_id)); } else { PRINTF("Invalid or unknown swap protocol\n"); From f18fb4ad6d5e4bc42a9ed331dc80ea8318d57dfb Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 15 Jul 2024 11:59:56 +0200 Subject: [PATCH 5/7] Improve swap error handling --- src/handler/sign_psbt.c | 6 ++++++ src/swap/handle_swap_sign_transaction.c | 5 +++-- src/swap/swap_globals.h | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/handler/sign_psbt.c b/src/handler/sign_psbt.c index 8ff05505..6cadf10c 100644 --- a/src/handler/sign_psbt.c +++ b/src/handler/sign_psbt.c @@ -1278,6 +1278,12 @@ execute_swap_checks(dispatcher_context_t *dc, sign_psbt_state_t *st) { SEND_SW(dc, SW_FAIL_SWAP); finalize_exchange_sign_transaction(false); } + } else if (G_swap_state.mode == SWAP_MODE_ERROR) { + // an error was detected in handle_swap_sign_transaction.c::copy_transaction_parameters + // special case only to improve error reporting in debug mode + PRINTF("Invalid parameters for swap feature\n"); + SEND_SW(dc, SW_FAIL_SWAP); + finalize_exchange_sign_transaction(false); } else { PRINTF("Unknown swap mode: %d\n", G_swap_state.mode); SEND_SW(dc, SW_FAIL_SWAP); diff --git a/src/swap/handle_swap_sign_transaction.c b/src/swap/handle_swap_sign_transaction.c index fd501c11..f439ed55 100644 --- a/src/swap/handle_swap_sign_transaction.c +++ b/src/swap/handle_swap_sign_transaction.c @@ -80,8 +80,9 @@ bool copy_transaction_parameters(create_transaction_parameters_t* sign_transacti destination_address_extra_data, sizeof(G_swap_state.payin_extra_id)); } else { - PRINTF("Invalid or unknown swap protocol\n"); - return false; + // Since we cannot return an error status word here, we mark the swap state as invalid + // and will return an error later, once an attempt is made to sign. + G_swap_state.mode = SWAP_MODE_ERROR; } return true; diff --git a/src/swap/swap_globals.h b/src/swap/swap_globals.h index 715e65d5..7d8984f1 100644 --- a/src/swap/swap_globals.h +++ b/src/swap/swap_globals.h @@ -5,6 +5,7 @@ enum { SWAP_MODE_STANDARD = 0, SWAP_MODE_CROSSCHAIN = 1, + SWAP_MODE_ERROR = 0xFF, }; typedef struct swap_globals_s { From 50cc569a4596f27b5d10242355965b2f8e2291b8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:06:36 +0200 Subject: [PATCH 6/7] Bump version to 2.2.5; update CHANGELOG --- CHANGELOG.md | 4 ++++ Makefile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6d80a6..7c617fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Dates are in `dd-mm-yyyy` format. ## [2.2.5] - TBD +### Added + +- Support for crosschain swap protocol. + ### Fixed - `signMessage` would fail since version 2.2.2 for certain message lengths. diff --git a/Makefile b/Makefile index 0720350a..e9e2f2a4 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ PATH_SLIP21_APP_LOAD_PARAMS = "LEDGER-Wallet policy" # Application version APPVERSION_M = 2 APPVERSION_N = 2 -APPVERSION_P = 4 +APPVERSION_P = 5 APPVERSION_SUFFIX = # if not empty, appended at the end. Do not add a dash. ifeq ($(APPVERSION_SUFFIX),) From 6fde67861a9668979e2a03df68a4567adc2c12b8 Mon Sep 17 00:00:00 2001 From: Salvatore Ingala <6681844+bigspider@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:25:17 +0200 Subject: [PATCH 7/7] Update dashboard snapshots --- tests/snapshots/flex/test_dashboard/00001.png | Bin 8455 -> 8544 bytes .../snapshots/nanos/test_dashboard/00000.png | Bin 0 -> 466 bytes .../snapshots/nanos/test_dashboard/00001.png | Bin 0 -> 326 bytes .../snapshots/nanos/test_dashboard/00002.png | Bin 0 -> 327 bytes .../snapshots/nanos/test_dashboard/00003.png | Bin 0 -> 274 bytes .../snapshots/nanosp/test_dashboard/00001.png | Bin 360 -> 350 bytes .../snapshots/nanox/test_dashboard/00001.png | Bin 360 -> 350 bytes tests/snapshots/stax/test_dashboard/00001.png | Bin 10325 -> 10378 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/snapshots/nanos/test_dashboard/00000.png create mode 100644 tests/snapshots/nanos/test_dashboard/00001.png create mode 100644 tests/snapshots/nanos/test_dashboard/00002.png create mode 100644 tests/snapshots/nanos/test_dashboard/00003.png diff --git a/tests/snapshots/flex/test_dashboard/00001.png b/tests/snapshots/flex/test_dashboard/00001.png index 3cad5446006cee8fbe0b697367ffe50c6fbed143..e4ee31190f646631306027d962c67db09f409a65 100644 GIT binary patch literal 8544 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 literal 8455 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 diff --git a/tests/snapshots/nanos/test_dashboard/00000.png b/tests/snapshots/nanos/test_dashboard/00000.png new file mode 100644 index 0000000000000000000000000000000000000000..38d9414f769088f2c86ad9cdcbff2f27950cd8fc GIT binary patch literal 466 zcmV;@0WJQCP)&I)G}(b@ugt_KXU9S%3$?C)XReARkSpC12uzhHnLBm__Gh20 z9X_CN$;QEJfFl0ZxI6$^`m6&O`y8)6c`PWOUiL!;vM+gIICe_rsG!j{dTrA3#hhS6 z$jVIk_NuOwi<>%NAps32z`Y5n0U=Sc)WKH)JzGE+b5au;fDR^F;W{*l9gbi-vtS`^xhCE-8KL+C2u+!Wu1il)PWqH+fdN)J)mV@*5pa+)PqKm6QYHj zedbD}hBIiz0-Me>bw`C(1G%xB2eqUp2h*Jm-O!pG$G~_CY9-7E8D;8`-lehVRCu$Y zJF!#hv5E~D{lj>G<}59w8`(BVAfjp|SO`ikNPeG4k|b&H1}a7(*RzP(QUCw|07*qo IM6N<$f`qludH?_b literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_dashboard/00001.png b/tests/snapshots/nanos/test_dashboard/00001.png new file mode 100644 index 0000000000000000000000000000000000000000..38c0ebdd27545d496cb996ad6ae9918584f334fc GIT binary patch literal 326 zcmV-M0lEH(P)H`*5;wFuAX1^kq> zMXz8Q-~{NsoXsA)r0v~b8r1+%X($79kDdsSut)R)42nu^R-{p3${6bj)vTNY7y#aY zye0Ln?C6%$wyk!(H4)5v0U!Y#`pT+QLHZp6S%Um3t@?BBL`@Gy0*algkxP?xveZ$_ zxD~}^wR7}h4D{0e4VaQ?|27(`Y5!)JgGg9~BKee@8eynITj$=!6KX~SLDM@y=k07> zJS+mnhu9y3eHU~Eq&w&JnW{hjC~@(%ea`?g;EH|cm{3l~P&BMRCY>Sv01!e5A%sBk Y0#G20gK{$lJ^%m!07*qoM6N<$g4!*L9smFU literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_dashboard/00002.png b/tests/snapshots/nanos/test_dashboard/00002.png new file mode 100644 index 0000000000000000000000000000000000000000..3476b972aad8b16723e63d03d83db7de63b698cd GIT binary patch literal 327 zcmV-N0l5B&P)q%Lk}$JN=*+Ya3V52_$ri_Tw`0y zW~o~dSBJ_)jqx$FVQ-p{pr~clG9JlqkTUKDen}9lbG=+9r&_RRCT`@ta9}o@5 zoGR6f6v&VngBn2&rF;h11n5AoQ(n^?5wLi<0+<0A9oYrVM?&+_T6|lSEB|J;2qA=^ ZvjZ`uW$88COrih)002ovPDHLkV1i+HiM9X$ literal 0 HcmV?d00001 diff --git a/tests/snapshots/nanos/test_dashboard/00003.png b/tests/snapshots/nanos/test_dashboard/00003.png new file mode 100644 index 0000000000000000000000000000000000000000..e2279803e79ec6d6443deff45a1f9326b562e303 GIT binary patch literal 274 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