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")]
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")]
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#[spec_locked("5.2.1")]
171pub fn is_pay_to_script_hash(script: &[u8]) -> bool {
172 script.len() == 23
173 && script[0] == OP_HASH160 && script[1] == 0x14 && script[22] == OP_EQUAL }
177
178#[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 let (len, advance) = if opcode < OP_PUSHDATA1 {
194 let len = opcode as usize;
196 (len, 1)
197 } else if opcode == OP_PUSHDATA1 {
198 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 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 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 (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 last_data = Some(vec![opcode - OP_N_BASE]); i += 1;
238 } else {
239 return None;
241 }
242 }
243
244 last_data
245}
246
247#[spec_locked("5.2.2")]
258pub fn get_legacy_sigop_count(tx: &Transaction) -> u32 {
259 let mut count = 0u32;
260
261 for input in &tx.inputs {
263 count = count.saturating_add(count_sigops_in_script(&input.script_sig, false));
264 }
265
266 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#[spec_locked("5.2.2")]
287pub fn get_p2sh_sigop_count<U: UtxoLookup>(tx: &Transaction, utxo_lookup: &U) -> Result<u32> {
288 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 if let Some(utxo) = utxo_lookup.get(&input.prevout) {
299 if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
301 if let Some(redeem_script) = extract_redeem_script_from_scriptsig(&input.script_sig)
303 {
304 count = count.saturating_add(count_sigops_in_script(&redeem_script, true));
306 }
307 }
308 }
309 }
310
311 Ok(count)
312}
313
314#[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 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 if script_pubkey.len() == 22 && script_pubkey[0] == OP_0 && script_pubkey[1] == 0x14 {
353 if let Some(witness) = witnesses.get(i) {
355 if !witness.is_empty() {
356 count = count.saturating_add(1);
357 }
358 }
359 }
360 else if script_pubkey.len() == 34
362 && script_pubkey[0] == OP_0
363 && script_pubkey[1] == 0x20
364 {
365 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 }
379 }
380
381 Ok(count)
382}
383
384#[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
398pub 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
416pub 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#[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#[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 }
525 }
526 }
527 }
528
529 Ok(total_cost)
530}
531
532#[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 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 if (flags & 0x01) != 0 {
552 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 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 let script = vec![OP_1, OP_1, OP_CHECKSIG]; assert_eq!(count_sigops_in_script(&script, false), 1);
575 }
576
577 #[test]
578 fn test_count_sigops_checksigverify() {
579 let script = vec![OP_1, OP_1, OP_CHECKSIGVERIFY]; assert_eq!(count_sigops_in_script(&script, false), 1);
582 }
583
584 #[test]
585 fn test_count_sigops_multisig() {
586 let script = vec![OP_1, OP_2, OP_CHECKMULTISIG]; assert_eq!(count_sigops_in_script(&script, false), 20);
589
590 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], sequence: 0xffffffff,
605 }]
606 .into(),
607 outputs: vec![TransactionOutput {
608 value: 1000,
609 script_pubkey: vec![OP_1, OP_CHECKSIGVERIFY].into(), }]
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 let mut p2sh_script = vec![OP_HASH160, 0x14]; p2sh_script.extend_from_slice(&[0u8; 20]);
623 p2sh_script.push(OP_EQUAL); assert!(is_pay_to_script_hash(&p2sh_script));
626
627 assert!(!is_pay_to_script_hash(&vec![OP_HASH160, 0x14]));
629
630 let p2pkh = vec![OP_DUP, OP_HASH160, 0x14]; assert!(!is_pay_to_script_hash(&p2pkh));
633 }
634
635 #[test]
643 fn test_pushdata1_containing_checksig_byte_not_counted() {
644 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 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 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 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 let script = vec![0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG]; 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 let script = vec![
730 OP_PUSHDATA1,
731 0x02,
732 OP_CHECKSIG,
733 OP_CHECKSIG,
734 OP_2,
735 OP_CHECKMULTISIG,
736 ];
737 assert_eq!(
739 count_sigops_in_script(&script, false),
740 20,
741 "Only real OP_CHECKMULTISIG should count (inaccurate=20)"
742 );
743 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 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 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 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 let mut script = vec![OP_PUSHDATA2, 100, 0x00]; script.extend_from_slice(&[OP_CHECKSIG; 100]); 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 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 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 let script = vec![0x51, 0xae];
811 assert_eq!(count_sigops_in_script(&script, true), 1);
812 }
813
814 #[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}