Add typed accessors for the PSBT fields read while signing
What changed, and why it matters
This commit is a code cleanup (refactor) in Ledger's Bitcoin app. It moves the logic for reading PSBT (Partially Signed Bitcoin Transaction) fields into dedicated helper functions with clearer success/error/absent status codes. The author explicitly states there is no behavior change; the patch preserves existing handling of two optional fields exactly as before, with fixes for those edge cases planned in a follow-up commit. The changes add unit tests and improve code clarity, but do not by themselves fix a security bug.
No immediate security action required. Treat as a normal code-quality refactor. Review the planned follow-up commit that will tighten the defaulting behavior for PSBT_IN_SEQUENCE and PSBT_GLOBAL_FALLBACK_LOCKTIME, as that may have actual security relevance.
Security signals we found
Refactor of PSBT field parsing with explicit tri-state status codes
New unit tests assert distinction between ABSENT and ERROR statuses
Commit message explicitly states 'pure refactor: no behavioral change'
Two optional fields retain legacy defaulting behavior, marked for future fix
No direct evidence of vulnerability fix or exploit mitigation in this commit
Evidence from the diff
The change introduces sign_psbt/psbt_fields.{c,h}, which centralizes typed accessors for PSBT fields read during signing. Each accessor returns a tri-state psbt_field_status_t (ERROR=-1, ABSENT=0, PRESENT=1). Callers are updated to compare against PSBT_FIELD_PRESENT explicitly because ERROR is truthy (-1). The refactor replaces inline call_get_merkleized_map_value/call_get_merkleized_map_value_u32_le usage at 26 call sites. The commit message and code comments note that two optional fields deliberately retain pre-existing defaulting behavior: (1) PSBT_IN_SEQUENCE in txhashes.c falls back to 0xFFFFFFFF on any non-PRESENT status, and (2) PSBT_GLOBAL_FALLBACK_LOCKTIME reports every read failure as ABSENT, yielding locktime 0. Unit tests are added/updated to assert the separated statuses. No CVE, advisory, or vendor security disclosure is present in the supplied materials.
Changed components
src/handler/sign_psbt/psbt_fields.csrc/handler/sign_psbt/psbt_fields.hsrc/handler/sign_psbt/amount_from_psbt.csrc/handler/sign_psbt/init_global_state.csrc/handler/sign_psbt/preprocess_inputs.csrc/handler/sign_psbt/preprocess_outputs.csrc/handler/sign_psbt/sign_input.csrc/handler/sign_psbt/transaction_display.csrc/handler/sign_psbt/txhashes.csrc/handler/lib/get_merkleized_map_value.hunit-tests/test_get_merkleized_map_value.cunit-tests/test_psbt_fields.cInspect captured patch +955 / −342
### src/handler/lib/get_merkleized_map_value.h
@@ -1,8 +1,5 @@
#pragma once
-/* SDK headers */
-#include "read.h"
-
/* Local headers */
#include "dispatcher.h"
#include "map_value_status.h"
@@ -35,24 +32,3 @@ int call_get_merkleized_map_value(dispatcher_context_t *dispatcher_context,
size_t key_len,
uint8_t *out,
size_t out_len);
-
-/**
- * Convenience shortcut to read a little-endian unsigned 32-bit int.
- * TODO: more docs
- */
-static inline int call_get_merkleized_map_value_u32_le(dispatcher_context_t *dispatcher_context,
- const merkleized_map_commitment_t *map,
- const uint8_t *key,
- size_t key_len,
- uint32_t *out) {
- uint8_t result_raw[4];
-
- int res = call_get_merkleized_map_value(dispatcher_context, map, key, key_len, result_raw, 4);
- if (res != 4) {
- return -1;
- }
-
- *out = read_u32_le(result_raw, 0);
-
- return 4;
-}
### src/handler/sign_psbt/amount_from_psbt.c
@@ -20,8 +20,12 @@
/* Local headers */
#include "get_merkleized_map_value.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "psbt_parse_rawtx.h"
+/* SDK headers */
+#include "read.h"
+
/*
Convenience function to get the amount and scriptpubkey from the non-witness-utxo of a certain
input in a PSBTv2.
@@ -42,11 +46,7 @@ int __attribute__((noinline)) get_amount_scriptpubkey_from_psbt_nonwitness(
// Read the prevout index
uint32_t prevout_n;
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- &prevout_n)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
return -1;
}
### src/handler/sign_psbt/init_global_state.c
@@ -37,11 +37,11 @@
#include "error_codes.h"
#include "get_merkle_leaf_element.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "get_preimage.h"
#include "musig.h"
#include "policy.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
#include "wallet.h"
@@ -130,39 +130,25 @@ static bool __attribute__((noinline)) process_global_map(dispatcher_context_t *d
return false;
}
- uint8_t raw_result[9]; // max size for a varint
- int result_len;
-
// Read tx version
- result_len = call_get_merkleized_map_value(dc,
- &st->global_map,
- (uint8_t[]) {PSBT_GLOBAL_TX_VERSION},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 4) {
+ if (PSBT_FIELD_PRESENT != psbt_get_global_tx_version(dc, &st->global_map, &st->tx_version)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- st->tx_version = read_u32_le(raw_result, 0);
// Read fallback locktime.
// Unlike BIP-0370 recommendation, we use the fallback locktime as-is, ignoring each input's
// preferred height/block locktime. If that's relevant, the client must set the fallback
// locktime to the appropriate value before calling sign_psbt.
- result_len = call_get_merkleized_map_value(dc,
- &st->global_map,
- (uint8_t[]) {PSBT_GLOBAL_FALLBACK_LOCKTIME},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len < 0) {
- st->locktime = 0;
- } else if (result_len != 4) {
- SEND_SW(dc, SW_INCORRECT_DATA);
- return false;
- } else {
- st->locktime = read_u32_le(raw_result, 0);
+ switch (psbt_get_global_fallback_locktime(dc, &st->global_map, &st->locktime)) {
+ case PSBT_FIELD_ABSENT:
+ st->locktime = 0;
+ break;
+ case PSBT_FIELD_PRESENT:
+ break;
+ default: // PSBT_FIELD_ERROR: present but malformed
+ SEND_SW(dc, SW_INCORRECT_DATA);
+ return false;
}
return true;
### src/handler/sign_psbt/preprocess_inputs.c
@@ -33,11 +33,11 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "init_global_state.h"
#include "policy.h"
#include "process_in_outs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sighash.h"
#include "sign_psbt_cache.h"
#include "sw.h"
@@ -168,12 +168,8 @@ bool __attribute__((noinline)) preprocess_inputs(
// check if the prevout_hash of the transaction matches the computed one from the
// non-witness utxo
- if (32 != call_get_merkleized_map_value(dc,
- &input.in_out.map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- prevout_hash,
- sizeof(prevout_hash))) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_prevout_txid(dc, &input.in_out.map, prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
@@ -301,11 +297,8 @@ bool __attribute__((noinline)) preprocess_inputs(
}
// get the sighash_type
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- &input.in_out.map,
- (uint8_t[]) {PSBT_IN_SIGHASH_TYPE},
- 1,
- &input.sighash_type)) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_sighash_type(dc, &input.in_out.map, &input.sighash_type)) {
PRINTF("Malformed PSBT_IN_SIGHASH_TYPE for input %d\n", cur_input_index);
SEND_SW(dc, SW_INCORRECT_DATA);
### src/handler/sign_psbt/preprocess_outputs.c
@@ -30,9 +30,9 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "process_in_outs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
@@ -131,21 +131,12 @@ bool __attribute__((noinline)) preprocess_outputs(
return false;
}
- // Read output amount
- uint8_t raw_result[8];
-
// Read the output's amount
- int result_len = call_get_merkleized_map_value(dc,
- &output.in_out.map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 8) {
+ uint64_t value;
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &output.in_out.map, &value)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- uint64_t value = read_u64_le(raw_result, 0);
if (value > BITCOIN_TOTAL_SUPPLY) {
// sanity check to avoid overflows in amounts
@@ -158,20 +149,15 @@ bool __attribute__((noinline)) preprocess_outputs(
st->outputs.total_amount += value;
// Read the output's scriptPubKey
- result_len = call_get_merkleized_map_value(dc,
- &output.in_out.map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- output.in_out.scriptPubKey,
- sizeof(output.in_out.scriptPubKey));
-
- if (result_len < 0 || result_len > (int) sizeof(output.in_out.scriptPubKey)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_script(dc,
+ &output.in_out.map,
+ output.in_out.scriptPubKey,
+ sizeof(output.in_out.scriptPubKey),
+ &output.in_out.scriptPubKey_len)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- output.in_out.scriptPubKey_len = result_len;
-
int is_internal = is_in_out_internal(dc, st, sign_psbt_cache, &output.in_out, false);
if (is_internal < 0) {
### src/handler/sign_psbt/psbt_fields.c
@@ -0,0 +1,207 @@
+/*****************************************************************************
+ * Ledger App Bitcoin.
+ * (c) 2026 Ledger SAS.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *****************************************************************************/
+
+#include "psbt_fields.h"
+
+/* SDK headers */
+#include "read.h"
+
+/* Local headers */
+#include "constants.h"
+#include "get_merkleized_map_value.h"
+#include "psbt.h"
+
+/* -------------------------------------------------------------------------- */
+/* Reading helpers */
+/* -------------------------------------------------------------------------- */
+
+/*
+ * Every PSBT key handled here is a single key-type byte, and every field has a length policy (an
+ * exact size, or a maximum). These helpers apply that policy on top of the raw map reader and
+ * translate its outcome into a psbt_field_status_t, so the accessors below only have to name the
+ * key type and the expected shape.
+ *
+ * The length policy is a PSBT-level concern, which is why these live here rather than alongside
+ * call_get_merkleized_map_value: the map layer has no opinion on how long a value ought to be.
+ */
+
+/**
+ * Reads a value of variable length (up to `out_cap` bytes) into `out`, writing its length to
+ * `*out_len`. A value longer than `out_cap` is reported as PSBT_FIELD_ERROR.
+ */
+static psbt_field_status_t read_var(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ int res = call_get_merkleized_map_value(dc, map, &key_type, 1, out, out_cap);
+ if (res == MAP_VALUE_ABSENT) {
+ return PSBT_FIELD_ABSENT;
+ }
+ if (res < 0) {
+ return PSBT_FIELD_ERROR;
+ }
+ *out_len = (size_t) res;
+ return PSBT_FIELD_PRESENT;
+}
+
+/**
+ * Reads a value that must be exactly `len` bytes into `out`. A present value of any other length
+ * is malformed, hence PSBT_FIELD_ERROR.
+ */
+static psbt_field_status_t read_fixed(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint8_t *out,
+ size_t len) {
+ size_t read_len;
+ psbt_field_status_t status = read_var(dc, map, key_type, out, len, &read_len);
+ if (status != PSBT_FIELD_PRESENT) {
+ return status;
+ }
+ return read_len == len ? PSBT_FIELD_PRESENT : PSBT_FIELD_ERROR;
+}
+
+/** Reads a value that must be exactly 4 bytes, decoded as a little-endian unsigned 32-bit int. */
+static psbt_field_status_t read_u32_le_field(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint32_t *out) {
+ uint8_t raw[4];
+ psbt_field_status_t status = read_fixed(dc, map, key_type, raw, sizeof(raw));
+ if (status == PSBT_FIELD_PRESENT) {
+ *out = read_u32_le(raw, 0);
+ }
+ return status;
+}
+
+/** Reads a value that must be exactly 8 bytes, decoded as a little-endian unsigned 64-bit int. */
+static psbt_field_status_t read_u64_le_field(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *map,
+ uint8_t key_type,
+ uint64_t *out) {
+ uint8_t raw[8];
+ psbt_field_status_t status = read_fixed(dc, map, key_type, raw, sizeof(raw));
+ if (status == PSBT_FIELD_PRESENT) {
+ *out = read_u64_le(raw, 0);
+ }
+ return status;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Global map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_global_tx_version(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, global_map, PSBT_GLOBAL_TX_VERSION, out);
+}
+
+psbt_field_status_t psbt_get_global_fallback_locktime(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out) {
+ // Sized as a varint (9 bytes) so that a present-but-wrong-length value is classified as ERROR
+ // rather than being rejected as "too long" (which would be indistinguishable from ABSENT).
+ uint8_t raw[9];
+ size_t len;
+ psbt_field_status_t status =
+ read_var(dc, global_map, PSBT_GLOBAL_FALLBACK_LOCKTIME, raw, sizeof(raw), &len);
+ if (status != PSBT_FIELD_PRESENT) {
+ return PSBT_FIELD_ABSENT;
+ }
+ if (len != 4) {
+ return PSBT_FIELD_ERROR;
+ }
+ *out = read_u32_le(raw, 0);
+ return PSBT_FIELD_PRESENT;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Input map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_input_prevout_txid(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t out[static 32]) {
+ return read_fixed(dc, input_map, PSBT_IN_PREVIOUS_TXID, out, 32);
+}
+
+psbt_field_status_t psbt_get_input_prevout_index(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_OUTPUT_INDEX, out);
+}
+
+psbt_field_status_t psbt_get_input_sequence(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_SEQUENCE, out);
+}
+
+psbt_field_status_t psbt_get_input_sighash_type(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out) {
+ return read_u32_le_field(dc, input_map, PSBT_IN_SIGHASH_TYPE, out);
+}
+
+psbt_field_status_t psbt_get_input_redeem_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ return read_var(dc, input_map, PSBT_IN_REDEEM_SCRIPT, out, out_cap, out_len);
+}
+
+psbt_field_status_t psbt_get_input_witness_utxo_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint64_t *amount) {
+ // The witness UTXO is encoded as [8-byte amount][varint scriptPubKey len][scriptPubKey]. Here
+ // we only need the amount, but the whole value must fit in the buffer for the read to succeed.
+ uint8_t raw[8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN];
+ size_t len;
+ psbt_field_status_t status =
+ read_var(dc, input_map, PSBT_IN_WITNESS_UTXO, raw, sizeof(raw), &len);
+ if (status != PSBT_FIELD_PRESENT) {
+ return status;
+ }
+ if (len < 8) {
+ // present, but too short to even contain the amount
+ return PSBT_FIELD_ERROR;
+ }
+ *amount = read_u64_le(raw, 0);
+ return PSBT_FIELD_PRESENT;
+}
+
+/* -------------------------------------------------------------------------- */
+/* Output map */
+/* -------------------------------------------------------------------------- */
+
+psbt_field_status_t psbt_get_output_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint64_t *out) {
+ return read_u64_le_field(dc, output_map, PSBT_OUT_AMOUNT, out);
+}
+
+psbt_field_status_t psbt_get_output_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len) {
+ return read_var(dc, output_map, PSBT_OUT_SCRIPT, out, out_cap, out_len);
+}
### src/handler/sign_psbt/psbt_fields.h
@@ -0,0 +1,147 @@
+/*****************************************************************************
+ * Ledger App Bitcoin.
+ * (c) 2025, 2026 Ledger SAS.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *****************************************************************************/
+
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/* Local headers */
+#include "dispatcher.h"
+#include "merkle.h"
+
+/**
+ * Per-field accessors for the PSBT maps read while signing.
+ *
+ * Each function wraps the low-level merkleized-map value reader and owns the *structural*
+ * validation of one PSBT field: the key type, the expected length, whether the field is optional,
+ * and how to decode it into a typed value.
+ */
+
+/**
+ * Outcome of reading one PSBT field. Every accessor below returns this, so that a caller can
+ * always tell a field that is simply not there from one that could not be read.
+ *
+ * Callers applying a default for an optional field MUST branch on PSBT_FIELD_ABSENT specifically
+ * and treat PSBT_FIELD_ERROR as fatal. Testing for "not present" would silently substitute the
+ * default after a Merkle proof failure, making the device sign over a value the client never
+ * committed to.
+ *
+ * SECURITY: PSBT_FIELD_ABSENT rests on the client's word. See the note in
+ * handler/lib/map_value_status.h — the device requests no proof of absence, so a suppressed key
+ * cannot be told apart from an honest omission. Where that matters, use the presence flags derived
+ * from key enumeration (has_witnessUtxo and friends), which are committed to by keys_root.
+ */
+typedef enum {
+ PSBT_FIELD_ERROR = -1, // present but malformed (wrong length), or a fetch/proof failure
+ PSBT_FIELD_ABSENT = 0, // the key is not present in the map (per the client)
+ PSBT_FIELD_PRESENT = 1, // present and well-formed; the out-parameter has been written
+} psbt_field_status_t;
+
+/* -------------------------------------------------------------------------- */
+/* Global map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_GLOBAL_TX_VERSION: 4-byte little-endian version. Mandatory: ABSENT is a malformed PSBT. */
+psbt_field_status_t psbt_get_global_tx_version(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out);
+
+/**
+ * PSBT_GLOBAL_FALLBACK_LOCKTIME: optional 4-byte little-endian locktime.
+ * On ABSENT the caller must use locktime 0 (BIP-0370); ERROR must abort.
+ *
+ * NOTE: ABSENT currently also absorbs read failures (a value too long for the buffer, or a failed
+ * proof), which preserves the behaviour that predates this refactor. Tightened in a later commit.
+ */
+psbt_field_status_t psbt_get_global_fallback_locktime(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *global_map,
+ uint32_t *out);
+
+/* -------------------------------------------------------------------------- */
+/* Input map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_IN_PREVIOUS_TXID: 32-byte prevout txid. Mandatory. */
+psbt_field_status_t psbt_get_input_prevout_txid(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t out[static 32]);
+
+/** PSBT_IN_OUTPUT_INDEX: 4-byte little-endian prevout index. Mandatory. */
+psbt_field_status_t psbt_get_input_prevout_index(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_SEQUENCE: optional 4-byte little-endian nSequence.
+ * On ABSENT the caller must use the 0xFFFFFFFF default (BIP-0370).
+ *
+ * NOTE: the callers in txhashes.c currently also fall back to that default on ERROR, which
+ * preserves the behaviour that predates this refactor. Tightened in a later commit.
+ */
+psbt_field_status_t psbt_get_input_sequence(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_SIGHASH_TYPE: 4-byte little-endian sighash type. Callers that only read it once
+ * has_sighash_type is set treat ABSENT as a malformed PSBT.
+ */
+psbt_field_status_t psbt_get_input_sighash_type(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint32_t *out);
+
+/**
+ * PSBT_IN_REDEEM_SCRIPT: variable-length redeem script. On PSBT_FIELD_PRESENT, copies the value
+ * into `out` and writes its length to `*out_len`. A script longer than `out_cap` yields
+ * PSBT_FIELD_ERROR.
+ */
+psbt_field_status_t psbt_get_input_redeem_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len);
+
+/**
+ * PSBT_IN_WITNESS_UTXO amount: reads the witness UTXO and returns its 8-byte little-endian amount.
+ * Only requires the value to be at least 8 bytes; the full structural validation of the witness
+ * UTXO is done elsewhere (see get_amount_scriptpubkey_from_psbt_witness).
+ */
+psbt_field_status_t psbt_get_input_witness_utxo_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *input_map,
+ uint64_t *amount);
+
+/* -------------------------------------------------------------------------- */
+/* Output map */
+/* -------------------------------------------------------------------------- */
+
+/** PSBT_OUT_AMOUNT: 8-byte little-endian amount. Mandatory. */
+psbt_field_status_t psbt_get_output_amount(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint64_t *out);
+
+/**
+ * PSBT_OUT_SCRIPT: variable-length scriptPubKey. On PSBT_FIELD_PRESENT, copies the value into
+ * `out` and writes its length to `*out_len`. A script longer than `out_cap` yields
+ * PSBT_FIELD_ERROR.
+ */
+psbt_field_status_t psbt_get_output_script(dispatcher_context_t *dc,
+ const merkleized_map_commitment_t *output_map,
+ uint8_t *out,
+ size_t out_cap,
+ size_t *out_len);
### src/handler/sign_psbt/sign_input.c
@@ -34,12 +34,12 @@
#include "dispatcher.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "init_global_state.h"
#include "musig_signing.h"
#include "policy.h"
#include "preprocess_inputs.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "sign_psbt_cache.h"
#include "sw.h"
#include "txhashes.h"
@@ -231,11 +231,8 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_
// changes depending on the type of spend; therefore, we set it later.
if (input->has_sighash_type) {
// Get sighash type
- if (4 != call_get_merkleized_map_value_u32_le(dc,
- &input->in_out.map,
- (uint8_t[]) {PSBT_IN_SIGHASH_TYPE},
- 1,
- &input->sighash_type)) {
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_input_sighash_type(dc, &input->in_out.map, &input->sighash_type)) {
PRINTF("Malformed PSBT_IN_SIGHASH_TYPE for input %d\n", cur_input_index);
SEND_SW(dc, SW_INCORRECT_DATA);
@@ -324,14 +321,12 @@ static bool __attribute__((noinline)) sign_transaction_input(dispatcher_context_
// wrapped segwit transactions that we support
uint8_t redeemScript[MAX_PREVOUT_SCRIPTPUBKEY_LEN];
- int redeemScript_length =
- call_get_merkleized_map_value(dc,
- &input->in_out.map,
- (uint8_t[]) {PSBT_IN_REDEEM_SCRIPT},
- 1,
- redeemScript,
- sizeof(redeemScript));
- if (redeemScript_length < 0) {
+ size_t redeemScript_length;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_redeem_script(dc,
+ &input->in_out.map,
+ redeemScript,
+ sizeof(redeemScript),
+ &redeemScript_length)) {
PRINTF("Error fetching redeem script\n");
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
### src/handler/sign_psbt/transaction_display.c
@@ -29,9 +29,9 @@
#include "dispatcher.h"
#include "display.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "menu.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "script.h"
#include "sighash.h"
#include "sw.h"
@@ -91,38 +91,22 @@ static bool get_output_script_and_amount(
return false;
}
- // Read output amount
- uint8_t raw_result[8];
-
// Read the output's amount
- int result_len = call_get_merkleized_map_value(dc,
- &map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- raw_result,
- sizeof(raw_result));
- if (result_len != 8) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &map, out_amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- uint64_t value = read_u64_le(raw_result, 0);
- *out_amount = value;
// Read the output's scriptPubKey
- result_len = call_get_merkleized_map_value(dc,
- &map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- out_scriptPubKey,
- MAX_OUTPUT_SCRIPTPUBKEY_LEN);
-
- if (result_len < 0 || result_len > MAX_OUTPUT_SCRIPTPUBKEY_LEN) {
+ if (PSBT_FIELD_PRESENT != psbt_get_output_script(dc,
+ &map,
+ out_scriptPubKey,
+ MAX_OUTPUT_SCRIPTPUBKEY_LEN,
+ out_scriptPubKey_len)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- *out_scriptPubKey_len = result_len;
-
return true;
}
### src/handler/sign_psbt/txhashes.c
@@ -17,12 +17,15 @@
#include "txhashes.h"
+/* SDK headers */
+#include "write.h"
+
/* Local headers */
#include "amount_from_psbt.h"
#include "error_codes.h"
#include "get_merkleized_map.h"
-#include "get_merkleized_map_value.h"
#include "psbt.h"
+#include "psbt_fields.h"
#include "stream_merkleized_map_value.h"
/* BIP0341 tags for computing the tagged hashes when computing he sighash */
@@ -94,28 +97,21 @@ static int hash_output_n(dispatcher_context_t *dc,
}
// get output's amount
- uint8_t amount_raw[8];
- if (8 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_OUT_AMOUNT},
- 1,
- amount_raw,
- 8)) {
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_output_amount(dc, &ith_map, &amount)) {
return -1;
}
+ uint8_t amount_raw[8];
+ write_u64_le(amount_raw, 0, amount);
crypto_hash_update(hash_context, amount_raw, 8);
// get output's scriptPubKey
uint8_t out_script[MAX_OUTPUT_SCRIPTPUBKEY_LEN];
- int out_script_len = call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_OUT_SCRIPT},
- 1,
- out_script,
- sizeof(out_script));
- if (out_script_len < 0) {
+ size_t out_script_len;
+ if (PSBT_FIELD_PRESENT !=
+ psbt_get_output_script(dc, &ith_map, out_script, sizeof(out_script), &out_script_len)) {
return -1;
}
@@ -160,42 +156,31 @@ bool __attribute__((noinline)) compute_tx_hashes(dispatcher_context_t *dc,
// get prevout hash and output index for the i-th input
uint8_t ith_prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- ith_prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, &ith_map, ith_prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sha_prevouts_context.header, ith_prevout_hash, 32);
- uint8_t ith_prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- ith_prevout_n_raw,
- 4)) {
+ uint32_t ith_prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, &ith_map, &ith_prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t ith_prevout_n_raw[4];
+ write_u32_le(ith_prevout_n_raw, 0, ith_prevout_n);
crypto_hash_update(&sha_prevouts_context.header, ith_prevout_n_raw, 4);
- uint8_t ith_nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- ith_nSequence_raw,
- 4)) {
+ uint32_t ith_nSequence;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_sequence(dc, &ith_map, &ith_nSequence)) {
// if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(ith_nSequence_raw, 0xFF, 4);
+ ith_nSequence = 0xFFFFFFFF;
}
+ uint8_t ith_nSequence_raw[4];
+ write_u32_le(ith_nSequence_raw, 0, ith_nSequence);
crypto_hash_update(&sha_sequences_context.header, ith_nSequence_raw, 4);
}
@@ -309,29 +294,21 @@ bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_t *dc,
// get prevout hash and output index for the i-th input
uint8_t ith_prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- ith_prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, &ith_map, ith_prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, ith_prevout_hash, 32);
- uint8_t ith_prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- ith_prevout_n_raw,
- 4)) {
+ uint32_t ith_prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, &ith_map, &ith_prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t ith_prevout_n_raw[4];
+ write_u32_le(ith_prevout_n_raw, 0, ith_prevout_n);
crypto_hash_update(&sighash_context.header, ith_prevout_n_raw, 4);
if (i != input_index) {
@@ -368,17 +345,14 @@ bool __attribute__((noinline)) compute_sighash_legacy(dispatcher_context_t *dc,
}
}
- uint8_t ith_nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- &ith_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- ith_nSequence_raw,
- 4)) {
+ uint32_t ith_nSequence;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_sequence(dc, &ith_map, &ith_nSequence)) {
// if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(ith_nSequence_raw, 0xFF, 4);
+ ith_nSequence = 0xFFFFFFFF;
}
+ uint8_t ith_nSequence_raw[4];
+ write_u32_le(ith_nSequence_raw, 0, ith_nSequence);
crypto_hash_update(&sighash_context.header, ith_nSequence_raw, 4);
}
@@ -450,29 +424,21 @@ bool __attribute__((noinline)) compute_sighash_segwitv0(
// get prevout hash and output index for the current input
uint8_t prevout_hash[32];
- if (32 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- prevout_hash,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, input_map, prevout_hash)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, prevout_hash, 32);
- uint8_t prevout_n_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- prevout_n_raw,
- 4)) {
+ uint32_t prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ uint8_t prevout_n_raw[4];
+ write_u32_le(prevout_n_raw, 0, prevout_n);
crypto_hash_update(&sighash_context.header, prevout_n_raw, 4);
}
@@ -521,37 +487,28 @@ bool __attribute__((noinline)) compute_sighash_segwitv0(
}
{
- // input value, taken from the WITNESS_UTXO field
- uint8_t witness_utxo[8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN];
-
- int witness_utxo_len = call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_WITNESS_UTXO},
- 1,
- witness_utxo,
- sizeof(witness_utxo));
- if (witness_utxo_len < 8) {
+ // input value (8-byte amount), taken from the WITNESS_UTXO field
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_witness_utxo_amount(dc, input_map, &amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
- crypto_hash_update(&sighash_context.header,
- witness_utxo,
- 8); // only the first 8 bytes (amount)
+ uint8_t amount_raw[8];
+ write_u64_le(amount_raw, 0, amount);
+ crypto_hash_update(&sighash_context.header, amount_raw, 8);
}
// nSequence
{
- uint8_t nSequence_raw[4];
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- nSequence_raw,
- 4)) {
+ uint32_t nSequence;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_sequence(dc, input_map, &nSequence)) {
// if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(nSequence_raw, 0xFF, 4);
+ nSequence = 0xFFFFFFFF;
}
+
+ uint8_t nSequence_raw[4];
+ write_u32_le(nSequence_raw, 0, nSequence);
crypto_hash_update(&sighash_context.header, nSequence_raw, 4);
}
@@ -643,56 +600,42 @@ bool __attribute__((noinline)) compute_sighash_segwitv1(
if ((sighash_byte & 0x80) == SIGHASH_ANYONECANPAY) {
// outpoint (hash)
- if (32 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_PREVIOUS_TXID},
- 1,
- tmp,
- 32)) {
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_txid(dc, input_map, tmp)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
crypto_hash_update(&sighash_context.header, tmp, 32);
// outpoint (output index)
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_OUTPUT_INDEX},
- 1,
- tmp,
- 4)) {
+ uint32_t prevout_n;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_prevout_index(dc, input_map, &prevout_n)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
+ write_u32_le(tmp, 0, prevout_n);
crypto_hash_update(&sighash_context.header, tmp, 4);
- if (8 > call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_WITNESS_UTXO},
- 1,
- tmp,
- 8 + 1 + MAX_PREVOUT_SCRIPTPUBKEY_LEN)) {
+ uint64_t amount;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_witness_utxo_amount(dc, input_map, &amount)) {
SEND_SW(dc, SW_INCORRECT_DATA);
return false;
}
// amount
+ write_u64_le(tmp, 0, amount);
crypto_hash_update(&sighash_context.header, tmp, 8);
// scriptPubKey
crypto_hash_update_varint(&sighash_context.header, scriptPubKey_len);
crypto_hash_update(&sighash_context.header, scriptPubKey, scriptPubKey_len);
// nSequence
- if (4 != call_get_merkleized_map_value(dc,
- input_map,
- (uint8_t[]) {PSBT_IN_SEQUENCE},
- 1,
- tmp,
- 4)) {
+ uint32_t nSequence;
+ if (PSBT_FIELD_PRESENT != psbt_get_input_sequence(dc, input_map, &nSequence)) {
// if no PSBT_IN_SEQUENCE is present, we must assume nSequence 0xFFFFFFFF
- memset(tmp, 0xFF, 4);
+ nSequence = 0xFFFFFFFF;
}
+ write_u32_le(tmp, 0, nSequence);
crypto_hash_update(&sighash_context.header, tmp, 4);
} else {
// input_index
### unit-tests/CMakeLists.txt
@@ -263,6 +263,7 @@ if(SPECULOS AND SPECULOS_SRC)
../src/handler/lib/stream_merkleized_map_value.c
../src/handler/lib/stream_preimage.c
../src/handler/sign_psbt/extract_bip32_derivation.c
+ ../src/handler/sign_psbt/psbt_fields.c
../src/handler/sign_psbt/sign_psbt_cache.c
../src/musig/musig.c
$ENV{BOLOS_SDK}/lib_standard_app/crypto_helpers.c
@@ -307,13 +308,14 @@ if(SPECULOS AND SPECULOS_SRC)
add_unit_test(test_stream_merkleized_map_value LIBS test_merkle_common)
add_unit_test(test_get_merkleized_map LIBS test_merkle_common)
add_unit_test(test_get_merkleized_map_value LIBS test_merkle_common)
+ add_unit_test(test_psbt_fields LIBS test_merkle_common)
foreach(t
test_get_preimage test_get_merkle_preimage test_extract_bip32_derivation
test_check_merkle_tree_sorted test_get_merkle_leaf_element
test_get_merkle_leaf_hash test_get_merkle_leaf_index test_stream_preimage
test_stream_merkle_leaf_element test_stream_merkleized_map_value
- test_get_merkleized_map test_get_merkleized_map_value)
+ test_get_merkleized_map test_get_merkleized_map_value test_psbt_fields)
app_apply_real_sdk_config(${t})
endforeach()
### unit-tests/test_get_merkleized_map_value.c
@@ -1,7 +1,5 @@
/**
- * Unit tests for call_get_merkleized_map_value (and the
- * call_get_merkleized_map_value_u32_le convenience wrapper) using the
- * mock dispatcher.
+ * Unit tests for call_get_merkleized_map_value using the mock dispatcher.
*
* call_get_merkleized_map_value looks up a key in a merkleized key-value
* map (by finding its index via Merkle leaf index), then fetches the
@@ -125,8 +123,8 @@ static void test_map_value_unsorted_input(void **state) {
}
/**
- * Error: the key is not present in the map. call_get_merkle_leaf_index
- * returns a negative value, which must be propagated as -1.
+ * The key is not present in the map, which must be reported as MAP_VALUE_ABSENT rather than as a
+ * generic failure.
*/
static void test_map_value_key_not_found(void **state) {
mock_dispatcher_t *mock = *state;
@@ -152,13 +150,14 @@ static void test_map_value_key_not_found(void **state) {
sizeof(missing_key),
out,
sizeof(out));
- assert_int_equal(result, -1);
+ /* A genuinely missing key must be reported as absent, distinctly from a failed lookup, so
+ * that callers may safely apply a default for an optional field. */
+ assert_int_equal(result, MAP_VALUE_ABSENT);
}
/**
- * Error: output buffer too small to hold the value. The underlying
- * call_get_merkle_leaf_element returns a negative value when the
- * preimage does not fit, which must be propagated.
+ * Error: output buffer too small to hold the value. The underlying flow reports that the preimage
+ * does not fit, which must reach the caller as MAP_VALUE_ERROR.
*/
static void test_map_value_out_buffer_too_small(void **state) {
mock_dispatcher_t *mock = *state;
@@ -178,7 +177,10 @@ static void test_map_value_out_buffer_too_small(void **state) {
uint8_t out[2];
dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
int result = call_get_merkleized_map_value(dc, &commitment, key, sizeof(key), out, sizeof(out));
- assert_true(result < 0);
+ /* Present but too long for the buffer is an ERROR, NOT ABSENT. Before the statuses were
+ * separated both surfaced as -1, so a caller substituting a default for an optional field
+ * would have signed over a value the client never committed to. */
+ assert_int_equal(result, MAP_VALUE_ERROR);
}
/**
@@ -207,93 +209,6 @@ static void test_map_value_empty_value(void **state) {
assert_int_equal(out[0], 0xCD);
}
-/**
- * Happy path for call_get_merkleized_map_value_u32_le: a 4-byte
- * little-endian value is decoded into a uint32_t.
- */
-static void test_map_value_u32_le(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- /* 0xDEADBEEF in little-endian */
- const uint8_t value[] = {0xEF, 0xBE, 0xAD, 0xDE};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- uint32_t got = 0;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc, &commitment, key, sizeof(key), &got);
-
- assert_int_equal(result, 4);
- assert_int_equal(got, 0xDEADBEEFu);
-}
-
-/**
- * Error path for call_get_merkleized_map_value_u32_le: the value at the
- * key has a length different from 4. The wrapper must return -1 without
- * writing to the output.
- */
-static void test_map_value_u32_le_wrong_length(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- /* Only 3 bytes — not a valid 32-bit value. */
- const uint8_t value[] = {0x01, 0x02, 0x03};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- uint32_t got = 0xCAFEBABEu;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc, &commitment, key, sizeof(key), &got);
-
- assert_int_equal(result, -1);
- /* The wrapper bails out before assigning to *out. */
- assert_int_equal(got, 0xCAFEBABEu);
-}
-
-/**
- * Error path for call_get_merkleized_map_value_u32_le: the key is not
- * present in the map.
- */
-static void test_map_value_u32_le_key_not_found(void **state) {
- mock_dispatcher_t *mock = *state;
-
- const uint8_t key[] = {0x10};
- const uint8_t value[] = {0x01, 0x02, 0x03, 0x04};
-
- const uint8_t *keys[] = {key};
- const size_t key_lens[] = {sizeof(key)};
- const uint8_t *values[] = {value};
- const size_t value_lens[] = {sizeof(value)};
-
- merkleized_map_commitment_t commitment;
- mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, &commitment);
-
- const uint8_t missing_key[] = {0xFF};
-
- uint32_t got = 0;
- dispatcher_context_t *dc = mock_dispatcher_get_dc(mock);
- int result = call_get_merkleized_map_value_u32_le(dc,
- &commitment,
- missing_key,
- sizeof(missing_key),
- &got);
-
- assert_int_equal(result, -1);
-}
-
/**
* Adversarial: client corrupts the leaf hash in the merkle proof for the
* VALUES tree. After the key index is resolved correctly, the value leaf
@@ -354,9 +269,6 @@ int main(void) {
T(test_map_value_key_not_found),
T(test_map_value_out_buffer_too_small),
T(test_map_value_empty_value),
- T(test_map_value_u32_le),
- T(test_map_value_u32_le_wrong_length),
- T(test_map_value_u32_le_key_not_found),
T(test_map_value_corrupted_value_proof),
};
#undef T
### unit-tests/test_psbt_fields.c
@@ -0,0 +1,482 @@
+/**
+ * Unit tests for the per-field PSBT accessors in handler/sign_psbt/psbt_fields.c.
+ *
+ * The property under test throughout is that the three outcomes stay separated: a field that is
+ * not in the map (PSBT_FIELD_ABSENT), a field that is there and well-formed (PSBT_FIELD_PRESENT),
+ * and a field that is there but cannot be used (PSBT_FIELD_ERROR).
+ *
+ * This matters because the callers of the optional fields substitute a default value on ABSENT.
+ * If a malformed or unfetchable field were reported as absent, the device would hash and sign that
+ * default even though the client committed to something else.
+ */
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+
+#include <cmocka.h>
+
+#include "mock_dispatcher.h"
+
+#include "common/psbt.h"
+#include "handler/sign_psbt/psbt_fields.h"
+
+/* ---------- Helpers ---------- */
+
+/**
+ * Registers a map holding a single (key_type, value) pair and returns its commitment.
+ */
+static void map_with_one_field(mock_dispatcher_t *mock,
+ uint8_t key_type,
+ const uint8_t *value,
+ size_t value_len,
+ merkleized_map_commitment_t *out) {
+ const uint8_t key[] = {key_type};
+ const uint8_t *keys[] = {key};
+ const size_t key_lens[] = {sizeof(key)};
+ const uint8_t *values[] = {value};
+ const size_t value_lens[] = {value_len};
+
+ mock_dispatcher_add_map(mock, keys, key_lens, values, value_lens, 1, out);
+}
+
+/**
+ * Registers a map holding a single unrelated field, so that lookups of anything else are absent.
+ */
+static void map_without_field(mock_dispatcher_t *mock, merkleized_map_commitment_t *out) {
+ /* An arbitrary key type that none of the accessors under test reads. */
+ const uint8_t filler[] = {0xAB};
+ map_with_one_field(mock, 0xFD, filler, sizeof(filler), out);
+}
+
+/* ---------- PSBT_IN_SEQUENCE (optional) ---------- */
+
+static void test_sequence_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* 0x12345678 little-endian */
+ const uint8_t value[] = {0x78, 0x56, 0x34, 0x12};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 0x12345678u);
+}
+
+static void test_sequence_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ /* Only this outcome may lead the caller to substitute the 0xFFFFFFFF default. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/**
+ * A present-but-malformed nSequence must NOT be reported as absent. This is the case that used to
+ * silently yield the 0xFFFFFFFF default.
+ */
+static void test_sequence_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t too_short[] = {0x01, 0x02, 0x03};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, too_short, sizeof(too_short), &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+static void test_sequence_too_long_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t too_long[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SEQUENCE, too_long, sizeof(too_long), &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status = psbt_get_input_sequence(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- PSBT_GLOBAL_FALLBACK_LOCKTIME (optional) ---------- */
+
+static void test_fallback_locktime_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0xD2, 0x04, 0x00, 0x00}; /* 1234 */
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_GLOBAL_FALLBACK_LOCKTIME, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 1234);
+}
+
+static void test_fallback_locktime_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ /* Only this outcome may lead the caller to substitute locktime 0. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+static void test_fallback_locktime_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t wrong_length[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock,
+ PSBT_GLOBAL_FALLBACK_LOCKTIME,
+ wrong_length,
+ sizeof(wrong_length),
+ &map);
+
+ uint32_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_global_fallback_locktime(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- Mandatory fields ---------- */
+
+static void test_prevout_txid_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t txid[32];
+ memset(txid, 0x5A, sizeof(txid));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_PREVIOUS_TXID, txid, sizeof(txid), &map);
+
+ uint8_t got[32];
+ memset(got, 0, sizeof(got));
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_memory_equal(got, txid, sizeof(txid));
+}
+
+static void test_prevout_txid_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint8_t got[32];
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ /* Fatal for the caller either way, but still reported distinctly from a malformed value. */
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+/**
+ * A short txid must be rejected rather than accepted with an uninitialized tail.
+ */
+static void test_prevout_txid_short_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t txid[31];
+ memset(txid, 0x5A, sizeof(txid));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_PREVIOUS_TXID, txid, sizeof(txid), &map);
+
+ uint8_t got[32];
+ psbt_field_status_t status =
+ psbt_get_input_prevout_txid(mock_dispatcher_get_dc(mock), &map, got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+}
+
+static void test_output_amount_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* 0x0102030405060708 little-endian */
+ const uint8_t value[] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_AMOUNT, value, sizeof(value), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_true(got == 0x0102030405060708ull);
+}
+
+static void test_output_amount_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+static void test_output_amount_wrong_length_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x01, 0x02, 0x03, 0x04};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_AMOUNT, value, sizeof(value), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status = psbt_get_output_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+}
+
+/* ---------- Key-type pinning for the remaining fixed-width fields ---------- */
+
+/*
+ * These accessors are one-line delegations to the shared readers, whose behaviour is already
+ * exercised above. What is specific to each of them - and what nothing else would catch - is that
+ * it names the right PSBT key type: reading the wrong one would report the field as absent, and
+ * the caller would silently carry on with a default.
+ */
+
+static void test_global_tx_version_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x02, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_GLOBAL_TX_VERSION, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_global_tx_version(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 2);
+}
+
+static void test_prevout_index_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x03, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_OUTPUT_INDEX, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_prevout_index(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 3);
+}
+
+static void test_sighash_type_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t value[] = {0x01, 0x00, 0x00, 0x00};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_SIGHASH_TYPE, value, sizeof(value), &map);
+
+ uint32_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_sighash_type(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 1);
+}
+
+/* ---------- Variable-length fields ---------- */
+
+static void test_output_script_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t script[] = {0x76, 0xA9, 0x14, 0x01, 0x02, 0x03};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got_len, sizeof(script));
+ assert_memory_equal(got, script, sizeof(script));
+}
+
+static void test_output_script_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+}
+
+/**
+ * A script longer than the caller's buffer is an error, not an absent field.
+ */
+static void test_output_script_too_long_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ uint8_t script[40];
+ memset(script, 0x51, sizeof(script));
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_OUT_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[16];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_output_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+}
+
+static void test_redeem_script_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t script[] = {0xA9, 0x14, 0xDE, 0xAD, 0xBE, 0xEF, 0x87};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_REDEEM_SCRIPT, script, sizeof(script), &map);
+
+ uint8_t got[64];
+ size_t got_len = 0;
+ psbt_field_status_t status =
+ psbt_get_input_redeem_script(mock_dispatcher_get_dc(mock), &map, got, sizeof(got), &got_len);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got_len, sizeof(script));
+ assert_memory_equal(got, script, sizeof(script));
+}
+
+/* ---------- Witness UTXO amount ---------- */
+
+/**
+ * The witness UTXO is a serialized txout: [8-byte amount][varint len][scriptPubKey]. The accessor
+ * reads the whole value but returns only the amount.
+ */
+static void test_witness_utxo_amount_present(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ /* amount 0x0000000005F5E100 (100000000) little-endian, then a 22-byte P2WPKH scriptPubKey. */
+ uint8_t txout[8 + 1 + 22];
+ memset(txout, 0, sizeof(txout));
+ txout[0] = 0x00;
+ txout[1] = 0xE1;
+ txout[2] = 0xF5;
+ txout[3] = 0x05;
+ txout[8] = 22;
+ memset(txout + 9, 0x33, 22);
+
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_WITNESS_UTXO, txout, sizeof(txout), &map);
+
+ uint64_t got = 0;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_PRESENT);
+ assert_int_equal(got, 100000000u);
+}
+
+static void test_witness_utxo_amount_absent(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ merkleized_map_commitment_t map;
+ map_without_field(mock, &map);
+
+ uint64_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ABSENT);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/**
+ * A witness UTXO too short to even contain the 8-byte amount is malformed, not absent: defaulting
+ * on it would mean signing over an amount the client never committed to.
+ */
+static void test_witness_utxo_amount_too_short_is_error(void **state) {
+ mock_dispatcher_t *mock = *state;
+
+ const uint8_t truncated[] = {0x01, 0x02, 0x03, 0x04, 0x05};
+ merkleized_map_commitment_t map;
+ map_with_one_field(mock, PSBT_IN_WITNESS_UTXO, truncated, sizeof(truncated), &map);
+
+ uint64_t got = 0xCAFEBABEu;
+ psbt_field_status_t status =
+ psbt_get_input_witness_utxo_amount(mock_dispatcher_get_dc(mock), &map, &got);
+
+ assert_int_equal(status, PSBT_FIELD_ERROR);
+ assert_int_equal(got, 0xCAFEBABEu);
+}
+
+/* ---------- Main ---------- */
+
+int main(void) {
+#define T(fn) cmocka_unit_test_setup_teardown(fn, mock_dispatcher_setup, mock_dispatcher_teardown)
+ const struct CMUnitTest tests[] = {
+ T(test_sequence_present),
+ T(test_sequence_absent),
+ T(test_sequence_wrong_length_is_error),
+ T(test_sequence_too_long_is_error),
+ T(test_fallback_locktime_present),
+ T(test_fallback_locktime_absent),
+ T(test_fallback_locktime_wrong_length_is_error),
+ T(test_prevout_txid_present),
+ T(test_prevout_txid_absent),
+ T(test_prevout_txid_short_is_error),
+ T(test_output_amount_present),
+ T(test_output_amount_absent),
+ T(test_output_amount_wrong_length_is_error),
+ T(test_global_tx_version_present),
+ T(test_prevout_index_present),
+ T(test_sighash_type_present),
+ T(test_output_script_present),
+ T(test_output_script_absent),
+ T(test_output_script_too_long_is_error),
+ T(test_redeem_script_present),
+ T(test_witness_utxo_amount_present),
+ T(test_witness_utxo_amount_absent),
+ T(test_witness_utxo_amount_too_short_is_error),
+ };
+#undef T
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}Why this scored 12/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.