Trustworthy sign_psbt amount/fee display for non-default sighash: unit-tests
What changed, and why it matters
This commit only adds new automated unit tests for functions that decide how transaction amounts and fees are displayed on a Ledger device when signing a Bitcoin transaction with non-standard signature hash (sighash) modes. It does not change the actual signing or display logic, so by itself it cannot introduce or fix a security bug. The tests appear to be a companion to a separate code change that made the amount/fee display trustworthy for non-default sighash types.
No immediate action is required for this commit alone. Reviewers should locate the related functional commit that implements the 'trustworthy sign_psbt amount/fee display' change and assess whether the production logic correctly matches the tested predicates, especially for SIGHASH_NONE, SIGHASH_SINGLE, and ANYONECANPAY variants.
Security signals we found
Commit title references trustworthy amount/fee display for non-default sighash, a historically sensitive area in hardware-wallet signing UI
Tests cover sighash commitment semantics (SIGHASH_NONE, SIGHASH_SINGLE, SIGHASH_ANYONECANPAY combinations) that directly affect what outputs a signature commits to
No functional code changes in this commit; only unit-test additions
Evidence from the diff
The diff adds three test functions to unit-tests/test_sighash.c: test_sighash_commit_predicates, test_sighash_commits_provided_outputs, and test_tx_display_mode. They exercise helper functions (sighash_input_set_closed, sighash_output_set_closed, sighash_commits_provided_outputs, decide_tx_display_mode) that determine whether a given sighash type commits to the full input set, output set, and provided outputs, and how the UI should display the transaction (FULL, NET_ONLY, or UNAVAILABLE). The commit message frames this as ‘Trustworthy sign_psbt amount/fee display for non-default sighash: unit-tests’, implying it is test coverage for a prior or parallel functional change.
Changed components
unit-tests/test_sighash.csighash helper predicates (tested, not modified)transaction display-mode decision logic (tested, not modified)Inspect captured patch +102 / −0
diff --git a/unit-tests/test_sighash.c b/unit-tests/test_sighash.c
index cdb6bc1..cf26aef 100644
--- a/unit-tests/test_sighash.c
+++ b/unit-tests/test_sighash.c
@@ -95,9 +95,111 @@ static void test_classify_sighash(void **state) {
}
}
+// ========================================================================
+// Tests for the commit predicates and the display-mode decision
+// ========================================================================
+
+typedef struct {
+ uint32_t sighash_type;
+ bool commits_inputs;
+ bool commits_outputs;
+} commit_case_t;
+
+static const commit_case_t commit_cases[] = {
+ {SIGHASH_DEFAULT, true, true},
+ {SIGHASH_ALL, true, true},
+ {SIGHASH_NONE, true, false},
+ {SIGHASH_SINGLE, true, false},
+ {SIGHASH_ANYONECANPAY | SIGHASH_ALL, false, true},
+ {SIGHASH_ANYONECANPAY | SIGHASH_NONE, false, false},
+ {SIGHASH_ANYONECANPAY | SIGHASH_SINGLE, false, false},
+};
+
+static void test_sighash_commit_predicates(void **state) {
+ (void) state;
+ for (size_t i = 0; i < sizeof(commit_cases) / sizeof(commit_cases[0]); i++) {
+ const commit_case_t *c = &commit_cases[i];
+ bool gi = sighash_input_set_closed(c->sighash_type);
+ bool go = sighash_output_set_closed(c->sighash_type);
+ if (gi != c->commits_inputs || go != c->commits_outputs) {
+ fail_msg("case[%zu]: sighash 0x%02x -> inputs=%d outputs=%d, expected inputs=%d outputs=%d",
+ i, c->sighash_type, gi, go, c->commits_inputs, c->commits_outputs);
+ }
+ }
+}
+
+// commits_provided_outputs: base ALL/DEFAULT always; base SINGLE only with 1 output.
+typedef struct {
+ uint32_t sighash_type;
+ unsigned int n_outputs;
+ bool expected;
+} provided_outputs_case_t;
+
+static const provided_outputs_case_t provided_outputs_cases[] = {
+ // base ALL/DEFAULT: all provided outputs committed, regardless of count (or ANYONECANPAY)
+ {SIGHASH_ALL, 1, true},
+ {SIGHASH_ALL, 3, true},
+ {SIGHASH_DEFAULT, 5, true},
+ {SIGHASH_ANYONECANPAY | SIGHASH_ALL, 4, true},
+ // SINGLE / ACP|SINGLE: committed only when there is exactly one output
+ {SIGHASH_SINGLE, 1, true},
+ {SIGHASH_SINGLE, 2, false},
+ {SIGHASH_SINGLE, 0, false},
+ {SIGHASH_ANYONECANPAY | SIGHASH_SINGLE, 1, true},
+ {SIGHASH_ANYONECANPAY | SIGHASH_SINGLE, 3, false},
+ // NONE: never commits outputs
+ {SIGHASH_NONE, 1, false},
+ {SIGHASH_ANYONECANPAY | SIGHASH_NONE, 1, false},
+};
+
+static void test_sighash_commits_provided_outputs(void **state) {
+ (void) state;
+ for (size_t i = 0; i < sizeof(provided_outputs_cases) / sizeof(provided_outputs_cases[0]);
+ i++) {
+ const provided_outputs_case_t *c = &provided_outputs_cases[i];
+ bool got = sighash_commits_provided_outputs(c->sighash_type, c->n_outputs);
+ if (got != c->expected) {
+ fail_msg("case[%zu]: commits_provided_outputs(0x%02x, %u) = %d, expected %d",
+ i, c->sighash_type, c->n_outputs, got, c->expected);
+ }
+ }
+}
+
+typedef struct {
+ bool fee_trustworthy;
+ bool commits_outputs;
+ tx_display_mode_t expected;
+} mode_case_t;
+
+static const mode_case_t mode_cases[] = {
+ // fee trustworthy + outputs committed -> FULL
+ {true, true, TX_DISPLAY_FULL},
+ // outputs committed, fee not trustworthy -> NET_ONLY
+ {false, true, TX_DISPLAY_NET_ONLY},
+ // outputs not committed -> UNAVAILABLE (regardless of the fee)
+ {true, false, TX_DISPLAY_UNAVAILABLE},
+ {false, false, TX_DISPLAY_UNAVAILABLE},
+};
+
+static void test_tx_display_mode(void **state) {
+ (void) state;
+ for (size_t i = 0; i < sizeof(mode_cases) / sizeof(mode_cases[0]); i++) {
+ const mode_case_t *c = &mode_cases[i];
+ tx_display_mode_t got = decide_tx_display_mode(c->fee_trustworthy, c->commits_outputs);
+ if (got != c->expected) {
+ fail_msg("case[%zu]: decide(fee=%d,out=%d) = %d, expected %d",
+ i, c->fee_trustworthy, c->commits_outputs,
+ (int) got, (int) c->expected);
+ }
+ }
+}
+
int main() {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_classify_sighash),
+ cmocka_unit_test(test_sighash_commit_predicates),
+ cmocka_unit_test(test_sighash_commits_provided_outputs),
+ cmocka_unit_test(test_tx_display_mode),
};
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.