process: limit array sizes in sign_tx, register_multisig and sign_bip85_digests
What changed, and why it matters
This commit adds size limits to several message-handling functions in the Blockstream Jade hardware wallet firmware. It caps how many asset records, transaction inputs/outputs, multisig signers, and BIP85 RSA digests a single RPC message can contain. The change appears to be a hardening patch: without these limits, an attacker or malformed host application could send an extremely large array and potentially exhaust device memory, cause long processing delays, or trigger memory corruption during signing and multisig registration. The patch also makes the array-length check happen earlier and more consistently across the code.
Treat this commit as a security hardening fix and include it in the next firmware release. Review whether any other RPC endpoints that accept arrays still lack similar bounds, and consider adding a global message-size budget or heap-canary checks to detect future memory-pressure issues.
Security signals we found
Adds explicit upper bounds on attacker-controllable array sizes
Rejects zero-input/zero-output transactions before further processing
Moves BIP85 digest count validation before memory allocation
Centralizes array-length extraction in rpc_get_array()
Adds regression tests for oversized arrays and empty transactions
Evidence from the diff
The patch centralizes and strengthens array-length validation by modifying rpc_get_array() to always return the array length, then enforcing upper bounds in sign_tx, register_multisig, sign_bip85_digest, and asset parsing. New constants include MAX_TX_INPUTS (256), MAX_TX_OUTPUTS (64), and MAX_ASSET_INFO_ELEMS (64). Existing constants such as MAX_ALLOWED_SIGNERS are now enforced earlier. The BIP85 digest limit is moved before allocation and keyed to RSA key size. Additional checks reject transactions with zero inputs or outputs. Several helper functions were refactored to accept a precomputed item count, removing duplicate cbor_value_get_array_length calls and ensuring the loop bound cannot differ from the validated count.
Changed components
main/process/sign_tx.cmain/process/sign_utils.cmain/process/register_multisig.cmain/process/sign_bip85_digest.cmain/assets.cmain/multisig.cmain/utils/cbor_rpc.cInspect captured patch +251 / −81
### main/assets.c
@@ -14,6 +14,9 @@
#include <wally_elements.h>
#include <wally_transaction.h>
+// Maximum number of asset info records accepted in a single message
+#define MAX_ASSET_INFO_ELEMS 64u
+
#define ASSET_CONTRACT_BUFFER_LEN 768
// Compute the asset-id given the contract hash and the issuance prevout details
@@ -68,30 +71,24 @@ static bool get_asset_contract_hash(const CborValue* contract, uint8_t* contract
// Asset data is optional - but if present it must be correct/valid
bool assets_get_allocate(const char* field, const CborValue* value, asset_info_t** data, size_t* written)
{
- JADE_ASSERT(field);
- JADE_ASSERT(value);
+ JADE_ASSERT(field && value);
JADE_INIT_OUT_PPTR(data);
JADE_INIT_OUT_SIZE(written);
CborValue result;
- if (!rpc_get_array(field, value, &result)) {
+ size_t num_array_items = 0;
+ if (!rpc_get_array(field, value, &result, &num_array_items) || !num_array_items) {
// No asset data present is not an error
return true;
}
- size_t num_array_items = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_array_items);
- if (cberr != CborNoError) {
+ if (num_array_items > MAX_ASSET_INFO_ELEMS) {
+ JADE_LOGE("Too many asset data elements in %s", field);
return false;
}
- if (num_array_items == 0) {
- // No asset data present is not an error
- return true;
- }
-
CborValue arrayItem;
- cberr = cbor_value_enter_container(&result, &arrayItem);
+ CborError cberr = cbor_value_enter_container(&result, &arrayItem);
if (cberr != CborNoError || !cbor_value_is_valid(&arrayItem)) {
return false;
}
### main/multisig.c
@@ -400,28 +400,27 @@ bool multisig_load_from_storage(const char* multisig_name, multisig_data_t* outp
return true;
}
-bool multisig_validate_paths(
- const bool is_change, CborValue* all_signer_paths, bool* all_paths_as_expected, bool* final_elements_consistent)
+bool multisig_validate_paths(const bool is_change, CborValue* all_signer_paths, const size_t num_signer_paths,
+ bool* all_paths_as_expected, bool* final_elements_consistent)
{
JADE_ASSERT(all_signer_paths);
JADE_ASSERT(all_paths_as_expected);
- bool seen_unusual_path = false;
- bool seen_final_element_mismatch = false;
-
- size_t num_array_items = 0;
- if (cbor_value_get_array_length(all_signer_paths, &num_array_items) != CborNoError || num_array_items == 0) {
+ if (num_signer_paths == 0) {
return false;
}
+ bool seen_unusual_path = false;
+ bool seen_final_element_mismatch = false;
+
uint32_t expected_final_path_element;
uint32_t path[MAX_PATH_LEN];
const size_t max_path_len = sizeof(path) / sizeof(path[0]);
CborValue arrayItem;
CborError cberr = cbor_value_enter_container(all_signer_paths, &arrayItem);
JADE_ASSERT(cberr == CborNoError);
- for (size_t i = 0; i < num_array_items; ++i) {
+ for (size_t i = 0; i < num_signer_paths; ++i) {
JADE_ASSERT(!cbor_value_at_end(&arrayItem));
size_t path_len = 0;
### main/multisig.h
@@ -62,8 +62,8 @@ WARN_UNUSED_RESULT bool multisig_data_from_bytes(const uint8_t* bytes, size_t by
WARN_UNUSED_RESULT bool multisig_load_from_storage(const char* multisig_name, multisig_data_t* output,
signer_t* signer_details, size_t signer_details_len, size_t* written, const char** errmsg);
-WARN_UNUSED_RESULT bool multisig_validate_paths(
- const bool is_change, CborValue* all_signer_paths, bool* all_paths_as_expected, bool* final_elements_consistent);
+WARN_UNUSED_RESULT bool multisig_validate_paths(bool is_change, CborValue* all_signer_paths, size_t num_signer_paths,
+ bool* all_paths_as_expected, bool* final_elements_consistent);
WARN_UNUSED_RESULT bool multisig_get_pubkeys(const uint8_t* xpubs, size_t num_xpubs, CborValue* all_signer_paths,
uint8_t* pubkeys, size_t pubkeys_len, size_t* written);
### main/process/process_utils.c
@@ -234,10 +234,12 @@ bool params_multisig_pubkeys(const bool is_change, CborValue* params, multisig_d
// Validate paths
CborValue all_signer_paths;
+ size_t num_signer_paths = 0;
bool all_paths_as_expected;
bool final_elements_consistent;
- if (!rpc_get_array("paths", params, &all_signer_paths)
- || !multisig_validate_paths(is_change, &all_signer_paths, &all_paths_as_expected, &final_elements_consistent)) {
+ if (!rpc_get_array("paths", params, &all_signer_paths, &num_signer_paths)
+ || !multisig_validate_paths(
+ is_change, &all_signer_paths, num_signer_paths, &all_paths_as_expected, &final_elements_consistent)) {
*errmsg = "Failed to extract signer paths from parameters";
return false;
}
@@ -246,7 +248,8 @@ bool params_multisig_pubkeys(const bool is_change, CborValue* params, multisig_d
bool flipped_change_element = false;
if (!all_paths_as_expected) {
bool unused;
- if (!multisig_validate_paths(!is_change, &all_signer_paths, &flipped_change_element, &unused)) {
+ if (!multisig_validate_paths(
+ !is_change, &all_signer_paths, num_signer_paths, &flipped_change_element, &unused)) {
*errmsg = "Expected a valid change or non-change signer path";
return false;
}
### main/process/register_multisig.c
@@ -551,24 +551,19 @@ int register_multisig_file(const char* multisig_file, const size_t multisig_file
// Helper to collect signers' details from input cbor message
static void get_signers_allocate(const char* field, const CborValue* value, signer_t** data, size_t* written)
{
- JADE_ASSERT(field);
- JADE_ASSERT(value);
+ JADE_ASSERT(field && value);
JADE_INIT_OUT_PPTR(data);
JADE_INIT_OUT_SIZE(written);
CborValue result;
- if (!rpc_get_array(field, value, &result)) {
- return;
- }
-
size_t num_array_items = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_array_items);
- if (cberr != CborNoError || !num_array_items) {
+ if (!rpc_get_array(field, value, &result, &num_array_items) || !num_array_items
+ || num_array_items > MAX_ALLOWED_SIGNERS) {
return;
}
CborValue arrayItem;
- cberr = cbor_value_enter_container(&result, &arrayItem);
+ CborError cberr = cbor_value_enter_container(&result, &arrayItem);
if (cberr != CborNoError || !cbor_value_is_valid(&arrayItem)) {
return;
}
### main/process/sign_bip85_digest.c
@@ -36,26 +36,29 @@ static void reply_signatures(const void* ctx, CborEncoder* container)
JADE_ASSERT(cberr == CborNoError);
}
-static void get_digests_allocate(
- const char* field, const CborValue* value, rsa_signing_digest_t** data, size_t* written)
+static void get_digests_allocate(const char* field, const CborValue* value, const uint32_t key_bits,
+ rsa_signing_digest_t** data, size_t* written, const char** errmsg)
{
JADE_ASSERT(field && value);
JADE_INIT_OUT_PPTR(data);
JADE_INIT_OUT_SIZE(written);
+ *errmsg = "Failed to extract digests from parameters";
+
CborValue result;
- if (!rpc_get_array(field, value, &result)) {
+ size_t num_array_items = 0;
+ if (!rpc_get_array(field, value, &result, &num_array_items) || !num_array_items) {
return;
}
- size_t num_array_items = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_array_items);
- if (cberr != CborNoError || !num_array_items) {
+ const size_t max_digests = key_bits <= 2048 ? 8 : key_bits < 4096 ? 6 : 4;
+ if (num_array_items > max_digests) {
+ *errmsg = "Unsupported number of digests";
return;
}
CborValue arrayItem;
- cberr = cbor_value_enter_container(&result, &arrayItem);
+ CborError cberr = cbor_value_enter_container(&result, &arrayItem);
if (cberr != CborNoError || !cbor_value_is_valid(&arrayItem)) {
return;
}
@@ -82,6 +85,7 @@ static void get_digests_allocate(
return;
}
+ *errmsg = NULL;
*written = num_array_items;
*data = digests;
}
@@ -108,22 +112,16 @@ void sign_bip85_digests_process(void* process_ptr)
// Copy digest data
rsa_signing_digest_t* digests = NULL;
size_t num_digests = 0;
- get_digests_allocate("digests", ¶ms, &digests, &num_digests);
+ get_digests_allocate("digests", ¶ms, key_bits, &digests, &num_digests, &errmsg);
- if (num_digests == 0) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Failed to extract digests from parameters");
+ if (errmsg) {
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
goto cleanup;
}
JADE_ASSERT(digests);
jade_process_free_on_exit(process, digests);
- const size_t max_digests = key_bits <= 2048 ? 8 : key_bits < 4096 ? 6 : 4;
- if (num_digests > max_digests) {
- jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Unsupported number of digests");
- goto cleanup;
- }
-
// User to confirm signing
int ret;
char buf1[20], buf2[24], buf3[32];
### main/process/sign_tx.c
@@ -87,11 +87,12 @@ static bool params_signing_outputs(jade_process_t* process, const CborValue* par
JADE_ASSERT(process);
JADE_ASSERT(params);
JADE_ASSERT(network_id != NETWORK_NONE);
- JADE_ASSERT(tx);
+ JADE_ASSERT(tx && tx->num_outputs);
JADE_INIT_OUT_PPTR(output_info);
CborValue wallet_outputs;
- const bool have_outputs = rpc_get_array("change", params, &wallet_outputs);
+ size_t num_wallet_outputs = 0;
+ const bool have_outputs = rpc_get_array("change", params, &wallet_outputs, &num_wallet_outputs);
// For Bitcoin, we only need the output info if the caller gave it.
// For Liquid, we always need output_info to 'unblind' confidential txs.
if (have_outputs || for_liquid) {
@@ -107,10 +108,7 @@ static bool params_signing_outputs(jade_process_t* process, const CborValue* par
multisig_data_t* multisig_data = NULL;
descriptor_data_t* descriptor = NULL;
- size_t num_array_items = 0;
- if (!cbor_value_is_array(&wallet_outputs)
- || cbor_value_get_array_length(&wallet_outputs, &num_array_items) != CborNoError
- || num_array_items != tx->num_outputs) {
+ if (num_wallet_outputs != tx->num_outputs) {
errmsg = "Unexpected number of output entries for transaction";
goto cleanup;
}
@@ -473,8 +471,10 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, "Invalid asset info passed");
goto cleanup;
}
- jade_process_free_on_exit(process, assets);
- JADE_LOGI("Read %d assets from message", num_assets);
+ if (assets) {
+ jade_process_free_on_exit(process, assets);
+ JADE_LOGI("Read %d assets from message", num_assets);
+ }
}
const char* errmsg = NULL;
### main/process/sign_utils.c
@@ -30,6 +30,22 @@ bool params_txn_validate(const network_t network_id, const bool for_liquid, cons
return false;
}
+ // Reject empty transactions. A tx must have at least one input and one
+ // output - this also guarantees a non-zero output count for later processing.
+ if (!tx->num_inputs || !tx->num_outputs) {
+ *errmsg = "Transaction has no inputs or outputs";
+ return false;
+ }
+
+ if (tx->num_inputs > MAX_TX_INPUTS) {
+ *errmsg = "Too many transaction inputs";
+ return false;
+ }
+ if (tx->num_outputs > MAX_TX_OUTPUTS) {
+ *errmsg = "Too many transaction outputs";
+ return false;
+ }
+
if (!for_liquid) {
return true; // Bitcoin: No further checks needed
}
@@ -106,29 +122,29 @@ static bool rpc_get_txtype(jade_process_t* process, CborValue* value, TxType_t*
return false; // Unknown tx_type
}
-static void rpc_get_asset_summary(
- jade_process_t* process, const char* field, const CborValue* value, asset_summary_t** data, size_t* written)
+static bool rpc_get_asset_summary(jade_process_t* process, const char* field, const CborValue* value,
+ const size_t max_items, asset_summary_t** data, size_t* written)
{
- JADE_ASSERT(field);
- JADE_ASSERT(value);
+ JADE_ASSERT(field && value);
JADE_INIT_OUT_PPTR(data);
JADE_INIT_OUT_SIZE(written);
CborValue result;
- if (!rpc_get_array(field, value, &result)) {
- return;
+ size_t num_array_items = 0;
+ if (!rpc_get_array(field, value, &result, &num_array_items) || !num_array_items) {
+ // Summary data is optional
+ return true;
}
- size_t num_array_items = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_array_items);
- if (cberr != CborNoError || !num_array_items) {
- return;
+ if (num_array_items > max_items) {
+ JADE_LOGE("Too many asset summary records in message: %zu (max %zu)", num_array_items, max_items);
+ return false;
}
CborValue arrayItem;
- cberr = cbor_value_enter_container(&result, &arrayItem);
+ CborError cberr = cbor_value_enter_container(&result, &arrayItem);
if (cberr != CborNoError || !cbor_value_is_valid(&arrayItem)) {
- return;
+ return true;
}
asset_summary_t* const sums = JADE_CALLOC(num_array_items, sizeof(asset_summary_t));
@@ -141,7 +157,7 @@ static void rpc_get_asset_summary(
if (!cbor_value_is_map(&arrayItem)
|| !rpc_get_n_bytes("asset_id", &arrayItem, sizeof(item->asset_id), item->asset_id)
|| !rpc_get_uint64("satoshi", &arrayItem, &item->value)) {
- return;
+ return true;
}
cberr = cbor_value_advance(&arrayItem);
@@ -153,6 +169,7 @@ static void rpc_get_asset_summary(
*written = num_array_items;
*data = sums;
}
+ return true;
}
static bool validate_additional_info(const struct wally_tx* tx, const TxType_t txtype, const bool is_partial,
@@ -219,8 +236,12 @@ bool params_additional_info(jade_process_t* process, CborValue* params, const st
}
// input/output summaries required for some complex txn types, eg. swaps
- rpc_get_asset_summary(process, "wallet_input_summary", &additional_info, in_sums, num_in_sums);
- rpc_get_asset_summary(process, "wallet_output_summary", &additional_info, out_sums, num_out_sums);
+ if (!rpc_get_asset_summary(process, "wallet_input_summary", &additional_info, tx->num_inputs, in_sums, num_in_sums)
+ || !rpc_get_asset_summary(
+ process, "wallet_output_summary", &additional_info, tx->num_outputs, out_sums, num_out_sums)) {
+ *errmsg = "Invalid number of asset summaries";
+ return false;
+ }
// 'partial' flag (defaults to false, initially also defaulted above)
*is_partial = rpc_get_bool_or("is_partial", &additional_info, false);
@@ -469,22 +490,21 @@ bool params_trusted_commitments(
const char* errmsg = NULL;
CborValue result;
- if (!rpc_get_array("trusted_commitments", params, &result)) {
+ size_t num_outputs = 0;
+ if (!rpc_get_array("trusted_commitments", params, &result, &num_outputs)) {
errmsg = "Failed to extract trusted commitments from parameters";
goto cleanup;
}
// Expect one commitment element in the array for each output.
// (Can be null/zero's for unblinded outputs.)
- size_t num_outputs = 0;
- CborError cberr = cbor_value_get_array_length(&result, &num_outputs);
- if (cberr != CborNoError || !num_outputs || num_outputs != tx->num_outputs) {
+ if (!num_outputs || num_outputs != tx->num_outputs) {
errmsg = "Unexpected number of trusted commitments for transaction";
goto cleanup;
}
CborValue arrayItem;
- cberr = cbor_value_enter_container(&result, &arrayItem);
+ CborError cberr = cbor_value_enter_container(&result, &arrayItem);
if (cberr != CborNoError || !cbor_value_is_valid(&arrayItem)) {
errmsg = "Invalid trusted commitments for transaction";
goto cleanup;
### main/process/sign_utils.h
@@ -20,6 +20,10 @@ typedef struct _asset_summary {
uint64_t validated_value;
} asset_summary_t;
+// Maximum number of inputs/outputs accepted
+#define MAX_TX_INPUTS 256
+#define MAX_TX_OUTPUTS 64
+
WARN_UNUSED_RESULT bool params_txn_validate(network_t network_id, bool for_liquid, const struct wally_tx* const tx,
uint64_t* explicit_fee, const char** errmsg);
### main/utils/cbor_rpc.c
@@ -489,12 +489,16 @@ bool rpc_has_field_data(const char* field, const CborValue* value)
return rpc_get_data(field, value, &result) && !cbor_value_is_null(&result);
}
-bool rpc_get_array(const char* field, const CborValue* value, CborValue* result)
+bool rpc_get_array(const char* field, const CborValue* value, CborValue* result, size_t* num_array_items)
{
JADE_ASSERT(field);
JADE_ASSERT(value);
JADE_ASSERT(result);
- return rpc_get_data(field, value, result) && cbor_value_is_array(result);
+ JADE_INIT_OUT_SIZE(num_array_items);
+ if (!rpc_get_data(field, value, result) || !cbor_value_is_array(result)) {
+ return false;
+ }
+ return cbor_value_get_array_length(result, num_array_items) == CborNoError;
}
bool rpc_get_map(const char* field, const CborValue* value, CborValue* result)
### main/utils/cbor_rpc.h
@@ -66,7 +66,8 @@ WARN_UNUSED_RESULT bool rpc_get_bip32_path(
WARN_UNUSED_RESULT bool rpc_get_bip32_path_from_value(
CborValue* value, uint32_t* path_ptr, size_t max_path_len, size_t* written);
-WARN_UNUSED_RESULT bool rpc_get_array(const char* field, const CborValue* value, CborValue* result);
+WARN_UNUSED_RESULT bool rpc_get_array(
+ const char* field, const CborValue* value, CborValue* result, size_t* num_array_items);
WARN_UNUSED_RESULT bool rpc_get_map(const char* field, const CborValue* value, CborValue* result);
// Build response objects
### tests/rpc/test_bip85.py
@@ -99,3 +99,15 @@ def test_bip85_rsa_signing(jade, test_case):
assert len(digests) == len(expected)
sigs = jade.sign_bip85_digests('RSA', keylen, index, digests)
assert sigs == expected
+
+
+@pytest.mark.parametrize('key_bits, num_digests', [(2048, 9), (4096, 5)])
+def test_sign_bip85_digests_too_many(jade, key_bits, num_digests):
+ # 2048-bit keys support at most 8 digests, 4096-bit keys at most 4; send
+ # one more than the maximum for each size
+ try:
+ jade.sign_bip85_digests('RSA', key_bits, 0, [bytes(32)] * num_digests)
+ assert False, 'Expected error for oversized digests array'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Unsupported number of digests', e.message
### tests/rpc/test_multisig.py
@@ -374,3 +374,15 @@ def test_generic_multisig_ss_signer(jade, mnemonic, test_case):
# the same addresses as it did previously (for the other signatory)
_check_multisig_registration(jade, test_case)
pass
+
+
+def test_register_multisig_too_many_signers(jade):
+ """More signers than MAX_ALLOWED_SIGNERS must be rejected."""
+ try:
+ MAX_ALLOWED_SIGNERS = 15
+ signers = [{}] * (MAX_ALLOWED_SIGNERS + 1)
+ jade.register_multisig('testnet', 'msig_toomany', 'sh(multi(k))', False, 1, signers)
+ assert False, 'Expected error for oversized signers array'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Failed to extract valid co-signers from parameters', e.message
### tests/rpc/test_sign_tx.py
@@ -246,3 +246,128 @@ def test_sign_tx_liquid_singlesig(jade, mnemonic, test_case):
# @with_test_cases('tests/rpc/data/sign_tx/bad_liquid_ss_tx_*.json')
# def test_sign_tx_bad_liquid_singlesig(jade, mnemonic, test_case):
# _test_sign_tx(jade, test_case)
+
+
+@with_test_cases('tests/rpc/data/sign_tx/liquid_tx_asset_lowr.json')
+def test_sign_liquid_tx_too_many_assets(jade, test_case):
+ """More asset_info records than MAX_ASSET_INFO_ELEMS must be rejected"""
+ if not get_jade_config().has_psram:
+ pytest.skip('Oversized tx test requires PSRAM (larger inbound message buffer)')
+
+ inputdata = test_case['input']
+ assert 'liquid' in inputdata['network']
+ assert inputdata.get('asset_info'), 'test case must provide a valid asset record'
+
+ # Duplicate one valid record to exceed MAX_ASSET_INFO_ELEMS (64).
+ asset_info = [dict(inputdata['asset_info'][0]) for _ in range(64 + 1)]
+
+ try:
+ jade.sign_liquid_tx(inputdata['network'],
+ inputdata['txn'],
+ inputdata['inputs'],
+ inputdata['trusted_commitments'],
+ inputdata['change'],
+ inputdata.get('use_ae_signatures', False),
+ asset_info,
+ inputdata.get('additional_info'))
+ assert False, 'Expected error for oversized asset_info array'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Invalid asset info passed', e.message
+
+
+@with_test_cases('tests/rpc/data/sign_tx/liquid_tx_swap_maker_send_lbtc_ae.json')
+def test_sign_liquid_tx_too_many_asset_summaries(jade, test_case):
+ """More wallet input/output summary records than the tx bounds must be rejected."""
+ inputdata = test_case['input']
+ assert 'liquid' in inputdata['network']
+
+ # This swap has a single input and output, so each summary must contain at
+ # most one record. Check both the input and output summaries.
+ for field in ('wallet_input_summary', 'wallet_output_summary'):
+ summary = inputdata['additional_info'][field]
+ assert len(summary) == 1, (field, summary)
+
+ # Duplicate one valid record so the array length exceeds the tx bound.
+ inputdata['additional_info'][field] = [dict(summary[0]), dict(summary[0])]
+ try:
+ jade.sign_liquid_tx(inputdata['network'],
+ inputdata['txn'],
+ inputdata['inputs'],
+ inputdata['trusted_commitments'],
+ inputdata['change'],
+ inputdata.get('use_ae_signatures', False),
+ inputdata.get('asset_info'),
+ inputdata.get('additional_info'))
+ assert False, f'Expected error for oversized {field} array'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Invalid number of asset summaries', e.message
+ finally:
+ # Restore the original single-record summary for the next iteration
+ inputdata['additional_info'][field] = summary
+
+
+def _varint(n):
+ """Encode an integer as a Bitcoin compact-size (varint) integer."""
+ if n < 0xfd:
+ return bytes([n])
+ if n <= 0xffff:
+ return b'\xfd' + n.to_bytes(2, 'little')
+ if n <= 0xffffffff:
+ return b'\xfe' + n.to_bytes(4, 'little')
+ return b'\xff' + n.to_bytes(8, 'little')
+
+
+def _make_tx_bytes(num_inputs, num_outputs):
+ """Build a minimal raw Bitcoin transaction (version 2, no witness) with the
+ given number of inputs/outputs, to exercise the tx input/output caps."""
+ tx = (2).to_bytes(4, 'little') # version
+ tx += _varint(num_inputs) # input count
+ for _ in range(num_inputs):
+ tx += bytes(32) # prevout txid
+ tx += (0).to_bytes(4, 'little') # prevout index
+ tx += b'\x00' # empty script
+ tx += (0xffffffff).to_bytes(4, 'little') # sequence
+ tx += _varint(num_outputs) # output count
+ for _ in range(num_outputs):
+ tx += (1000).to_bytes(8, 'little') # value
+ tx += b'\x01' + b'\x51' # script: OP_TRUE
+ tx += (0).to_bytes(4, 'little') # locktime
+ return tx
+
+
+def test_sign_tx_too_many_inputs(jade):
+ """A tx with more than MAX_TX_INPUTS inputs must be rejected."""
+ MAX_TX_INPUTS = 256
+ txn = _make_tx_bytes(257, 1)
+ try:
+ txinputs = [None] * (MAX_TX_INPUTS + 1)
+ jade.sign_tx('testnet', txn, txinputs, None, use_ae_signatures=False, use_legacy=False)
+ assert False, 'Expected error for oversized tx input count'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Too many transaction inputs', e.message
+
+
+def test_sign_tx_too_many_outputs(jade):
+ """A tx with more than MAX_TX_OUTPUTS outputs must be rejected."""
+ MAX_TX_OUTPUTS = 64
+ txn = _make_tx_bytes(1, MAX_TX_OUTPUTS + 1)
+ try:
+ jade.sign_tx('testnet', txn, [None], None, use_ae_signatures=False, use_legacy=False)
+ assert False, 'Expected error for oversized tx output count'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Too many transaction outputs', e.message
+
+
+def test_sign_tx_no_outputs(jade):
+ """A tx with no outputs must be rejected"""
+ txn = _make_tx_bytes(1, 0)
+ try:
+ jade.sign_tx('testnet', txn, [None], None, use_ae_signatures=False, use_legacy=False)
+ assert False, 'Expected error for tx with no outputs'
+ except JadeError as e:
+ assert e.code == JadeError.BAD_PARAMETERS, e
+ assert e.message == 'Transaction has no inputs or outputs', e.messageWhy this scored 63/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.