Skip to main content

bitcoin_rs_script/
sigops.rs

1use bitcoin::{Block, Script};
2
3/// Counts legacy sigops in a script using Core's legacy multisig rule.
4pub fn count_legacy(script: &Script) -> u32 {
5    saturating_u32(script.count_sigops_legacy())
6}
7
8/// Counts segwit-v0 sigops for a witness program and witness stack.
9pub fn count_segwit(script: &Script, witness: &[Vec<u8>]) -> u32 {
10    if script.is_p2wpkh() {
11        return 1;
12    }
13    if !script.is_p2wsh() {
14        return 0;
15    }
16    witness.last().map_or(0, |witness_script| {
17        saturating_u32(Script::from_bytes(witness_script).count_sigops())
18    })
19}
20
21/// Counts taproot sigops under BIP342's per-input budget model.
22///
23/// Taproot does not contribute to the legacy per-block sigop cost. Tapscript's
24/// `OP_CHECKSIG` and `OP_CHECKSIGADD` budget is enforced per input by the
25/// delegated interpreter, so this block-level counter returns zero.
26pub const fn count_taproot(_script: &Script, _witness: &[Vec<u8>]) -> u32 {
27    0
28}
29
30/// Counts sigop cost visible without a UTXO set.
31///
32/// P2SH redeem-script and witness sigops that require previous outputs cannot be
33/// counted from a bare block alone; callers with a UTXO view should use the
34/// `bitcoin::Transaction::total_sigop_cost` closure shape directly.
35pub fn count_block(block: &Block) -> u32 {
36    block.txdata.iter().fold(0u32, |count, tx| {
37        count.saturating_add(saturating_u32(tx.total_sigop_cost(|_| None)))
38    })
39}
40
41fn saturating_u32(value: usize) -> u32 {
42    u32::try_from(value).unwrap_or(u32::MAX)
43}
44
45#[cfg(test)]
46mod tests {
47    use bitcoin::blockdata::opcodes::all::{
48        OP_CHECKMULTISIG, OP_CHECKSIG, OP_PUSHNUM_1, OP_PUSHNUM_3,
49    };
50    use bitcoin::script::Builder;
51
52    use super::{count_legacy, count_segwit, count_taproot};
53
54    #[test]
55    fn legacy_count_matches_core_multisig_legacy_rule() {
56        let script = Builder::new()
57            .push_opcode(OP_PUSHNUM_1)
58            .push_slice([3; 33])
59            .push_slice([3; 33])
60            .push_slice([3; 33])
61            .push_opcode(OP_PUSHNUM_3)
62            .push_opcode(OP_CHECKMULTISIG)
63            .into_script();
64        assert_eq!(count_legacy(script.as_script()), 20);
65        assert_eq!(script.count_sigops(), 3);
66    }
67
68    #[test]
69    fn segwit_counts_p2wpkh_and_p2wsh_witness_script() {
70        let p2wpkh = Builder::new().push_int(0).push_slice([7; 20]).into_script();
71        assert_eq!(count_segwit(p2wpkh.as_script(), &[]), 1);
72
73        let witness_script = Builder::new().push_opcode(OP_CHECKSIG).into_script();
74        let p2wsh = Builder::new().push_int(0).push_slice([9; 32]).into_script();
75        assert_eq!(
76            count_segwit(p2wsh.as_script(), &[witness_script.as_bytes().to_vec()]),
77            1
78        );
79    }
80
81    #[test]
82    fn taproot_has_no_legacy_block_sigop_charge() {
83        let p2tr = Builder::new().push_int(1).push_slice([9; 32]).into_script();
84        assert_eq!(count_taproot(p2tr.as_script(), &[]), 0);
85    }
86}