Clear pushnum cache on signature operations in sigop count
What changed, and why it matters
This commit fixes a bug in how the library counts 'signature operations' (sigops) in Bitcoin scripts. A small cache that remembers recently pushed numbers was not being cleared after signature-checking opcodes. This could cause a later multi-signature operation to incorrectly reuse a stale number, making the sigop count too low or too high in edge cases. The fix resets the cache whenever a signature opcode is seen, matching Bitcoin Core's consensus behavior.
Review whether this sigop-counting logic is used anywhere in production validation paths. If so, backport the fix and add the regression test. Even if only used for diagnostics or fee estimation, update to match consensus behavior to avoid divergence from Bitcoin Core.
Security signals we found
Consensus-critical code path modified (transaction/script sigop counting)
Mismatch with Bitcoin Core consensus logic explicitly cited in commit message
Stale-cache bug could produce incorrect sigop counts
Incorrect sigop counts can affect transaction validity, fee estimation, and block validation limits
Evidence from the diff
In Script::count_sigops, a pushnum_cache stores the last small-integer push to optimize counting of OP_CHECKMULTISIG operations. Previously, the cache was only updated by opcode.decode_pushnum() in the default branch and was not cleared on OP_CHECKSIG/OP_CHECKSIGVERIFY. As a result, a CHECKMULTISIG following a CHECKSIG could inherit a stale pushnum_cache value even though no new push preceded it. The patch sets pushnum_cache = None after both single-signature and multi-signature opcodes, aligning with Bitcoin Core’s consensus sigop counting. A regression test demonstrates the corrected count of 21 for OP_1 OP_CHECKSIG OP_CHECKMULTISIG (1 + 20).
Changed components
bitcoin/src/blockdata/script/borrowed.rsScript::count_sigops / sigop_count implementationInspect captured patch +12 / −0
diff --git a/bitcoin/src/blockdata/script/borrowed.rs b/bitcoin/src/blockdata/script/borrowed.rs
index 8bb5890c..e67eac80 100644
--- a/bitcoin/src/blockdata/script/borrowed.rs
+++ b/bitcoin/src/blockdata/script/borrowed.rs
@@ -476,6 +476,7 @@ internal_macros::define_extension_trait! {
// p2pk, p2pkh
OP_CHECKSIG | OP_CHECKSIGVERIFY => {
n += 1;
+ pushnum_cache = None;
}
OP_CHECKMULTISIG | OP_CHECKMULTISIGVERIFY => {
match (accurate, pushnum_cache) {
@@ -489,6 +490,7 @@ internal_macros::define_extension_trait! {
n += 20;
}
}
+ pushnum_cache = None;
}
_ => {
pushnum_cache = opcode.decode_pushnum();
diff --git a/bitcoin/src/blockdata/script/tests.rs b/bitcoin/src/blockdata/script/tests.rs
index abbe0671..7d90ffa2 100644
--- a/bitcoin/src/blockdata/script/tests.rs
+++ b/bitcoin/src/blockdata/script/tests.rs
@@ -854,6 +854,16 @@ fn default_dust_value() {
#[test]
fn script_get_sigop_count() {
+ // 1 (CHECKSIG) + 20 (CHECKMULTISIG, lastOpcode = CHECKSIG)
+ assert_eq!(
+ Script::builder()
+ .push_opcode(OP_1)
+ .push_opcode(OP_CHECKSIG)
+ .push_opcode(OP_CHECKMULTISIG)
+ .into_script()
+ .count_sigops(),
+ 21
+ );
assert_eq!(
Script::builder()
.push_opcode(OP_DUP)
Why this scored 60/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.