tx: reject non-corresponding output as per bip341
What changed, and why it matters
This commit fixes a bug in how Bitcoin taproot-style transaction signatures (BIP341) are computed. Under a special signature mode called SIGHASH_SINGLE, the code previously did not check whether the requested transaction output actually exists. BIP341 says such signatures must be rejected. Without this check, a wallet or signer could produce an invalid or ambiguous signature that other software might accept differently, potentially leading to unexpected transaction behavior or security issues in multi-party signing.
Review all call sites of bip341_signature_hash() to ensure callers handle WALLY_EINVAL correctly. Verify that test coverage includes the SIGHASH_SINGLE index-out-of-range case for both mainnet and Elements/Liquid builds. Consider whether any other sighash modes in BIP341 need similar validation.
Security signals we found
BIP341 specification non-compliance
Missing bounds check on SIGHASH_SINGLE output index
Potential signature hash ambiguity
Reported by external researcher
Evidence from the diff
In src/tx_io.c, the bip341_signature_hash() function now rejects the case where output_type == WALLY_SIGHASH_SINGLE and index >= tx->num_outputs, matching BIP341’s rule that the corresponding output must exist. Previously only the input index and annex prefix were validated. The legacy_signature_hash() already had a similar check but returned a defined zero-hash result; BIP341 instead requires returning WALLY_EINVAL. The change is small (two added lines) and aligns implementation with the BIP341 specification.
Changed components
src/tx_io.cbip341_signature_hash()Taproot/Schnorr signature hashingInspect captured patch +4 / −2
diff --git a/src/tx_io.c b/src/tx_io.c
index 229d86f..0256b71 100644
--- a/src/tx_io.c
+++ b/src/tx_io.c
@@ -606,7 +606,7 @@ static int legacy_signature_hash(
!bytes_out || len != SHA256_LEN)
return WALLY_EINVAL;
- if (index >= tx->num_inputs || (sh_single && index >= tx->num_outputs)) {
+ if (index >= tx->num_inputs || (sh_single && index >= tx->num_outputs)) {
memset(bytes_out, 0, SHA256_LEN);
bytes_out[0] = 0x1;
return WALLY_OK;
@@ -832,7 +832,9 @@ static int bip341_signature_hash(
const bool sh_anyprevout_anyscript = bip341_is_input_hash_type(sighash, WALLY_SIGHASH_ANYPREVOUTANYSCRIPT);
cursor_io io;
- if (index >= tx->num_inputs || (annex && *annex != 0x50))
+ if (index >= tx->num_inputs ||
+ (output_type == WALLY_SIGHASH_SINGLE && index >= tx->num_outputs) ||
+ (annex && *annex != 0x50))
return WALLY_EINVAL;
if (is_elements) {
Why this scored 73/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.