Liquid (taproot): add sign_liquid_tx genesis blockhash support
What changed, and why it matters
This commit changes how Blockstream Jade handles the 'genesis blockhash' when signing Liquid transactions. Previously, the device only accepted the network's built-in genesis hash. Now it lets users provide a custom genesis hash for test networks (like regtest), but blocks attempts to use the real Liquid mainnet genesis hash on test networks to prevent spoofing. The change also applies the same validation to both PSBT/PSET signing and raw transaction signing. It appears to be a hardening/feature addition rather than a fix for an active exploit, but it does close a spoofing path on test networks.
Review the new params_genesis_hash() helper for off-by-one or comparison errors, ensure network_id values cannot be spoofed before validation, and verify that the cleanup path in sign_psbt.c does not leak the genesis hash from a partially signed PSET. No urgent patch is indicated, but firmware using this code should include this commit before allowing custom regtest genesis hashes.
Security signals we found
Adds input validation for caller-supplied genesis blockhash
Prevents mainnet genesis hash from being used on Liquid test networks
Restricts genesis_hash parameter to Liquid networks only
Unifies genesis blockhash handling across PSET and raw transaction signing
Suggested-by external contributor in commit message
Evidence from the diff
The patch introduces a shared helper, params_genesis_hash(), in main/process/sign_utils.c. For Liquid networks, it either uses the default network genesis hash or validates a caller-supplied one. On Liquid testnet/regtest, any 32-byte genesis hash is accepted except the mainnet Liquid genesis hash. On Liquid mainnet, only the mainnet genesis hash is accepted. On Bitcoin, providing a genesis hash is rejected. The helper is now called from sign_psbt.c (PSET path) and sign_tx.c (raw Liquid transaction path), replacing direct comparisons and ensuring consistent behavior. The commit also removes the unconditional injection of the genesis hash before signing and only preserves it in the output PSET when signatures were actually produced.
Changed components
main/process/sign_psbt.cmain/process/sign_tx.cmain/process/sign_utils.cmain/process/sign_utils.hInspect captured patch +82 / −26
diff --git a/main/process/sign_psbt.c b/main/process/sign_psbt.c
index f475f32..0f18dfd 100644
--- a/main/process/sign_psbt.c
+++ b/main/process/sign_psbt.c
@@ -731,19 +731,17 @@ int sign_psbt(jade_process_t* process, CborValue* params, const network_t networ
return CBOR_RPC_BAD_PARAMETERS;
}
const bool for_liquid = is_elements;
- bool has_genesis_blockhash = false;
+ // Liquid: Optional ELIP-0101 genesis blockhash can override test network defaults.
+ // Defers to params_genesis_hash() for validation (only needed for Lisuid/PSET).
+ size_t has_genesis_blockhash = 0;
if (for_liquid) {
- // Liquid: Check ELIP-0101 genesis blockhash
- size_t has_genesis = 0;
- JADE_WALLY_VERIFY(wally_psbt_has_global_genesis_blockhash(psbt, &has_genesis));
- has_genesis_blockhash = has_genesis;
- if (has_genesis_blockhash) {
- uint8_t genesis[SHA256_LEN];
- network_to_genesis_hash(network_id, genesis, sizeof(genesis));
- if (memcmp(psbt->genesis_blockhash, genesis, sizeof(genesis))) {
- *errmsg = "Network/pset genesis mismatch";
- return CBOR_RPC_BAD_PARAMETERS;
- }
+ JADE_WALLY_VERIFY(wally_psbt_has_global_genesis_blockhash(psbt, &has_genesis_blockhash));
+ const uint8_t* psbt_genesis = has_genesis_blockhash ? psbt->genesis_blockhash : NULL;
+ const size_t psbt_genesis_len = has_genesis_blockhash ? sizeof(psbt->genesis_blockhash) : 0;
+ params_genesis_hash(network_id, for_liquid, psbt_genesis, psbt_genesis_len, psbt->genesis_blockhash,
+ sizeof(psbt->genesis_blockhash), errmsg);
+ if (*errmsg) {
+ return CBOR_RPC_BAD_PARAMETERS;
}
}
@@ -1058,11 +1056,6 @@ int sign_psbt(jade_process_t* process, CborValue* params, const network_t networ
display_processing_message_activity();
// Sign our inputs
- if (signing_flags && for_liquid && !has_genesis_blockhash) {
- // Liquid: Provide the ELIP-0101 genesis blockhash when signing
- network_to_genesis_hash(network_id, psbt->genesis_blockhash, sizeof(psbt->genesis_blockhash));
- }
-
JADE_WALLY_VERIFY(wally_psbt_signing_cache_enable(psbt, 0));
for (size_t index = 0; index < psbt->num_inputs; ++index) {
@@ -1116,6 +1109,13 @@ int sign_psbt(jade_process_t* process, CborValue* params, const network_t networ
JADE_ASSERT(!retval);
cleanup:
+ if (!signing_flags && for_liquid && !has_genesis_blockhash) {
+ // Liquid: We didn't sign any inputs and the user didn't provide an
+ // ELIP-0101 genesis blockhash, so remove it from the result PSET.
+ // Note if we did sign, then per ELIP-0101 we keep any added genesis hash.
+ JADE_WALLY_VERIFY(wally_bzero(psbt->genesis_blockhash, sizeof(psbt->genesis_blockhash)));
+ }
+
SENSITIVE_POP(&iter);
free(descriptor);
free(multisig_data);
diff --git a/main/process/sign_tx.c b/main/process/sign_tx.c
index e551913..828dd39 100644
--- a/main/process/sign_tx.c
+++ b/main/process/sign_tx.c
@@ -491,6 +491,20 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
goto cleanup;
}
+ // Liquid: Optional ELIP-0101 genesis blockhash can override test network defaults.
+ // Defers to params_genesis_hash() for validation (incl. disallowing on Bitcoin).
+ uint8_t genesis_hash[SHA256_LEN];
+ {
+ const uint8_t* genesis = NULL;
+ size_t genesis_len = 0;
+ rpc_get_bytes_ptr("genesis_hash", ¶ms, &genesis, &genesis_len);
+ params_genesis_hash(network_id, for_liquid, genesis, genesis_len, genesis_hash, sizeof(genesis_hash), &errmsg);
+ if (errmsg) {
+ jade_process_reject_message(process, CBOR_RPC_BAD_PARAMETERS, errmsg);
+ goto cleanup;
+ }
+ }
+
// Liquid: Gather the (unblinded) output info for user confirmation,
// then validate output and additional_info values
if (for_liquid) {
@@ -812,21 +826,13 @@ static void sign_tx_impl(jade_process_t* process, const bool for_liquid)
// Loop to process any taproot inputs now that we have all input
// amounts and scriptpubkeys
- uint8_t genesis_buff[SHA256_LEN], *genesis = NULL;
- size_t genesis_len = 0;
- if (for_liquid && num_p2tr_to_sign) {
- // Liquid: Fetch the genesis blockhash for taproot hash generation
- genesis = genesis_buff;
- network_to_genesis_hash(network_id, genesis, sizeof(genesis_buff));
- genesis_len = sizeof(genesis_buff);
- }
for (size_t index = 0; num_p2tr_to_sign != 0 && index < tx->num_inputs; ++index) {
input_data_t* const input_data = &signing_data->inputs[index];
if (input_data->sig_type != WALLY_SIGTYPE_SW_V1 || !input_data->path_len) {
// Not signing this input
continue;
}
- if (!wallet_get_tx_input_hash(tx, index, signing_data, NULL, 0, genesis, genesis_len)) {
+ if (!wallet_get_tx_input_hash(tx, index, signing_data, NULL, 0, genesis_hash, sizeof(genesis_hash))) {
// We are using ae-signatures, so we need to load the message to send the error back on
jade_process_load_in_message(process, true);
jade_process_reject_message(process, CBOR_RPC_INTERNAL_ERROR, "Failed to make taproot tx input hash");
diff --git a/main/process/sign_utils.c b/main/process/sign_utils.c
index 8a78fc1..5935d9d 100644
--- a/main/process/sign_utils.c
+++ b/main/process/sign_utils.c
@@ -664,6 +664,50 @@ bool sighash_is_supported(const TxType_t txtype, const uint32_t sig_type, const
return sighash == WALLY_SIGHASH_ALL;
}
+void params_genesis_hash(network_t network_id, const bool for_liquid, const uint8_t* genesis, const size_t genesis_len,
+ uint8_t* genesis_out, const size_t genesis_out_len, const char** errmsg)
+{
+ JADE_ASSERT(genesis_out && genesis_out_len == SHA256_LEN);
+ JADE_INIT_OUT_PPTR(errmsg);
+
+ if (!for_liquid) {
+ // Bitcoin
+ if (genesis) {
+ *errmsg = "genesis_hash only appropriate for liquid networks";
+ }
+ return;
+ }
+ // Liquid
+ if (!genesis) {
+ // Use the consensus genesis blockhash for the network
+ network_to_genesis_hash(network_id, genesis_out, genesis_out_len);
+ return;
+ }
+ // Caller has provided a genesis blockhash to use
+ if (genesis_len != SHA256_LEN) {
+ *errmsg = "Invalid genesis_hash";
+ return;
+ }
+ uint8_t mainnet_genesis[SHA256_LEN];
+ network_to_genesis_hash(NETWORK_LIQUID, mainnet_genesis, sizeof(mainnet_genesis));
+ const bool is_mainnet_genesis = !memcmp(genesis, mainnet_genesis, genesis_len);
+ if (network_id == NETWORK_LIQUID_TESTNET || network_id == NETWORK_LIQUID_REGTEST) {
+ if (is_mainnet_genesis) {
+ // Caller attempting to use the mainnet genesis hash on a test network
+ *errmsg = "Network/pset genesis mismatch";
+ return;
+ }
+ } else if (!is_mainnet_genesis) {
+ // Caller attempting to use a non-mainnet genesis hash on mainnet
+ *errmsg = "Network/pset genesis mismatch";
+ return;
+ }
+ if (genesis_out != genesis) {
+ // Caller provided a new buffer for the result; copy it there
+ memcpy(genesis_out, genesis, genesis_len);
+ }
+}
+
bool show_btc_fee_confirmation_activity(const network_t network_id, const struct wally_tx* tx,
const output_info_t* outinfo, const script_flavour_t aggregate_inputs_scripts_flavour, const uint64_t input_amount,
const uint64_t output_amount)
diff --git a/main/process/sign_utils.h b/main/process/sign_utils.h
index c25df06..6fe6ed5 100644
--- a/main/process/sign_utils.h
+++ b/main/process/sign_utils.h
@@ -51,6 +51,12 @@ WARN_UNUSED_RESULT bool validate_elements_outputs(network_t network_id, const st
// Whether or not the sighash flags for a given tx/signature type is supported
bool sighash_is_supported(TxType_t txtype, uint32_t sig_type, uint32_t sighash, bool for_liquid, bool is_partial);
+// Liquid: populates genesis_out with the given or network-default genesis.
+// Non-Liquid: errors if genesis hash is provided, leaves genesis_out unset.
+// If validation fails, errmsg is set to non-NULL.
+void params_genesis_hash(network_t network_id, bool for_liquid, const uint8_t* genesis, size_t genesis_len,
+ uint8_t* genesis_out, size_t genesis_out_len, const char** errmsg);
+
bool show_btc_fee_confirmation_activity(network_t network_id, const struct wally_tx* tx, const output_info_t* outinfo,
script_flavour_t aggregate_inputs_scripts_flavour, uint64_t input_amount, uint64_t output_amount);
Why this scored 42/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.