Reject non-pushnum opcodes in multisig pattern
What changed, and why it matters
This commit fixes a bug in how the library decides whether a Bitcoin script is a classic 'multisig' script. Before the fix, the final opcode that states how many public keys are required could be something other than a normal number-pushing opcode (for example, a reserved or meaningless opcode), and the code would still treat the script as a valid multisig. After the fix, only opcodes that push a number matching the actual count of public keys are accepted. This could matter to any code that relies on is_multisig() to classify or validate scripts, because a misclassified script could lead to wrong policy decisions, fee calculations, or wallet behavior.
Review any downstream code that calls is_multisig() and confirm it now rejects the malformed scripts it previously accepted. Consider whether other script-pattern helpers (e.g., is_p2pkh, is_p2sh, is_p2wpkh) have similar 'break on any opcode' behavior and audit them for the same class of bug. No emergency action is indicated, but updating to the patched version is prudent for libraries that parse untrusted scripts.
Security signals we found
Script pattern validation bypass
Multisig classification bug
Non-canonical opcode accepted as valid push number
Regression test added for malformed multisig script
Evidence from the diff
In bitcoin/src/blockdata/script/borrowed.rs, the is_multisig() helper previously iterated script instructions, counted PushBytes as pubkeys, and on any Instruction::Op would: if it decoded to a pushnum, check the count and break; otherwise simply break without rejecting. That meant non-pushnum opcodes (e.g. OP_RESERVED, OP_NOP, OP_RETURN) in the pubkey-count slot were treated as valid terminators. The patch changes the logic so any Instruction::Op that is not a pushnum matching the counted pubkeys returns false. A regression test is added using a script whose final opcode is OP_RESERVED (0x50) instead of a pushnum, asserting is_multisig() is now false.
Changed components
bitcoin/src/blockdata/script/borrowed.rsScript::is_multisig() / is_multisig helperInspect captured patch +10 / −7
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index 8bb5890c..5da05cc5 100644
--- a/bitcoin/src/blockdata/script/borrowed.rs
+++ b/bitcoin/src/blockdata/script/borrowed.rs
@@ -340,13 +340,9 @@ internal_macros::define_extension_trait! {
Instruction::PushBytes(_) => {
num_pubkeys += 1;
}
- Instruction::Op(op) => {
- if let Some(pushnum) = op.decode_pushnum() {
- if pushnum != num_pubkeys {
- return false;
- }
- }
- break;
+ Instruction::Op(op) => match op.decode_pushnum() {
+ Some(push_num) if push_num == num_pubkeys => break,
+ _ => return false,
}
}
}
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index abbe0671..cc07afbf 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -585,6 +585,13 @@ fn multisig() {
)
.unwrap()
.is_multisig());
+
+ // Num pubkeys must be a pushnum opcode (OP_1..OP_16).
+ assert!(
+ !ScriptPubKeyBuf::from_hex_no_length_prefix("5221021c4ac2ecebc398e390e07f045aac5cc421f82f0739c1ce724d3d53964dc6537d21023a2e9155e0b62f76737605504819a2b4e5ce20653f6c397d7a178ae42ba702f475ae")
+ .unwrap()
+ .is_multisig()
+ );
}
#[test]
Why this scored 62/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.