Add initial mock for the dispatcher, and tests for get_preimage
What changed, and why it matters
This commit only adds unit-test infrastructure: a mock implementation of the Ledger SDK dispatcher and SHA-256 hashing, plus tests for the get_preimage handler. It does not change any production code in the Bitcoin app, so it cannot introduce a runtime security vulnerability on its own.
No security action required. Treat as normal engineering/test code. Optionally review the mock for correctness so that future tests give meaningful results, but this commit itself is not a vulnerability.
Security signals we found
No production code modified
Test-only mock additions
No privilege boundary crossed
No input parsing changes in firmware
No cryptographic implementation deployed to device
Evidence from the diff
The diff is entirely confined to the unit-tests/ directory. It introduces mock_dispatcher.c/h (a C mock of the Python client command interpreter), cx_hash_mock.c/h (a software SHA-256 mock backed by sha-256.c), minor formatting/declaration updates to mock SDK headers lcx_hash.h and lcx_sha256.h, CMakeLists.txt wiring, and test_get_preimage.c (cmocka tests for call_get_preimage). No source files under src/ are modified, and no production firmware behavior changes.
Changed components
unit-tests/libs/mock_dispatcher.cunit-tests/libs/mock_dispatcher.hunit-tests/libs/cx_hash_mock.cunit-tests/libs/cx_hash_mock.hunit-tests/mock_includes/lcx_hash.hunit-tests/mock_includes/lcx_sha256.hunit-tests/test_get_preimage.cunit-tests/CMakeLists.txtInspect captured patch +1071 / −40
diff --git a/unit-tests/CMakeLists.txt b/unit-tests/CMakeLists.txt
index e617edf..5602e80 100644
--- a/unit-tests/CMakeLists.txt
+++ b/unit-tests/CMakeLists.txt
@@ -53,12 +53,15 @@ add_executable(test_display_utils test_display_utils.c)
add_executable(test_parser test_parser.c)
add_executable(test_script test_script.c)
add_executable(test_wallet test_wallet.c)
+add_executable(test_get_preimage test_get_preimage.c)
# add_executable(test_crypto test_crypto.c)
# Mock libraries
add_library(crypto_mocks SHARED libs/crypto_mocks.c)
add_library(sha256 SHARED libs/sha-256.c)
+add_library(cx_hash_mock SHARED libs/cx_hash_mock.c)
+add_library(mock_dispatcher SHARED libs/mock_dispatcher.c)
# App's libraries
add_library(base58 SHARED $ENV{BOLOS_SDK}/lib_standard_app/base58.c)
@@ -66,6 +69,8 @@ add_library(bip32 SHARED $ENV{BOLOS_SDK}/lib_standard_app/bip32.c)
add_library(buffer SHARED $ENV{BOLOS_SDK}/lib_standard_app/buffer.c)
add_library(buffer_ext SHARED ../src/common/buffer_ext.c)
add_library(display_utils SHARED ../src/ui/display_utils.c)
+add_library(get_preimage SHARED ../src/handler/lib/get_preimage.c)
+add_library(merkle SHARED ../src/common/merkle.c)
add_library(parser SHARED ../src/common/parser_ext.c)
add_library(read SHARED $ENV{BOLOS_SDK}/lib_standard_app/read.c)
add_library(script SHARED ../src/common/script.c)
@@ -75,8 +80,14 @@ add_library(write SHARED $ENV{BOLOS_SDK}/lib_standard_app/write.c)
# add_library(crypto SHARED ../src/crypto.c)
+# Additional include directories for handler code
+target_include_directories(get_preimage PRIVATE ../src/handler ../src/handler/lib)
+target_include_directories(mock_dispatcher PRIVATE ../src/handler ../src/handler/lib ../src/common)
+target_include_directories(test_get_preimage PRIVATE ../src/handler ../src/handler/lib)
+
# Mock libraries
target_link_libraries(crypto_mocks PUBLIC sha256)
+target_link_libraries(cx_hash_mock PUBLIC sha256)
# App's libraries
target_link_libraries(test_bitvector PUBLIC cmocka gcov)
@@ -85,6 +96,7 @@ target_link_libraries(test_display_utils PUBLIC cmocka gcov display_utils)
target_link_libraries(test_parser PUBLIC cmocka gcov parser buffer buffer_ext varint read write bip32)
target_link_libraries(test_script PUBLIC cmocka gcov script buffer varint read write bip32)
target_link_libraries(test_wallet PUBLIC cmocka gcov wallet script buffer buffer_ext varint read write bip32 base58 crypto_mocks)
+target_link_libraries(test_get_preimage PUBLIC cmocka gcov mock_dispatcher cx_hash_mock sha256 buffer buffer_ext varint read write bip32 merkle get_preimage)
# target_link_libraries(test_crypto PUBLIC cmocka gcov crypto)
add_test(test_bitvector test_bitvector)
@@ -93,5 +105,6 @@ add_test(test_display_utils test_display_utils)
add_test(test_parser test_parser)
add_test(test_script test_script)
add_test(test_wallet test_wallet)
+add_test(test_get_preimage test_get_preimage)
# add_test(test_crypto test_crypto)
diff --git a/unit-tests/libs/cx_hash_mock.c b/unit-tests/libs/cx_hash_mock.c
new file mode 100644
index 0000000..8ef68a7
--- /dev/null
+++ b/unit-tests/libs/cx_hash_mock.c
@@ -0,0 +1,94 @@
+/**
+ * Mock implementation of Ledger SDK cx_hash / cx_sha256 functions,
+ * wrapping the reference SHA-256 library (sha-256.c).
+ *
+ * Since `struct Sha_256` does not fit inside `cx_sha256_t`, we use a
+ * static pool of contexts and store the pool index in `cx_sha256_t.blen`.
+ */
+
+#include <string.h>
+#include <assert.h>
+
+#include "cx_hash_mock.h"
+#include "sha-256.h"
+
+/* Pool of streaming SHA-256 contexts for the mock */
+#define MAX_HASH_CONTEXTS 32
+static struct Sha_256 g_sha256_pool[MAX_HASH_CONTEXTS];
+static uint8_t g_sha256_hash_out[MAX_HASH_CONTEXTS][32];
+int g_sha256_pool_next = 0;
+
+/**
+ * Map a cx_sha256_t to its pool index. We store the index in `blen`.
+ */
+static struct Sha_256 *get_sha256_ctx(cx_sha256_t *hash) {
+ unsigned int idx = hash->blen;
+ assert(idx < MAX_HASH_CONTEXTS);
+ return &g_sha256_pool[idx];
+}
+
+int cx_sha256_init(cx_sha256_t *hash) {
+ memset(hash, 0, sizeof(cx_sha256_t));
+ hash->header.algo = CX_SHA256;
+ hash->header.counter = 0;
+
+ /* Allocate a pool slot */
+ assert(g_sha256_pool_next < MAX_HASH_CONTEXTS);
+ int idx = g_sha256_pool_next++;
+ hash->blen = (unsigned int) idx;
+
+ sha_256_init(&g_sha256_pool[idx], g_sha256_hash_out[idx]);
+ return CX_SHA256;
+}
+
+int cx_hash_no_throw(cx_hash_t *hash,
+ int mode,
+ const unsigned char *in,
+ unsigned int in_len,
+ unsigned char *out,
+ unsigned int out_len) {
+ /* We only support SHA-256 in this mock */
+ assert(hash->algo == CX_SHA256);
+
+ cx_sha256_t *sha = (cx_sha256_t *) hash;
+ struct Sha_256 *ctx = get_sha256_ctx(sha);
+
+ if (in != NULL && in_len > 0) {
+ sha_256_write(ctx, in, in_len);
+ }
+
+ if (mode & CX_LAST) {
+ uint8_t *result = sha_256_close(ctx);
+ if (out != NULL && out_len >= 32) {
+ memcpy(out, result, 32);
+ }
+ }
+
+ return 0;
+}
+
+int cx_hash_sha256(const unsigned char *in,
+ unsigned int in_len,
+ unsigned char *out,
+ unsigned int out_len) {
+ (void) out_len;
+ calc_sha_256(out, in, in_len);
+ return CX_SHA256_SIZE;
+}
+
+int cx_sha256_hash_iovec(const cx_iovec_t *iovec, size_t iovec_count, uint8_t out[32]) {
+ /* Use a temporary streaming context */
+ struct Sha_256 ctx;
+ uint8_t hash_buf[32];
+ sha_256_init(&ctx, hash_buf);
+
+ for (size_t i = 0; i < iovec_count; i++) {
+ if (iovec[i].iov_base != NULL && iovec[i].iov_len > 0) {
+ sha_256_write(&ctx, iovec[i].iov_base, iovec[i].iov_len);
+ }
+ }
+
+ sha_256_close(&ctx);
+ memcpy(out, hash_buf, 32);
+ return 0;
+}
diff --git a/unit-tests/libs/cx_hash_mock.h b/unit-tests/libs/cx_hash_mock.h
new file mode 100644
index 0000000..07dea73
--- /dev/null
+++ b/unit-tests/libs/cx_hash_mock.h
@@ -0,0 +1,23 @@
+#pragma once
+
+/**
+ * Mock implementation of the Ledger SDK cx_hash / cx_sha256 functions,
+ * backed by the reference SHA-256 library (sha-256.c from amosnier/sha-2).
+ *
+ * Provides:
+ * - cx_sha256_init
+ * - cx_hash_no_throw (update / finalize)
+ * - cx_hash_sha256 (one-shot)
+ * - cx_sha256_hash_iovec
+ *
+ * The function prototypes are already declared in the mock SDK headers
+ * (lcx_hash.h / lcx_sha256.h). This header just needs to be included
+ * in the mock .c file; test files should include the SDK headers via
+ * cx.h or directly.
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "os.h"
+#include "cx.h"
diff --git a/unit-tests/libs/mock_dispatcher.c b/unit-tests/libs/mock_dispatcher.c
new file mode 100644
index 0000000..73a8a35
--- /dev/null
+++ b/unit-tests/libs/mock_dispatcher.c
@@ -0,0 +1,472 @@
+/**
+ * Mock dispatcher_context_t implementation for unit testing.
+ *
+ * Implements the client-side command interpreter entirely in C, matching
+ * the behavior of the Python ClientCommandInterpreter in client_command.py.
+ */
+
+#include <string.h>
+#include <assert.h>
+#include <stdio.h>
+
+#include "mock_dispatcher.h"
+#include "cx_hash_mock.h"
+#include "sha-256.h"
+
+#include "buffer.h"
+#include "varint.h"
+#include "common/merkle.h"
+#include "client_commands.h"
+
+/* ---- Global pointer to active mock (needed for function-pointer callbacks) ---- */
+static mock_dispatcher_t *g_active_mock = NULL;
+
+/* ---- External: reset the cx_hash_mock pool ---- */
+extern int g_sha256_pool_next;
+
+void mock_dispatcher_reset_hash_pool(void) {
+ g_sha256_pool_next = 0;
+}
+
+/* ---- Helper: compute SHA-256 of a buffer ---- */
+static void mock_sha256(const uint8_t *data, size_t len, uint8_t out[32]) {
+ calc_sha_256(out, data, len);
+}
+
+/* ===========================================================================
+ * Merkle tree builder
+ *
+ * Builds a left-complete binary Merkle tree (RFC 6962 style) matching the
+ * Python MerkleTree class. We store all node hashes in a flat array using
+ * a recursive construction.
+ * =========================================================================== */
+
+/**
+ * Recursively compute the Merkle root for the sub-array hashes[begin..begin+size).
+ *
+ * The tree is a left-complete binary tree (RFC 6962 style): the left subtree
+ * always contains the largest power of 2 that is strictly less than `size`
+ * leaves, and the right subtree contains the remainder. Internal node hashes
+ * are computed by merkle_combine_hashes(left, right).
+ *
+ * The resulting root hash is written to `out`.
+ */
+
+static void build_merkle_root(const uint8_t hashes[][32],
+ size_t begin,
+ size_t size,
+ uint8_t out[32]) {
+ if (size == 0) {
+ memset(out, 0, 32);
+ return;
+ }
+ if (size == 1) {
+ memcpy(out, hashes[begin], 32);
+ return;
+ }
+
+ /* Left subtree has largest-power-of-2-less-than(size) leaves */
+ size_t lsize = 1;
+ while (2 * lsize < size) {
+ lsize *= 2;
+ }
+ /* lsize is the largest power of 2 < size (when size is not a power of 2)
+ * or size/2 (when size is a power of 2) */
+
+ uint8_t left_hash[32], right_hash[32];
+ build_merkle_root(hashes, begin, lsize, left_hash);
+ build_merkle_root(hashes, begin + lsize, size - lsize, right_hash);
+ merkle_combine_hashes(left_hash, right_hash, out);
+}
+
+/**
+ * Generate the Merkle proof for leaf at `leaf_index` in a tree of `size` elements.
+ * Returns the proof as an array of 32-byte sibling hashes (leaf to root), and
+ * sets *proof_len to the number of proof elements.
+ */
+static void generate_merkle_proof(const uint8_t hashes[][32],
+ size_t begin,
+ size_t size,
+ size_t leaf_index,
+ uint8_t proof[][32],
+ size_t *proof_len) {
+ if (size <= 1) {
+ *proof_len = 0;
+ return;
+ }
+
+ size_t lsize = 1;
+ while (2 * lsize < size) {
+ lsize *= 2;
+ }
+
+ uint8_t sibling_hash[32];
+
+ if (leaf_index < lsize) {
+ /* Leaf is in the left subtree; sibling is the right subtree root */
+ build_merkle_root(hashes, begin + lsize, size - lsize, sibling_hash);
+
+ size_t sub_proof_len = 0;
+ generate_merkle_proof(hashes, begin, lsize, leaf_index, proof, &sub_proof_len);
+
+ /* Append sibling at the end (proof goes leaf → root) */
+ memcpy(proof[sub_proof_len], sibling_hash, 32);
+ *proof_len = sub_proof_len + 1;
+ } else {
+ /* Leaf is in the right subtree; sibling is the left subtree root */
+ build_merkle_root(hashes, begin, lsize, sibling_hash);
+
+ size_t sub_proof_len = 0;
+ generate_merkle_proof(hashes,
+ begin + lsize,
+ size - lsize,
+ leaf_index - lsize,
+ proof,
+ &sub_proof_len);
+
+ memcpy(proof[sub_proof_len], sibling_hash, 32);
+ *proof_len = sub_proof_len + 1;
+ }
+}
+
+/* ===========================================================================
+ * dispatcher_context_t function-pointer implementations
+ * =========================================================================== */
+
+static void mock_add_to_response(const void *rdata, size_t rdata_len) {
+ assert(g_active_mock != NULL);
+ mock_dispatcher_t *m = g_active_mock;
+
+ assert(m->request_len + rdata_len <= sizeof(m->request_buf));
+ memcpy(m->request_buf + m->request_len, rdata, rdata_len);
+ m->request_len += rdata_len;
+}
+
+static void mock_finalize_response(uint16_t sw) {
+ assert(g_active_mock != NULL);
+ g_active_mock->last_sw = sw;
+}
+
+static void mock_send_response(void) {
+ /* No-op in mock: responses are consumed internally */
+}
+
+static void mock_set_ui_dirty(void) {
+ /* No-op in mock */
+}
+
+/* ---- Client command handlers ---- */
+
+static int handle_get_preimage(mock_dispatcher_t *m) {
+ /* Request format: <CCMD_GET_PREIMAGE:1> <hash_type:1> <hash:32> */
+ if (m->request_len < 1 + 1 + 32) {
+ return -1;
+ }
+
+ const uint8_t *req_hash = m->request_buf + 2; /* skip cmd byte and hash_type */
+
+ /* Look up the preimage */
+ int found_idx = -1;
+ for (size_t i = 0; i < m->n_preimages; i++) {
+ if (memcmp(m->preimages[i].hash, req_hash, 32) == 0) {
+ found_idx = (int) i;
+ break;
+ }
+ }
+
+ if (found_idx < 0) {
+ /* Unknown preimage — return error */
+ return -1;
+ }
+
+ const uint8_t *preimage = m->preimages[found_idx].data;
+ size_t preimage_len = m->preimages[found_idx].len;
+
+ /* Build response: <preimage_len_varint> <partial_data_len:1> <partial_data> */
+ uint8_t varint_buf[9];
+ int varint_len = varint_write(varint_buf, 0, (uint64_t) preimage_len);
+
+ /* Max payload in first response: 255 - varint_len - 1 */
+ size_t max_payload = 255 - (size_t) varint_len - 1;
+ size_t payload_size = preimage_len < max_payload ? preimage_len : max_payload;
+
+ /* If there's overflow, queue the remaining bytes as 1-byte elements */
+ if (payload_size < preimage_len) {
+ m->queue.element_size = 1;
+ m->queue.count = preimage_len - payload_size;
+ m->queue.head = 0;
+ for (size_t i = 0; i < m->queue.count; i++) {
+ m->queue.data[i][0] = preimage[payload_size + i];
+ }
+ }
+
+ /* Write response */
+ m->response_len = 0;
+ memcpy(m->response_buf + m->response_len, varint_buf, (size_t) varint_len);
+ m->response_len += (size_t) varint_len;
+ m->response_buf[m->response_len++] = (uint8_t) payload_size;
+ memcpy(m->response_buf + m->response_len, preimage, payload_size);
+ m->response_len += payload_size;
+
+ return 0;
+}
+
+static int handle_get_more_elements(mock_dispatcher_t *m) {
+ /* Request: just the command byte */
+ if (m->queue.head >= m->queue.count) {
+ return -1; /* Nothing in queue */
+ }
+
+ size_t element_size = m->queue.element_size;
+ size_t remaining = m->queue.count - m->queue.head;
+
+ /* Fit as many as possible in 255 bytes: 1 (n_elements) + 1 (el_len) + n*el_len <= 255 */
+ size_t max_elements = (253) / element_size;
+ size_t n_elements = remaining < max_elements ? remaining : max_elements;
+
+ m->response_len = 0;
+ m->response_buf[m->response_len++] = (uint8_t) n_elements;
+ m->response_buf[m->response_len++] = (uint8_t) element_size;
+
+ for (size_t i = 0; i < n_elements; i++) {
+ memcpy(m->response_buf + m->response_len, m->queue.data[m->queue.head], element_size);
+ m->response_len += element_size;
+ m->queue.head++;
+ }
+
+ return 0;
+}
+
+static int handle_yield(mock_dispatcher_t *m) {
+ /* Store everything after the command byte as a yielded value */
+ assert(m->n_yielded < MOCK_MAX_YIELDED);
+
+ size_t data_len = m->request_len > 0 ? m->request_len - 1 : 0;
+ assert(data_len <= MOCK_MAX_YIELDED_LEN);
+
+ m->yielded[m->n_yielded].len = data_len;
+ if (data_len > 0) {
+ memcpy(m->yielded[m->n_yielded].data, m->request_buf + 1, data_len);
+ }
+ m->n_yielded++;
+
+ /* Response is empty */
+ m->response_len = 0;
+ return 0;
+}
+
+static int handle_get_merkle_leaf_proof(mock_dispatcher_t *m) {
+ /* Request: <cmd:1> <merkle_root:32> <tree_size:varint> <leaf_index:varint> */
+ buffer_t req = buffer_create(m->request_buf + 1, m->request_len - 1);
+
+ uint8_t root[32];
+ if (!buffer_read_bytes(&req, root, 32)) return -1;
+
+ uint64_t tree_size_u64, leaf_index_u64;
+ if (!buffer_read_varint(&req, &tree_size_u64)) return -1;
+ if (!buffer_read_varint(&req, &leaf_index_u64)) return -1;
+
+ size_t tree_size = (size_t) tree_size_u64;
+ size_t leaf_index = (size_t) leaf_index_u64;
+
+ /* Find the tree by root */
+ mock_merkle_tree_t *tree = NULL;
+ for (size_t i = 0; i < m->n_trees; i++) {
+ if (memcmp(m->trees[i].root, root, 32) == 0) {
+ tree = &m->trees[i];
+ break;
+ }
+ }
+ if (tree == NULL || tree->n_elements != tree_size || leaf_index >= tree_size) {
+ return -1;
+ }
+
+ /* Generate proof */
+ uint8_t proof[MAX_MERKLE_TREE_DEPTH][32];
+ size_t proof_len = 0;
+ generate_merkle_proof((const uint8_t(*)[32]) tree->element_hashes,
+ 0,
+ tree->n_elements,
+ leaf_index,
+ proof,
+ &proof_len);
+
+ /* How many proof elements fit in first response: 255 - 32 - 1 - 1 = 221 bytes -> 221/32 = 6 */
+ size_t max_first = (255 - 32 - 1 - 1) / 32;
+ size_t n_response_elements = proof_len < max_first ? proof_len : max_first;
+ size_t n_leftover = proof_len - n_response_elements;
+
+ /* Queue leftover proof elements */
+ if (n_leftover > 0) {
+ m->queue.element_size = 32;
+ m->queue.count = n_leftover;
+ m->queue.head = 0;
+ for (size_t i = 0; i < n_leftover; i++) {
+ memcpy(m->queue.data[i], proof[n_response_elements + i], 32);
+ }
+ }
+
+ /* Build response: <leaf_hash:32> <proof_size:1> <n_proof_elements:1> <proof_hashes...> */
+ m->response_len = 0;
+ memcpy(m->response_buf + m->response_len, tree->element_hashes[leaf_index], 32);
+ m->response_len += 32;
+ m->response_buf[m->response_len++] = (uint8_t) proof_len;
+ m->response_buf[m->response_len++] = (uint8_t) n_response_elements;
+ for (size_t i = 0; i < n_response_elements; i++) {
+ memcpy(m->response_buf + m->response_len, proof[i], 32);
+ m->response_len += 32;
+ }
+
+ return 0;
+}
+
+static int handle_get_merkle_leaf_index(mock_dispatcher_t *m) {
+ /* Request: <cmd:1> <merkle_root:32> <leaf_hash:32> */
+ if (m->request_len < 1 + 32 + 32) return -1;
+
+ const uint8_t *root = m->request_buf + 1;
+ const uint8_t *leaf_hash = m->request_buf + 1 + 32;
+
+ /* Find tree */
+ mock_merkle_tree_t *tree = NULL;
+ for (size_t i = 0; i < m->n_trees; i++) {
+ if (memcmp(m->trees[i].root, root, 32) == 0) {
+ tree = &m->trees[i];
+ break;
+ }
+ }
+
+ uint8_t found = 0;
+ uint64_t index = 0;
+
+ if (tree != NULL) {
+ for (size_t i = 0; i < tree->n_elements; i++) {
+ if (memcmp(tree->element_hashes[i], leaf_hash, 32) == 0) {
+ found = 1;
+ index = (uint64_t) i;
+ break;
+ }
+ }
+ }
+
+ /* Response: <found:1> <index:varint> */
+ m->response_len = 0;
+ m->response_buf[m->response_len++] = found;
+ uint8_t varint_buf[9];
+ int vlen = varint_write(varint_buf, 0, index);
+ memcpy(m->response_buf + m->response_len, varint_buf, (size_t) vlen);
+ m->response_len += (size_t) vlen;
+
+ return 0;
+}
+
+/* ---- Main interruption handler ---- */
+
+static int mock_process_interruption(dispatcher_context_t *dc) {
+ mock_dispatcher_t *m = g_active_mock;
+ assert(m != NULL);
+ assert(dc == &m->dc);
+
+ if (m->request_len == 0) {
+ return -1;
+ }
+
+ uint8_t cmd = m->request_buf[0];
+ int rc;
+
+ switch (cmd) {
+ case CCMD_GET_PREIMAGE:
+ rc = handle_get_preimage(m);
+ break;
+ case CCMD_GET_MORE_ELEMENTS:
+ rc = handle_get_more_elements(m);
+ break;
+ case CCMD_YIELD:
+ rc = handle_yield(m);
+ break;
+ case CCMD_GET_MERKLE_LEAF_PROOF:
+ rc = handle_get_merkle_leaf_proof(m);
+ break;
+ case CCMD_GET_MERKLE_LEAF_INDEX:
+ rc = handle_get_merkle_leaf_index(m);
+ break;
+ default:
+ fprintf(stderr, "mock_process_interruption: unknown command 0x%02X\n", cmd);
+ rc = -1;
+ break;
+ }
+
+ /* Reset request buffer for next round */
+ m->request_len = 0;
+
+ if (rc < 0) {
+ return -1;
+ }
+
+ /* Set read_buffer to point at the response */
+ dc->read_buffer = buffer_create(m->response_buf, m->response_len);
+ return 0;
+}
+
+/* ===========================================================================
+ * Public API
+ * =========================================================================== */
+
+void mock_dispatcher_init(mock_dispatcher_t *mock) {
+ memset(mock, 0, sizeof(mock_dispatcher_t));
+
+ mock->dc.add_to_response = mock_add_to_response;
+ mock->dc.finalize_response = mock_finalize_response;
+ mock->dc.send_response = mock_send_response;
+ mock->dc.set_ui_dirty = mock_set_ui_dirty;
+ mock->dc.process_interruption = mock_process_interruption;
+
+ /* Set global pointer so callbacks can find us */
+ g_active_mock = mock;
+}
+
+void mock_dispatcher_add_preimage(mock_dispatcher_t *mock, const uint8_t *data, size_t len) {
+ assert(mock->n_preimages < MOCK_MAX_PREIMAGES);
+ assert(len <= MOCK_BUF_SIZE);
+
+ size_t idx = mock->n_preimages++;
+ mock_sha256(data, len, mock->preimages[idx].hash);
+ memcpy(mock->preimages[idx].data, data, len);
+ mock->preimages[idx].len = len;
+}
+
+void mock_dispatcher_add_list(mock_dispatcher_t *mock,
+ const uint8_t *const *elements,
+ const size_t *element_lens,
+ size_t n) {
+ assert(mock->n_trees < MOCK_MAX_TREES);
+ assert(n <= MOCK_MAX_TREE_ELEMS);
+
+ mock_merkle_tree_t *tree = &mock->trees[mock->n_trees++];
+ memset(tree, 0, sizeof(mock_merkle_tree_t));
+ tree->n_elements = n;
+
+ /* For each element:
+ * 1. Compute element_hash = SHA256(0x00 || element)
+ * 2. Store the raw element
+ * 3. Register the preimage (0x00 || element) so GET_PREIMAGE can retrieve it
+ */
+ for (size_t i = 0; i < n; i++) {
+ assert(element_lens[i] <= 256);
+
+ memcpy(tree->raw_elements[i], elements[i], element_lens[i]);
+ tree->raw_element_lens[i] = element_lens[i];
+
+ /* element_hash = SHA256(0x00 || element) */
+ merkle_compute_element_hash(elements[i], element_lens[i], tree->element_hashes[i]);
+
+ /* Register preimage: the preimage is (0x00 || element), its hash is element_hashes[i] */
+ uint8_t prefixed[257];
+ prefixed[0] = 0x00;
+ memcpy(prefixed + 1, elements[i], element_lens[i]);
+ mock_dispatcher_add_preimage(mock, prefixed, 1 + element_lens[i]);
+ }
+
+ /* Compute Merkle root */
+ build_merkle_root((const uint8_t(*)[32]) tree->element_hashes, 0, n, tree->root);
+}
diff --git a/unit-tests/libs/mock_dispatcher.h b/unit-tests/libs/mock_dispatcher.h
new file mode 100644
index 0000000..ff43e97
--- /dev/null
+++ b/unit-tests/libs/mock_dispatcher.h
@@ -0,0 +1,137 @@
+#pragma once
+
+/**
+ * Mock dispatcher_context_t for unit testing.
+ *
+ * This module implements a C equivalent of the Python ClientCommandInterpreter,
+ * handling the following client commands entirely in-process:
+ * - CCMD_GET_PREIMAGE (0x40)
+ * - CCMD_GET_MERKLE_LEAF_PROOF (0x41)
+ * - CCMD_GET_MERKLE_LEAF_INDEX (0x42)
+ * - CCMD_GET_MORE_ELEMENTS (0xA0)
+ * - CCMD_YIELD (0x10)
+ *
+ * Usage:
+ * mock_dispatcher_t mock;
+ * mock_dispatcher_init(&mock);
+ * mock_dispatcher_add_preimage(&mock, data, len);
+ * dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ * int result = call_get_preimage(dc, hash, out, out_len);
+ */
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "dispatcher.h"
+
+/* ---- Configuration ---- */
+#define MOCK_MAX_PREIMAGES 1024
+#define MOCK_MAX_TREES 16
+#define MOCK_MAX_TREE_ELEMS 1024
+#define MOCK_MAX_YIELDED 1024
+#define MOCK_MAX_QUEUE_ELEMS 1024
+#define MOCK_BUF_SIZE 2048
+#define MOCK_MAX_YIELDED_LEN 1024
+
+/* ---- Merkle tree storage ---- */
+typedef struct {
+ uint8_t root[32];
+
+ /* element_hashes[i] = SHA256(0x00 || raw_elements[i]) */
+ uint8_t element_hashes[MOCK_MAX_TREE_ELEMS][32];
+
+ /* raw element bytes (without 0x00 prefix) */
+ uint8_t raw_elements[MOCK_MAX_TREE_ELEMS][256];
+ size_t raw_element_lens[MOCK_MAX_TREE_ELEMS];
+
+ size_t n_elements;
+
+ /* all_hashes: full binary tree node hashes for proof generation.
+ * Indexed like a segment tree: node 1 = root, node 2i = left child, 2i+1 = right.
+ * We store up to 2 * MOCK_MAX_TREE_ELEMS nodes. */
+ uint8_t node_hashes[2 * MOCK_MAX_TREE_ELEMS][32];
+} mock_merkle_tree_t;
+
+/* ---- Queue for GET_MORE_ELEMENTS ---- */
+typedef struct {
+ uint8_t data[MOCK_MAX_QUEUE_ELEMS][32]; /* elements (up to 32 bytes each) */
+ size_t element_size; /* size of each element */
+ size_t count; /* total enqueued */
+ size_t head; /* next to dequeue */
+} mock_queue_t;
+
+/* ---- Main mock state ---- */
+typedef struct {
+ dispatcher_context_t dc; /* MUST be first member (container_of pattern) */
+
+ /* Request accumulation (from add_to_response calls) */
+ uint8_t request_buf[MOCK_BUF_SIZE];
+ size_t request_len;
+ uint16_t last_sw;
+
+ /* Client response buffer (backing for dc.read_buffer) */
+ uint8_t response_buf[MOCK_BUF_SIZE];
+ size_t response_len;
+
+ /* Known preimages: sha256(data) -> data */
+ struct {
+ uint8_t hash[32];
+ uint8_t data[MOCK_BUF_SIZE];
+ size_t len;
+ } preimages[MOCK_MAX_PREIMAGES];
+ size_t n_preimages;
+
+ /* Known Merkle trees */
+ mock_merkle_tree_t trees[MOCK_MAX_TREES];
+ size_t n_trees;
+
+ /* GET_MORE_ELEMENTS queue */
+ mock_queue_t queue;
+
+ /* Yielded values */
+ struct {
+ uint8_t data[MOCK_MAX_YIELDED_LEN];
+ size_t len;
+ } yielded[MOCK_MAX_YIELDED];
+ size_t n_yielded;
+} mock_dispatcher_t;
+
+/* ---- Public API ---- */
+
+/**
+ * Initialize a mock dispatcher. Zero-initializes all state and wires up
+ * the function pointers in mock->dc.
+ */
+void mock_dispatcher_init(mock_dispatcher_t *mock);
+
+/**
+ * Register a known preimage. Computes sha256(data) and stores the mapping.
+ * The mock will respond to CCMD_GET_PREIMAGE requests matching this hash.
+ */
+void mock_dispatcher_add_preimage(mock_dispatcher_t *mock, const uint8_t *data, size_t len);
+
+/**
+ * Build a Merkle tree from a list of elements and register it.
+ * Also registers each leaf preimage (0x00 || element) as a known preimage.
+ *
+ * @param elements Array of pointers to element data.
+ * @param element_lens Array of element lengths.
+ * @param n Number of elements.
+ */
+void mock_dispatcher_add_list(mock_dispatcher_t *mock,
+ const uint8_t *const *elements,
+ const size_t *element_lens,
+ size_t n);
+
+/**
+ * Get the dispatcher_context_t pointer for use with app functions.
+ */
+static inline dispatcher_context_t *mock_dispatcher_get_dc(mock_dispatcher_t *mock) {
+ return &mock->dc;
+}
+
+/**
+ * Reset the hash context pool (call between independent tests to avoid
+ * exhausting the fixed-size pool in cx_hash_mock).
+ */
+void mock_dispatcher_reset_hash_pool(void);
diff --git a/unit-tests/mock_includes/lcx_hash.h b/unit-tests/mock_includes/lcx_hash.h
index 2ffc1c9..56c50c9 100644
--- a/unit-tests/mock_includes/lcx_hash.h
+++ b/unit-tests/mock_includes/lcx_hash.h
@@ -30,34 +30,34 @@
#define PLENGTH(...)
#endif
-#define WIDE // const // don't !!
+#define WIDE // const // don't !!
/** Message Digest algorithm identifiers. */
enum cx_md_e {
- /** NONE Digest */
- CX_NONE,
- /** RIPEMD160 Digest */
- CX_RIPEMD160, // 20 bytes
- /** SHA224 Digest */
- CX_SHA224, // 28 bytes
- /** SHA256 Digest */
- CX_SHA256, // 32 bytes
- /** SHA384 Digest */
- CX_SHA384, // 48 bytes
- /** SHA512 Digest */
- CX_SHA512, // 64 bytes
- /** Keccak (pre-SHA3) Digest */
- CX_KECCAK, // 28,32,48,64 bytes
- /** SHA3 Digest */
- CX_SHA3, // 28,32,48,64 bytes
- /** Groestl Digest */
- CX_GROESTL,
- /** Blake Digest */
- CX_BLAKE2B,
- /** SHAKE-128 Digest */
- CX_SHAKE128, // any bytes
- /** SHAKE-128 Digest */
- CX_SHAKE256, // any bytes
+ /** NONE Digest */
+ CX_NONE,
+ /** RIPEMD160 Digest */
+ CX_RIPEMD160, // 20 bytes
+ /** SHA224 Digest */
+ CX_SHA224, // 28 bytes
+ /** SHA256 Digest */
+ CX_SHA256, // 32 bytes
+ /** SHA384 Digest */
+ CX_SHA384, // 48 bytes
+ /** SHA512 Digest */
+ CX_SHA512, // 64 bytes
+ /** Keccak (pre-SHA3) Digest */
+ CX_KECCAK, // 28,32,48,64 bytes
+ /** SHA3 Digest */
+ CX_SHA3, // 28,32,48,64 bytes
+ /** Groestl Digest */
+ CX_GROESTL,
+ /** Blake Digest */
+ CX_BLAKE2B,
+ /** SHAKE-128 Digest */
+ CX_SHAKE128, // any bytes
+ /** SHAKE-128 Digest */
+ CX_SHAKE256, // any bytes
};
/** Convenience type. See #cx_md_e. */
typedef enum cx_md_e cx_md_t;
@@ -72,10 +72,10 @@ typedef enum cx_md_e cx_md_t;
* Common Message Digest context, used as abstract type.
*/
struct cx_hash_header_s {
- /** Message digest identifier, See cx_md_e. */
- cx_md_t algo;
- /** Number of block already processed */
- unsigned int counter;
+ /** Message digest identifier, See cx_md_e. */
+ cx_md_t algo;
+ /** Number of block already processed */
+ unsigned int counter;
};
/** Convenience type. See #cx_hash_header_s. */
typedef struct cx_hash_header_s cx_hash_t;
@@ -116,8 +116,29 @@ typedef struct cx_hash_header_s cx_hash_t;
*
*/
CXCALL int cx_hash(cx_hash_t *hash PLENGTH(scc__cx_scc_struct_size_hash__hash),
- int mode, const unsigned char WIDE *in PLENGTH(len),
- unsigned int len, unsigned char *out PLENGTH(out_len),
+ int mode,
+ const unsigned char WIDE *in PLENGTH(len),
+ unsigned int len,
+ unsigned char *out PLENGTH(out_len),
unsigned int out_len);
+/**
+ * cx_hash_no_throw - same as cx_hash but returns error code instead of throwing.
+ * In unit tests we use this as the primary implementation.
+ */
+int cx_hash_no_throw(cx_hash_t *hash,
+ int mode,
+ const unsigned char *in,
+ unsigned int in_len,
+ unsigned char *out,
+ unsigned int out_len);
+
+/**
+ * I/O vector for cx_sha256_hash_iovec.
+ */
+typedef struct {
+ const uint8_t *iov_base;
+ size_t iov_len;
+} cx_iovec_t;
+
#endif
diff --git a/unit-tests/mock_includes/lcx_sha256.h b/unit-tests/mock_includes/lcx_sha256.h
index 6f9a1d1..d18dd97 100644
--- a/unit-tests/mock_includes/lcx_sha256.h
+++ b/unit-tests/mock_includes/lcx_sha256.h
@@ -28,14 +28,14 @@
* SHA-224 and SHA-256 context
*/
struct cx_sha256_s {
- /** @copydoc cx_ripemd160_s::header */
- struct cx_hash_header_s header;
- /** @internal @copydoc cx_ripemd160_s::blen */
- unsigned int blen;
- /** @internal @copydoc cx_ripemd160_s::block */
- unsigned char block[64];
- /** @copydoc cx_ripemd160_s::acc */
- unsigned char acc[8 * 4];
+ /** @copydoc cx_ripemd160_s::header */
+ struct cx_hash_header_s header;
+ /** @internal @copydoc cx_ripemd160_s::blen */
+ unsigned int blen;
+ /** @internal @copydoc cx_ripemd160_s::block */
+ unsigned char block[64];
+ /** @copydoc cx_ripemd160_s::acc */
+ unsigned char acc[8 * 4];
};
/** Convenience type. See #cx_sha256_s. */
typedef struct cx_sha256_s cx_sha256_t;
@@ -74,7 +74,13 @@ CXCALL int cx_sha256_init(cx_sha256_t *hash PLENGTH(sizeof(cx_sha256_t)));
*
*/
CXCALL int cx_hash_sha256(const unsigned char WIDE *in PLENGTH(len),
- unsigned int len, unsigned char *out PLENGTH(out_len),
+ unsigned int len,
+ unsigned char *out PLENGTH(out_len),
unsigned int out_len);
+/**
+ * Compute SHA-256 over an I/O vector.
+ */
+int cx_sha256_hash_iovec(const cx_iovec_t *iovec, size_t iovec_count, uint8_t out[32]);
+
#endif
diff --git a/unit-tests/test_get_preimage.c b/unit-tests/test_get_preimage.c
new file mode 100644
index 0000000..ec8962f
--- /dev/null
+++ b/unit-tests/test_get_preimage.c
@@ -0,0 +1,265 @@
+/**
+ * Unit tests for call_get_preimage using the mock dispatcher.
+ *
+ * Tests verify that the C implementation of call_get_preimage correctly
+ * handles the client command protocol (GET_PREIMAGE / GET_MORE_ELEMENTS)
+ * and validates the SHA-256 hash of the received data.
+ */
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <cmocka.h>
+
+/* SDK mock stubs */
+unsigned int pic(unsigned int linked_address) {
+ return linked_address;
+}
+#undef PIC
+#define PIC(x) (x)
+
+#include "mock_dispatcher.h"
+#include "cx_hash_mock.h"
+#include "sha-256.h"
+
+#include "handler/lib/get_preimage.h"
+
+/* ---------- Helpers ---------- */
+
+static void compute_sha256(const uint8_t *data, size_t len, uint8_t out[32]) {
+ calc_sha_256(out, data, len);
+}
+
+/* ---------- Test cases ---------- */
+
+/**
+ * Happy path: small preimage (fits entirely in the first response, no
+ * GET_MORE_ELEMENTS needed).
+ */
+static void test_get_preimage_small(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* A small preimage: 50 bytes */
+ uint8_t preimage[50];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) (i & 0xFF);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ uint8_t out[256];
+ memset(out, 0xAA, sizeof(out));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, (int) sizeof(preimage));
+ assert_memory_equal(out, preimage, sizeof(preimage));
+}
+
+/**
+ * Happy path: large preimage that requires GET_MORE_ELEMENTS to transfer
+ * all the bytes.
+ */
+static void test_get_preimage_large(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* A larger preimage: 300 bytes */
+ uint8_t preimage[300];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) ((i * 7 + 13) & 0xFF);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ uint8_t out[512];
+ memset(out, 0, sizeof(out));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, (int) sizeof(preimage));
+ assert_memory_equal(out, preimage, sizeof(preimage));
+}
+
+/**
+ * Error: requesting preimage of an unknown hash should return a negative value.
+ */
+static void test_get_preimage_unknown_hash(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* Don't register any preimage; just call with a random hash */
+ uint8_t hash[32] = {0xDE, 0xAD, 0xBE, 0xEF};
+ uint8_t out[256];
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ /* process_interruption returns -1 → call_get_preimage returns -1 */
+ assert_true(result < 0);
+}
+
+/**
+ * Error: output buffer too small for the preimage.
+ * call_get_preimage should return -10.
+ */
+static void test_get_preimage_buffer_too_small(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[100];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) i;
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ uint8_t out[50]; /* Too small! */
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, -10);
+}
+
+/**
+ * Edge case: minimal preimage of exactly 1 byte.
+ */
+static void test_get_preimage_one_byte(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[1] = {0x42};
+ mock_dispatcher_add_preimage(&mock, preimage, 1);
+
+ uint8_t hash[32];
+ compute_sha256(preimage, 1, hash);
+
+ uint8_t out[64];
+ memset(out, 0, sizeof(out));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, 1);
+ assert_int_equal(out[0], 0x42);
+}
+
+/**
+ * Edge case: preimage that exactly fills the max first-response payload.
+ *
+ * For a preimage of length L, the varint encoding takes:
+ * 1 byte if L < 253, 3 bytes if L < 65536, etc.
+ * Max payload = 255 - varint_len - 1.
+ * For varint_len=1: max_payload = 253.
+ * So a 253-byte preimage should fit exactly with no GET_MORE_ELEMENTS.
+ */
+static void test_get_preimage_exact_fit(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ uint8_t preimage[253];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) (i ^ 0xA5);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ uint8_t out[512];
+ memset(out, 0, sizeof(out));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, (int) sizeof(preimage));
+ assert_memory_equal(out, preimage, sizeof(preimage));
+}
+
+/**
+ * Edge case: preimage of length 254 (one byte over the exact-fit boundary,
+ * so a few bytes go through GET_MORE_ELEMENTS).
+ */
+static void test_get_preimage_one_byte_overflow(void **state) {
+ (void) state;
+
+ static mock_dispatcher_t mock;
+ mock_dispatcher_init(&mock);
+ mock_dispatcher_reset_hash_pool();
+
+ /* Varint encodings above 253 bytes (and less than 65536) take 3 bytes,
+ * therefore max_payload = 255 - 3 - 1 = 251.
+ * Hence, for length 254, 3 bytes go through GET_MORE_ELEMENTS.
+ */
+ uint8_t preimage[254];
+ for (size_t i = 0; i < sizeof(preimage); i++) {
+ preimage[i] = (uint8_t) (i * 3);
+ }
+
+ mock_dispatcher_add_preimage(&mock, preimage, sizeof(preimage));
+
+ uint8_t hash[32];
+ compute_sha256(preimage, sizeof(preimage), hash);
+
+ uint8_t out[512];
+ memset(out, 0, sizeof(out));
+
+ dispatcher_context_t *dc = mock_dispatcher_get_dc(&mock);
+ int result = call_get_preimage(dc, hash, out, sizeof(out));
+
+ assert_int_equal(result, (int) sizeof(preimage));
+ assert_memory_equal(out, preimage, sizeof(preimage));
+}
+
+/* ---------- Main ---------- */
+
+int main(void) {
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test(test_get_preimage_small),
+ cmocka_unit_test(test_get_preimage_large),
+ cmocka_unit_test(test_get_preimage_unknown_hash),
+ cmocka_unit_test(test_get_preimage_buffer_too_small),
+ cmocka_unit_test(test_get_preimage_one_byte),
+ cmocka_unit_test(test_get_preimage_exact_fit),
+ cmocka_unit_test(test_get_preimage_one_byte_overflow),
+ };
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.