Skip to main content

blvm_consensus/
sigop.rs

1//! Signature operation counting functions
2//!
3//! Implements consensus's sigop counting for block validation.
4//! Sigops are counted to enforce MAX_BLOCK_SIGOPS_COST limit (80,000).
5//!
6//! Reference: consensus `tx_verify.cpp` and `script.cpp`
7
8use crate::error::Result;
9use crate::opcodes::*;
10use crate::segwit::Witness;
11use crate::types::*;
12use crate::utxo_overlay::UtxoLookup;
13use blvm_spec_lock::spec_locked;
14
15/// Maximum number of public keys in a multisig (for sigop counting)
16/// This is used when we can't accurately determine the number from the script
17const MAX_PUBKEYS_PER_MULTISIG: u32 = 20;
18
19/// Witness scale factor for sigop cost calculation
20/// Legacy sigops count as 4x their actual number in sigop cost
21const WITNESS_SCALE_FACTOR: u64 = 4;
22
23/// Count sigops in a script (legacy counting)
24///
25/// Counts OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY.
26/// Matches consensus's CScript::GetSigOpCount(bool fAccurate).
27///
28/// Uses GetOp-style iteration: each call to the loop body reads one opcode and
29/// advances past any associated push data, exactly like consensus's GetOp().
30///
31/// # Arguments
32/// * `script` - Script to count sigops in
33/// * `accurate` - If true, use OP_1-OP_16 before OP_CHECKMULTISIG to determine key count
34///
35/// # Returns
36/// Number of sigops in the script
37#[spec_locked("5.2.2")]
38pub fn count_sigops_in_script(script: &ByteString, accurate: bool) -> u32 {
39    let mut count = 0u32;
40    let mut last_opcode: Option<u8> = None;
41    let mut i = 0;
42
43    while i < script.len() {
44        let opcode = script[i];
45
46        // Skip past push data (matches consensus's GetOp)
47        if opcode > 0 && opcode < OP_PUSHDATA1 {
48            // Direct push: opcode IS the length (1-75 bytes)
49            let len = opcode as usize;
50            last_opcode = Some(opcode);
51            i += 1 + len;
52            continue;
53        } else if opcode == OP_PUSHDATA1 {
54            // OP_PUSHDATA1: next byte is length
55            if i + 1 >= script.len() {
56                break;
57            }
58            let len = script[i + 1] as usize;
59            last_opcode = Some(opcode);
60            i += 2 + len;
61            continue;
62        } else if opcode == OP_PUSHDATA2 {
63            // OP_PUSHDATA2: next 2 bytes (little-endian) are length
64            if i + 2 >= script.len() {
65                break;
66            }
67            let len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
68            last_opcode = Some(opcode);
69            i += 3 + len;
70            continue;
71        } else if opcode == OP_PUSHDATA4 {
72            // OP_PUSHDATA4: next 4 bytes (little-endian) are length
73            if i + 4 >= script.len() {
74                break;
75            }
76            let len =
77                u32::from_le_bytes([script[i + 1], script[i + 2], script[i + 3], script[i + 4]])
78                    as usize;
79            last_opcode = Some(opcode);
80            i += 5 + len;
81            continue;
82        }
83
84        // OP_CHECKSIG and OP_CHECKSIGVERIFY count as 1 sigop each
85        if opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY {
86            count = count.saturating_add(1);
87        }
88        // OP_CHECKMULTISIG and OP_CHECKMULTISIGVERIFY count as multiple sigops
89        else if opcode == OP_CHECKMULTISIG || opcode == OP_CHECKMULTISIGVERIFY {
90            if accurate {
91                // If accurate mode and previous opcode is OP_1-OP_16, use that number
92                if let Some(prev_op) = last_opcode {
93                    if (OP_1..=OP_16).contains(&prev_op) {
94                        // OP_1 = 0x51, OP_16 = 0x60
95                        // Decode: OP_N = N - 0x50
96                        let n = (prev_op - OP_1 + 1) as u32;
97                        count = count.saturating_add(n);
98                    } else {
99                        count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
100                    }
101                } else {
102                    count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
103                }
104            } else {
105                // Not accurate: assume maximum
106                count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
107            }
108        }
109
110        last_opcode = Some(opcode);
111        i += 1;
112    }
113
114    count
115}
116
117/// Count sigops in a tapscript per BIP 342 (`CountTapscriptSigOps`).
118///
119/// Used for **per-tapscript validation weight** during script execution. This must **not** be folded
120/// into block `MAX_BLOCK_SIGOPS_COST`: Bitcoin Core `WitnessSigOps` only counts witness **v0**; v1
121/// (Taproot) contributes **0** to that total.
122#[spec_locked("11.2.8")]
123pub fn count_tapscript_sigops(script: &ByteString) -> u32 {
124    let mut count = 0u32;
125    let mut i = 0;
126
127    while i < script.len() {
128        let opcode = script[i];
129
130        if opcode > 0 && opcode < OP_PUSHDATA1 {
131            let len = opcode as usize;
132            i += 1 + len;
133            continue;
134        } else if opcode == OP_PUSHDATA1 {
135            if i + 1 >= script.len() {
136                break;
137            }
138            let len = script[i + 1] as usize;
139            i += 2 + len;
140            continue;
141        } else if opcode == OP_PUSHDATA2 {
142            if i + 2 >= script.len() {
143                break;
144            }
145            let len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
146            i += 3 + len;
147            continue;
148        } else if opcode == OP_PUSHDATA4 {
149            if i + 4 >= script.len() {
150                break;
151            }
152            let len =
153                u32::from_le_bytes([script[i + 1], script[i + 2], script[i + 3], script[i + 4]])
154                    as usize;
155            i += 5 + len;
156            continue;
157        }
158
159        if opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY || opcode == OP_CHECKSIGADD {
160            count = count.saturating_add(1);
161        }
162        i += 1;
163    }
164    count
165}
166
167/// Check if a script is P2SH (Pay-to-Script-Hash) — Orange Paper 5.2.1 IsP2SH
168///
169/// P2SH scripts have the format: OP_HASH160 (0xa9) <20-byte-hash> OP_EQUAL (0x87)
170#[spec_locked("5.2.1")]
171pub fn is_pay_to_script_hash(script: &[u8]) -> bool {
172    script.len() == 23
173        && script[0] == OP_HASH160  // OP_HASH160
174        && script[1] == 0x14  // Push 20 bytes
175        && script[22] == OP_EQUAL // OP_EQUAL
176}
177
178/// Extract redeem script from P2SH scriptSig
179///
180/// For P2SH, the scriptSig pushes the redeem script. We need to extract
181/// the last push data item from scriptSig.
182/// Orange Paper 5.2.1: P2SH scriptSig must contain only pushes; last push = redeem script.
183#[spec_locked("5.2.1")]
184fn extract_redeem_script_from_scriptsig(script_sig: &ByteString) -> Option<ByteString> {
185    let mut i = 0;
186    let mut last_data: Option<ByteString> = None;
187
188    while i < script_sig.len() {
189        let opcode = script_sig[i];
190
191        if opcode <= OP_PUSHDATA4 {
192            // Push data opcode
193            let (len, advance) = if opcode < OP_PUSHDATA1 {
194                // Direct push: opcode is the length
195                let len = opcode as usize;
196                (len, 1)
197            } else if opcode == OP_PUSHDATA1 {
198                // OP_PUSHDATA1
199                if i + 1 >= script_sig.len() {
200                    return None;
201                }
202                let len = script_sig[i + 1] as usize;
203                (len, 2)
204            } else if opcode == OP_PUSHDATA2 {
205                // OP_PUSHDATA2
206                if i + 2 >= script_sig.len() {
207                    return None;
208                }
209                let len = u16::from_le_bytes([script_sig[i + 1], script_sig[i + 2]]) as usize;
210                (len, 3)
211            } else if opcode == OP_PUSHDATA4 {
212                // OP_PUSHDATA4
213                if i + 4 >= script_sig.len() {
214                    return None;
215                }
216                let len = u32::from_le_bytes([
217                    script_sig[i + 1],
218                    script_sig[i + 2],
219                    script_sig[i + 3],
220                    script_sig[i + 4],
221                ]) as usize;
222                (len, 5)
223            } else {
224                // Other push opcodes (OP_1NEGATE, OP_RESERVED, OP_1-OP_16)
225                (0, 1)
226            };
227
228            if i + advance + len > script_sig.len() {
229                return None;
230            }
231
232            last_data = Some(script_sig[i + advance..i + advance + len].to_vec());
233            i += advance + len;
234        } else if (OP_1..=OP_16).contains(&opcode) {
235            // OP_1 to OP_16: push single byte
236            last_data = Some(vec![opcode - OP_N_BASE]); // Convert OP_N to value N
237            i += 1;
238        } else {
239            // Other opcode: not a push, invalid for P2SH
240            return None;
241        }
242    }
243
244    last_data
245}
246
247/// Get legacy sigop count from transaction
248///
249/// Counts sigops in scriptSig and scriptPubKey of all inputs and outputs.
250/// Matches consensus's GetLegacySigOpCount().
251///
252/// # Arguments
253/// * `tx` - Transaction to count sigops in
254///
255/// # Returns
256/// Total number of legacy sigops
257#[spec_locked("5.2.2")]
258pub fn get_legacy_sigop_count(tx: &Transaction) -> u32 {
259    let mut count = 0u32;
260
261    // Count sigops in all input scriptSigs
262    for input in &tx.inputs {
263        count = count.saturating_add(count_sigops_in_script(&input.script_sig, false));
264    }
265
266    // Count sigops in all output scriptPubKeys
267    for output in &tx.outputs {
268        count = count.saturating_add(count_sigops_in_script(&output.script_pubkey, false));
269    }
270
271    count
272}
273
274/// Get P2SH sigop count from transaction
275///
276/// Counts sigops in P2SH redeem scripts. Only counts sigops for outputs
277/// that are P2SH (Pay-to-Script-Hash).
278/// Matches consensus's GetP2SHSigOpCount().
279///
280/// # Arguments
281/// * `tx` - Transaction to count sigops in
282/// * `utxo_lookup` - UTXO lookup (UtxoSet or UtxoOverlay)
283///
284/// # Returns
285/// Total number of P2SH sigops
286#[spec_locked("5.2.2")]
287pub fn get_p2sh_sigop_count<U: UtxoLookup>(tx: &Transaction, utxo_lookup: &U) -> Result<u32> {
288    // Coinbase transactions have no P2SH sigops
289    use crate::transaction::is_coinbase;
290    if is_coinbase(tx) {
291        return Ok(0);
292    }
293
294    let mut count = 0u32;
295
296    for input in &tx.inputs {
297        // Get the UTXO (scriptPubKey) for this input
298        if let Some(utxo) = utxo_lookup.get(&input.prevout) {
299            // Check if this is a P2SH output
300            if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
301                // Extract redeem script from scriptSig
302                if let Some(redeem_script) = extract_redeem_script_from_scriptsig(&input.script_sig)
303                {
304                    // Count sigops in redeem script (use accurate counting)
305                    count = count.saturating_add(count_sigops_in_script(&redeem_script, true));
306                }
307            }
308        }
309    }
310
311    Ok(count)
312}
313
314/// Count witness sigops in transaction
315///
316/// Counts sigops in witness scripts for SegWit transactions.
317/// P2WPKH: 1 sigop; P2WSH: count in witness script; P2TR: count in tapscript.
318///
319/// # Arguments
320/// * `tx` - Transaction
321/// * `witnesses` - Witness data for each input (slice of Witness vectors)
322/// * `utxo_lookup` - UTXO lookup (UtxoSet or UtxoOverlay)
323/// * `flags` - Script verification flags
324///
325/// # Returns
326/// Number of witness sigops
327#[spec_locked("11.1")]
328pub(crate) fn count_witness_sigops<U: UtxoLookup>(
329    tx: &Transaction,
330    witnesses: &[Witness],
331    utxo_lookup: &U,
332    flags: u32,
333) -> Result<u64> {
334    use crate::transaction::is_coinbase;
335
336    // SegWit flag must be enabled
337    if (flags & 0x800) == 0 {
338        return Ok(0);
339    }
340
341    if is_coinbase(tx) {
342        return Ok(0);
343    }
344
345    let mut count = 0u64;
346
347    for (i, input) in tx.inputs.iter().enumerate() {
348        if let Some(utxo) = utxo_lookup.get(&input.prevout) {
349            let script_pubkey = &utxo.script_pubkey;
350
351            // P2WPKH: OP_0 <20-byte-hash>
352            if script_pubkey.len() == 22 && script_pubkey[0] == OP_0 && script_pubkey[1] == 0x14 {
353                // P2WPKH has 1 sigop (the CHECKSIG in the witness script)
354                if let Some(witness) = witnesses.get(i) {
355                    if !witness.is_empty() {
356                        count = count.saturating_add(1);
357                    }
358                }
359            }
360            // P2WSH: OP_0 <32-byte-hash>
361            else if script_pubkey.len() == 34
362                && script_pubkey[0] == OP_0
363                && script_pubkey[1] == 0x20
364            {
365                // P2WSH: count sigops in witness script
366                if let Some(witness) = witnesses.get(i) {
367                    if let Some(witness_script) = witness.last() {
368                        count = count
369                            .saturating_add(count_sigops_in_script(witness_script, true) as u64);
370                    }
371                }
372            }
373            // P2TR (witness v1): do **not** add tapscript sigops here.
374            // Bitcoin Core `WitnessSigOps` only handles version 0; v1 returns 0.
375            // BIP 342 enforces signature-related limits via tapscript validation weight during
376            // execution, not `MAX_BLOCK_SIGOPS_COST`. Counting tapscript ops here overstates the
377            // block total and rejects mined mainnet blocks (e.g. heavy tapscript spends).
378        }
379    }
380
381    Ok(count)
382}
383
384/// Legacy sigop count with accurate OP_CHECKMULTISIG (OP_1..OP_16 = 1..16, else 20).
385/// Used for BIP54 per-tx 2500 limit to match Core's GetSigOpCount(fAccurate=true).
386#[spec_locked("5.2.2")]
387pub fn get_legacy_sigop_count_accurate(tx: &Transaction) -> u32 {
388    let mut count = 0u32;
389    for input in &tx.inputs {
390        count = count.saturating_add(count_sigops_in_script(&input.script_sig, true));
391    }
392    for output in &tx.outputs {
393        count = count.saturating_add(count_sigops_in_script(&output.script_pubkey, true));
394    }
395    count
396}
397
398/// Get total transaction sigop count (BIP54 limit).
399///
400/// Sum of legacy + P2SH + witness sigop counts (same accounting as BIP16).
401/// Used to enforce per-transaction limit of 2500 sigops after BIP54 activation.
402pub fn get_transaction_sigop_count<U: UtxoLookup>(
403    tx: &Transaction,
404    utxo_lookup: &U,
405    witnesses: Option<&[Witness]>,
406    flags: u32,
407) -> Result<u64> {
408    let legacy = get_legacy_sigop_count(tx) as u64;
409    let p2sh = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
410    let witness = witnesses
411        .map(|w| count_witness_sigops(tx, w, utxo_lookup, flags))
412        .unwrap_or(Ok(0))?;
413    Ok(legacy.saturating_add(p2sh).saturating_add(witness))
414}
415
416/// BIP54 per-tx sigop count: legacy (accurate) + P2SH + witness.
417/// Matches Core's CheckSigopsBIP54 (GetSigOpCount(scriptSig, true) for legacy).
418pub fn get_transaction_sigop_count_for_bip54<U: UtxoLookup>(
419    tx: &Transaction,
420    utxo_lookup: &U,
421    witnesses: Option<&[Witness]>,
422    flags: u32,
423) -> Result<u64> {
424    let legacy = get_legacy_sigop_count_accurate(tx) as u64;
425    let p2sh = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
426    let witness = witnesses
427        .map(|w| count_witness_sigops(tx, w, utxo_lookup, flags))
428        .unwrap_or(Ok(0))?;
429    Ok(legacy.saturating_add(p2sh).saturating_add(witness))
430}
431
432/// Get total transaction sigop cost
433///
434/// Calculates total sigop cost for a transaction, including:
435/// - Legacy sigops × 4 (witness scale factor)
436/// - P2SH sigops × 4 (if P2SH enabled)
437/// - Witness sigops (actual count, not scaled)
438///
439/// Matches consensus's GetTransactionSigOpCost().
440///
441/// # Arguments
442/// * `tx` - Transaction to count sigops in
443/// * `utxo_set` - UTXO set to lookup inputs
444/// * `witness` - Witness data for this transaction (one Witness per input)
445/// * `flags` - Script verification flags
446///
447/// # Returns
448/// Total sigop cost
449#[spec_locked("5.2.2")]
450pub fn get_transaction_sigop_cost<U: UtxoLookup>(
451    tx: &Transaction,
452    utxo_lookup: &U,
453    witness: Option<&Witness>,
454    flags: u32,
455) -> Result<u64> {
456    let witness_slices = witness.map(std::slice::from_ref);
457    get_transaction_sigop_cost_with_witness_slices(tx, utxo_lookup, witness_slices, flags)
458}
459
460/// Same as get_transaction_sigop_cost but accepts pre-fetched UTXOs in input order.
461/// Avoids redundant overlay lookups when caller already has UTXO data.
462#[spec_locked("5.2.2")]
463pub fn get_transaction_sigop_cost_with_utxos(
464    tx: &Transaction,
465    utxos: &[Option<&UTXO>],
466    witnesses: Option<&[Witness]>,
467    flags: u32,
468) -> Result<u64> {
469    let legacy_count = get_legacy_sigop_count(tx) as u64;
470    let mut total_cost = legacy_count.saturating_mul(WITNESS_SCALE_FACTOR);
471
472    use crate::transaction::is_coinbase;
473    if is_coinbase(tx) {
474        return Ok(total_cost);
475    }
476
477    if (flags & 0x01) != 0 {
478        let mut p2sh_count = 0u32;
479        for (input, utxo_opt) in tx.inputs.iter().zip(utxos.iter()) {
480            if let Some(utxo) = utxo_opt {
481                if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
482                    if let Some(redeem_script) =
483                        extract_redeem_script_from_scriptsig(&input.script_sig)
484                    {
485                        p2sh_count =
486                            p2sh_count.saturating_add(count_sigops_in_script(&redeem_script, true));
487                    }
488                }
489            }
490        }
491        total_cost = total_cost
492            .saturating_add(p2sh_count.saturating_mul(WITNESS_SCALE_FACTOR as u32) as u64);
493    }
494
495    if let Some(witnesses) = witnesses {
496        if (flags & 0x800) != 0 {
497            for (i, (input, utxo_opt)) in tx.inputs.iter().zip(utxos.iter()).enumerate() {
498                if let Some(utxo) = utxo_opt {
499                    let script_pubkey = utxo.script_pubkey.as_ref();
500                    if script_pubkey.len() == 22
501                        && script_pubkey[0] == OP_0
502                        && script_pubkey[1] == 0x14
503                    {
504                        if let Some(witness) = witnesses.get(i) {
505                            if !witness.is_empty() {
506                                total_cost = total_cost.saturating_add(1);
507                            }
508                        }
509                    } else if script_pubkey.len() == 34
510                        && script_pubkey[0] == OP_0
511                        && script_pubkey[1] == 0x20
512                    {
513                        if let Some(witness) = witnesses.get(i) {
514                            if let Some(witness_script) = witness.last() {
515                                total_cost = total_cost.saturating_add(count_sigops_in_script(
516                                    witness_script,
517                                    true,
518                                )
519                                    as u64);
520                            }
521                        }
522                    }
523                    // P2TR / witness v1: witness sigop cost is 0 for block limit (match Core WitnessSigOps).
524                }
525            }
526        }
527    }
528
529    Ok(total_cost)
530}
531
532/// Same as get_transaction_sigop_cost but accepts per-input witness slices directly.
533/// Avoids flattening witness data in block validation hot path.
534#[spec_locked("5.2.2")]
535pub fn get_transaction_sigop_cost_with_witness_slices<U: UtxoLookup>(
536    tx: &Transaction,
537    utxo_lookup: &U,
538    witnesses: Option<&[Witness]>,
539    flags: u32,
540) -> Result<u64> {
541    // Legacy sigops × witness scale factor
542    let legacy_count = get_legacy_sigop_count(tx) as u64;
543    let mut total_cost = legacy_count.saturating_mul(WITNESS_SCALE_FACTOR);
544
545    use crate::transaction::is_coinbase;
546    if is_coinbase(tx) {
547        return Ok(total_cost);
548    }
549
550    // P2SH sigops × witness scale factor (if P2SH enabled)
551    if (flags & 0x01) != 0 {
552        // SCRIPT_VERIFY_P2SH flag enabled
553        let p2sh_count = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
554        total_cost = total_cost.saturating_add(p2sh_count.saturating_mul(WITNESS_SCALE_FACTOR));
555    }
556
557    // Witness sigops (actual count, not scaled)
558    if let Some(witnesses) = witnesses {
559        let witness_count = count_witness_sigops(tx, witnesses, utxo_lookup, flags)?;
560        total_cost = total_cost.saturating_add(witness_count);
561    }
562
563    Ok(total_cost)
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_count_sigops_checksig() {
572        // Script with OP_CHECKSIG
573        let script = vec![OP_1, OP_1, OP_CHECKSIG]; // OP_1, OP_1, OP_CHECKSIG
574        assert_eq!(count_sigops_in_script(&script, false), 1);
575    }
576
577    #[test]
578    fn test_count_sigops_checksigverify() {
579        // Script with OP_CHECKSIGVERIFY
580        let script = vec![OP_1, OP_1, OP_CHECKSIGVERIFY]; // OP_1, OP_1, OP_CHECKSIGVERIFY
581        assert_eq!(count_sigops_in_script(&script, false), 1);
582    }
583
584    #[test]
585    fn test_count_sigops_multisig() {
586        // Script with OP_CHECKMULTISIG (defaults to 20)
587        let script = vec![OP_1, OP_2, OP_CHECKMULTISIG]; // OP_1, OP_2, OP_CHECKMULTISIG
588        assert_eq!(count_sigops_in_script(&script, false), 20);
589
590        // Accurate mode: use OP_2 value (2 sigops)
591        assert_eq!(count_sigops_in_script(&script, true), 2);
592    }
593
594    #[test]
595    fn test_get_legacy_sigop_count() {
596        let tx = Transaction {
597            version: 1,
598            inputs: vec![TransactionInput {
599                prevout: OutPoint {
600                    hash: [0; 32].into(),
601                    index: 0,
602                },
603                script_sig: vec![OP_1, OP_CHECKSIG], // OP_1, OP_CHECKSIG
604                sequence: 0xffffffff,
605            }]
606            .into(),
607            outputs: vec![TransactionOutput {
608                value: 1000,
609                script_pubkey: vec![OP_1, OP_CHECKSIGVERIFY].into(), // OP_1, OP_CHECKSIGVERIFY
610            }]
611            .into(),
612            lock_time: 0,
613        };
614
615        assert_eq!(get_legacy_sigop_count(&tx), 2);
616    }
617
618    #[test]
619    fn test_is_pay_to_script_hash() {
620        // Valid P2SH script: OP_HASH160 <20 bytes> OP_EQUAL
621        let mut p2sh_script = vec![OP_HASH160, 0x14]; // OP_HASH160, push 20
622        p2sh_script.extend_from_slice(&[0u8; 20]);
623        p2sh_script.push(OP_EQUAL); // OP_EQUAL
624
625        assert!(is_pay_to_script_hash(&p2sh_script));
626
627        // Invalid: wrong length
628        assert!(!is_pay_to_script_hash(&vec![OP_HASH160, 0x14]));
629
630        // Invalid: not P2SH format
631        let p2pkh = vec![OP_DUP, OP_HASH160, 0x14]; // OP_DUP OP_HASH160
632        assert!(!is_pay_to_script_hash(&p2pkh));
633    }
634
635    // ==========================================================================
636    // REGRESSION TESTS: Push data must not be counted as sigops (block 310357 fix)
637    // ==========================================================================
638    // These tests prevent regression of the bug where bytes inside push data
639    // (e.g., 0xAC = OP_CHECKSIG) were incorrectly counted as sigops.
640    // This caused valid blocks to be rejected with "sigop cost exceeds maximum".
641
642    #[test]
643    fn test_pushdata1_containing_checksig_byte_not_counted() {
644        // OP_PUSHDATA1 <len=3> <0xAC 0xAC 0xAC>
645        // The 0xAC bytes are push DATA, not OP_CHECKSIG opcodes.
646        // Sigop count must be 0.
647        let script = vec![OP_PUSHDATA1, 0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG];
648        assert_eq!(
649            count_sigops_in_script(&script, false),
650            0,
651            "Push data containing 0xAC must NOT be counted as sigops"
652        );
653    }
654
655    #[test]
656    fn test_pushdata2_containing_checksig_byte_not_counted() {
657        // OP_PUSHDATA2 <len=4 as u16 LE> <0xAC 0xAD 0xAE 0xAF>
658        // These are push DATA bytes, not opcodes.
659        let script = vec![
660            OP_PUSHDATA2,
661            0x04,
662            0x00,
663            OP_CHECKSIG,
664            OP_CHECKSIGVERIFY,
665            OP_CHECKMULTISIG,
666            OP_CHECKMULTISIGVERIFY,
667        ];
668        assert_eq!(
669            count_sigops_in_script(&script, false),
670            0,
671            "Push data containing sigop-like bytes must NOT be counted"
672        );
673    }
674
675    #[test]
676    fn test_pushdata4_containing_checksig_byte_not_counted() {
677        // OP_PUSHDATA4 <len=2 as u32 LE> <0xAC 0xAC>
678        let script = vec![
679            OP_PUSHDATA4,
680            0x02,
681            0x00,
682            0x00,
683            0x00,
684            OP_CHECKSIG,
685            OP_CHECKSIG,
686        ];
687        assert_eq!(
688            count_sigops_in_script(&script, false),
689            0,
690            "OP_PUSHDATA4 data containing 0xAC must NOT be counted"
691        );
692    }
693
694    #[test]
695    fn test_direct_push_containing_checksig_byte_not_counted() {
696        // Direct push: opcode 0x05 means "push next 5 bytes"
697        // Data contains OP_CHECKSIG byte which must NOT be counted.
698        let script = vec![
699            0x05,
700            OP_CHECKSIG,
701            OP_CHECKSIG,
702            OP_CHECKSIG,
703            OP_CHECKSIG,
704            OP_CHECKSIG,
705        ];
706        assert_eq!(
707            count_sigops_in_script(&script, false),
708            0,
709            "Direct push data containing 0xAC must NOT be counted as sigops"
710        );
711    }
712
713    #[test]
714    fn test_push_data_then_real_checksig() {
715        // Direct push of 3 bytes (containing 0xAC), then a REAL OP_CHECKSIG
716        // Only the real OP_CHECKSIG (after push data) should count.
717        let script = vec![0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG]; // push 3, data, then OP_CHECKSIG
718        assert_eq!(
719            count_sigops_in_script(&script, false),
720            1,
721            "Only real OP_CHECKSIG after push data should count"
722        );
723    }
724
725    #[test]
726    fn test_pushdata1_then_real_multisig() {
727        // OP_PUSHDATA1 <len=2> <OP_CHECKSIG OP_CHECKSIG> then OP_2 OP_CHECKMULTISIG
728        // The OP_CHECKSIG bytes in push data don't count. Only the real OP_CHECKMULTISIG counts.
729        let script = vec![
730            OP_PUSHDATA1,
731            0x02,
732            OP_CHECKSIG,
733            OP_CHECKSIG,
734            OP_2,
735            OP_CHECKMULTISIG,
736        ];
737        // Inaccurate mode: multisig = 20
738        assert_eq!(
739            count_sigops_in_script(&script, false),
740            20,
741            "Only real OP_CHECKMULTISIG should count (inaccurate=20)"
742        );
743        // Accurate mode: OP_2 before OP_CHECKMULTISIG = 2
744        assert_eq!(
745            count_sigops_in_script(&script, true),
746            2,
747            "Accurate mode: OP_2 before OP_CHECKMULTISIG = 2 sigops"
748        );
749    }
750
751    #[test]
752    fn test_empty_script_zero_sigops() {
753        let script: Vec<u8> = vec![];
754        assert_eq!(count_sigops_in_script(&script, false), 0);
755        assert_eq!(count_sigops_in_script(&script, true), 0);
756    }
757
758    #[test]
759    fn test_truncated_pushdata1_does_not_panic() {
760        // OP_PUSHDATA1 at end of script (no length byte)
761        let script = vec![OP_PUSHDATA1];
762        assert_eq!(count_sigops_in_script(&script, false), 0);
763    }
764
765    #[test]
766    fn test_truncated_pushdata2_does_not_panic() {
767        // OP_PUSHDATA2 with only 1 of 2 length bytes
768        let script = vec![OP_PUSHDATA2, 0x01];
769        assert_eq!(count_sigops_in_script(&script, false), 0);
770    }
771
772    #[test]
773    fn test_truncated_pushdata4_does_not_panic() {
774        // OP_PUSHDATA4 with only 3 of 4 length bytes
775        let script = vec![OP_PUSHDATA4, 0x01, 0x00, 0x00];
776        assert_eq!(count_sigops_in_script(&script, false), 0);
777    }
778
779    #[test]
780    fn test_large_push_data_with_many_checksig_bytes() {
781        // Simulate a realistic script where push data contains many OP_CHECKSIG bytes
782        // This is the kind of script that caused the block 310357 failure.
783        // OP_PUSHDATA2 <len=100 as u16 LE> <100 bytes of OP_CHECKSIG>
784        let mut script = vec![OP_PUSHDATA2, 100, 0x00]; // OP_PUSHDATA2, length=100
785        script.extend_from_slice(&[OP_CHECKSIG; 100]); // 100 bytes of OP_CHECKSIG data
786        assert_eq!(
787            count_sigops_in_script(&script, false),
788            0,
789            "100 bytes of OP_CHECKSIG in push data must count as 0 sigops"
790        );
791    }
792
793    #[test]
794    fn test_multiple_sigop_opcodes() {
795        // OP_CHECKSIG OP_CHECKSIG OP_CHECKSIGVERIFY
796        let script = vec![0xac, 0xac, 0xad];
797        assert_eq!(count_sigops_in_script(&script, false), 3);
798    }
799
800    #[test]
801    fn test_multisig_accurate_op_16() {
802        // OP_16 (0x60) OP_CHECKMULTISIG
803        let script = vec![0x60, 0xae];
804        assert_eq!(count_sigops_in_script(&script, true), 16);
805    }
806
807    #[test]
808    fn test_multisig_accurate_op_1() {
809        // OP_1 (0x51) OP_CHECKMULTISIG
810        let script = vec![0x51, 0xae];
811        assert_eq!(count_sigops_in_script(&script, true), 1);
812    }
813
814    /// Prefetched-UTXO sigop cost must match overlay-based counting (used on assume-valid path).
815    /// P2TR witness does not contribute to legacy block sigop cost (`WitnessSigOps` v1 = 0 in Core).
816    #[test]
817    fn get_transaction_sigop_cost_with_utxos_matches_witness_slices_p2tr_excluded_from_block_cost()
818    {
819        use crate::segwit::Witness;
820
821        let prev = OutPoint {
822            hash: [7u8; 32].into(),
823            index: 0,
824        };
825        let mut spk = vec![OP_1, 0x20];
826        spk.extend_from_slice(&[9u8; 32]);
827
828        let utxo = UTXO {
829            value: 50_000,
830            script_pubkey: spk.into(),
831            height: 700_000,
832            is_coinbase: false,
833        };
834
835        let mut set: UtxoSet = Default::default();
836        utxo_set_insert(&mut set, prev, utxo);
837
838        let tapscript = vec![OP_CHECKSIG];
839        let witness_two: Witness = vec![tapscript.clone(), vec![0u8; 32]];
840        let witness_three: Witness = vec![tapscript, vec![0x50], vec![0u8; 32]];
841
842        let tx = Transaction {
843            version: 2,
844            inputs: vec![TransactionInput {
845                prevout: prev,
846                script_sig: vec![],
847                sequence: 0xffffffff,
848            }]
849            .into(),
850            outputs: vec![TransactionOutput {
851                value: 10_000,
852                script_pubkey: vec![OP_0].into(),
853            }]
854            .into(),
855            lock_time: 0,
856        };
857
858        let flags = 0x800 | 0x8000 | 0x01;
859        let uref = set.get(&prev).map(|a| a.as_ref());
860        let utxo_refs: Vec<Option<&UTXO>> = vec![uref];
861
862        for witnesses in [&witness_two, &witness_three] {
863            let w = vec![witnesses.clone()];
864            let with_slices = get_transaction_sigop_cost_with_witness_slices(
865                &tx,
866                &set,
867                Some(w.as_slice()),
868                flags,
869            )
870            .unwrap();
871            let with_utxos =
872                get_transaction_sigop_cost_with_utxos(&tx, &utxo_refs, Some(w.as_slice()), flags)
873                    .unwrap();
874            assert_eq!(
875                with_utxos, with_slices,
876                "sigop cost must match between utxo prefetch and overlay lookup"
877            );
878            assert_eq!(
879                with_slices, 0,
880                "tapscript in P2TR witness must not add to block sigop cost"
881            );
882        }
883    }
884}