tx: Fix BIP118 ANYPREVOUTANYSCRIPT sighash
What changed, and why it matters
This commit fixes a bug in how a special Bitcoin/Elements signature hash flag (BIP118 ANYPREVOUTANYSCRIPT) is processed. Because of the order of checks, the code was accidentally treating ANYPREVOUTANYSCRIPT as if it also needed to include the previous transaction output in the hash, which it should not. This could cause signatures created or verified with this flag to be incorrect, potentially leading to invalid transactions or, in the worst case, a signature that could be reused in unexpected ways.
Review any signatures or transactions generated with BIP118 ANYPREVOUTANYSCRIPT using the unfixed code; they may be invalid or non-standard. Update to the patched version and add test vectors covering APOAS sighash to prevent regression.
Security signals we found
Incorrect cryptographic hash construction for BIP118 ANYPREVOUTANYSCRIPT sighash
Flag implication bug: APOAS implies ANYONECANPAY, causing fall-through to wrong branch
Potential signature malleability or replay risk due to extra prevout data being hashed
Fix is small and targeted: reorder conditional checks
Evidence from the diff
In src/tx_io.c, the bip341_signature_hash function checks flags in the wrong order. ANYPREVOUTANYSCRIPT (sh_anyprevout_anyscript) implies ANYONECANPAY (sh_anyonecanpay), so the first condition if (sh_anyonecanpay || sh_anyprevout) was always true for APOAS, causing the code to fall through and hash the prevout/amount data via txio_hash_input. The fix moves the APOAS check first so it only hashes nSequence, consistent with BIP118. The old APOAS branch is removed.
Changed components
src/tx_io.cbip341_signature_hash functionBIP118 ANYPREVOUTANYSCRIPT sighash handlingInspect captured patch +4 / −3
diff --git a/src/tx_io.c b/src/tx_io.c
index 77b4959..229d86f 100644
--- a/src/tx_io.c
+++ b/src/tx_io.c
@@ -910,7 +910,10 @@ static int bip341_signature_hash(
}
/* Input data */
hash_u8(&io.ctx, (tapleaf_script ? 1 : 0) * 2 + (annex ? 1 : 0)); /* spend_type */
- if (sh_anyonecanpay || sh_anyprevout) {
+ if (sh_anyprevout_anyscript) {
+ // Note that this means sh_anyonecanpay is set so we check this first
+ hash_le32(&io.ctx, tx->inputs[index].sequence); /* nSequence */
+ } else if (sh_anyonecanpay || sh_anyprevout) {
if (sh_anyonecanpay) {
#ifdef BUILD_ELEMENTS
if (is_elements)
@@ -925,8 +928,6 @@ static int bip341_signature_hash(
else
#endif
txio_hash_input(&io, tx, index, scripts, values, NULL, 0, WALLY_SIGTYPE_SW_V1);
- } else if (sh_anyprevout_anyscript) {
- hash_le32(&io.ctx, tx->inputs[index].sequence); /* nSequence */
} else {
hash_le32(&io.ctx, index); /* input_index */
}
Why this scored 66/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.