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