1use 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
15const MAX_PUBKEYS_PER_MULTISIG: u32 = 20;
18
19const WITNESS_SCALE_FACTOR: u64 = 4;
22
23#[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 if opcode > 0 && opcode < OP_PUSHDATA1 {
48 let len = opcode as usize;
50 last_opcode = Some(opcode);
51 i += 1 + len;
52 continue;
53 } else if opcode == OP_PUSHDATA1 {
54 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 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 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 if opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY {
86 count = count.saturating_add(1);
87 }
88 else if opcode == OP_CHECKMULTISIG || opcode == OP_CHECKMULTISIGVERIFY {
90 if accurate {
91 if let Some(prev_op) = last_opcode {
93 if (OP_1..=OP_16).contains(&prev_op) {
94 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 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#[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
167pub fn is_pay_to_script_hash(script: &[u8]) -> bool {
169 script.len() == 23
170 && script[0] == OP_HASH160 && script[1] == 0x14 && script[22] == OP_EQUAL }
174
175fn 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 let (len, advance) = if opcode < OP_PUSHDATA1 {
190 let len = opcode as usize;
192 (len, 1)
193 } else if opcode == OP_PUSHDATA1 {
194 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 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 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 (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 last_data = Some(vec![opcode - OP_N_BASE]); i += 1;
234 } else {
235 return None;
237 }
238 }
239
240 last_data
241}
242
243#[spec_locked("5.2.2", "GetLegacySigOpCount")]
254pub fn get_legacy_sigop_count(tx: &Transaction) -> u32 {
255 let mut count = 0u32;
256
257 for input in &tx.inputs {
259 count = count.saturating_add(count_sigops_in_script(&input.script_sig, false));
260 }
261
262 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#[spec_locked("5.2.2", "GetP2SHSigOpCount")]
283pub fn get_p2sh_sigop_count<U: UtxoLookup>(tx: &Transaction, utxo_lookup: &U) -> Result<u32> {
284 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 if let Some(utxo) = utxo_lookup.get(&input.prevout) {
295 if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
297 if let Some(redeem_script) = extract_redeem_script_from_scriptsig(&input.script_sig)
299 {
300 count = count.saturating_add(count_sigops_in_script(&redeem_script, true));
302 }
303 }
304 }
305 }
306
307 Ok(count)
308}
309
310#[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 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 if script_pubkey.len() == 22 && script_pubkey[0] == OP_0 && script_pubkey[1] == 0x14 {
349 if let Some(witness) = witnesses.get(i) {
351 if !witness.is_empty() {
352 count = count.saturating_add(1);
353 }
354 }
355 }
356 else if script_pubkey.len() == 34
358 && script_pubkey[0] == OP_0
359 && script_pubkey[1] == 0x20
360 {
361 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 }
375 }
376
377 Ok(count)
378}
379
380#[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
394pub 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
412pub 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#[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#[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 }
521 }
522 }
523 }
524
525 Ok(total_cost)
526}
527
528#[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 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 if (flags & 0x01) != 0 {
548 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 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 let script = vec![OP_1, OP_1, OP_CHECKSIG]; assert_eq!(count_sigops_in_script(&script, false), 1);
571 }
572
573 #[test]
574 fn test_count_sigops_checksigverify() {
575 let script = vec![OP_1, OP_1, OP_CHECKSIGVERIFY]; assert_eq!(count_sigops_in_script(&script, false), 1);
578 }
579
580 #[test]
581 fn test_count_sigops_multisig() {
582 let script = vec![OP_1, OP_2, OP_CHECKMULTISIG]; assert_eq!(count_sigops_in_script(&script, false), 20);
585
586 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], sequence: 0xffffffff,
601 }]
602 .into(),
603 outputs: vec![TransactionOutput {
604 value: 1000,
605 script_pubkey: vec![OP_1, OP_CHECKSIGVERIFY].into(), }]
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 let mut p2sh_script = vec![OP_HASH160, 0x14]; p2sh_script.extend_from_slice(&[0u8; 20]);
619 p2sh_script.push(OP_EQUAL); assert!(is_pay_to_script_hash(&p2sh_script));
622
623 assert!(!is_pay_to_script_hash(&vec![OP_HASH160, 0x14]));
625
626 let p2pkh = vec![OP_DUP, OP_HASH160, 0x14]; assert!(!is_pay_to_script_hash(&p2pkh));
629 }
630
631 #[test]
639 fn test_pushdata1_containing_checksig_byte_not_counted() {
640 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 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 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 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 let script = vec![0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG]; 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 let script = vec![
726 OP_PUSHDATA1,
727 0x02,
728 OP_CHECKSIG,
729 OP_CHECKSIG,
730 OP_2,
731 OP_CHECKMULTISIG,
732 ];
733 assert_eq!(
735 count_sigops_in_script(&script, false),
736 20,
737 "Only real OP_CHECKMULTISIG should count (inaccurate=20)"
738 );
739 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 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 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 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 let mut script = vec![OP_PUSHDATA2, 100, 0x00]; script.extend_from_slice(&[OP_CHECKSIG; 100]); 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 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 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 let script = vec![0x51, 0xae];
807 assert_eq!(count_sigops_in_script(&script, true), 1);
808 }
809
810 #[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}