use crate::error::Result;
use crate::opcodes::*;
use crate::segwit::Witness;
use crate::types::*;
use crate::utxo_overlay::UtxoLookup;
use blvm_spec_lock::spec_locked;
const MAX_PUBKEYS_PER_MULTISIG: u32 = 20;
const WITNESS_SCALE_FACTOR: u64 = 4;
#[spec_locked("5.2.2")]
pub fn count_sigops_in_script(script: &ByteString, accurate: bool) -> u32 {
let mut count = 0u32;
let mut last_opcode: Option<u8> = None;
let mut i = 0;
while i < script.len() {
let opcode = script[i];
if opcode > 0 && opcode < OP_PUSHDATA1 {
let len = opcode as usize;
last_opcode = Some(opcode);
i += 1 + len;
continue;
} else if opcode == OP_PUSHDATA1 {
if i + 1 >= script.len() {
break;
}
let len = script[i + 1] as usize;
last_opcode = Some(opcode);
i += 2 + len;
continue;
} else if opcode == OP_PUSHDATA2 {
if i + 2 >= script.len() {
break;
}
let len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
last_opcode = Some(opcode);
i += 3 + len;
continue;
} else if opcode == OP_PUSHDATA4 {
if i + 4 >= script.len() {
break;
}
let len =
u32::from_le_bytes([script[i + 1], script[i + 2], script[i + 3], script[i + 4]])
as usize;
last_opcode = Some(opcode);
i += 5 + len;
continue;
}
if opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY {
count = count.saturating_add(1);
}
else if opcode == OP_CHECKMULTISIG || opcode == OP_CHECKMULTISIGVERIFY {
if accurate {
if let Some(prev_op) = last_opcode {
if (OP_1..=OP_16).contains(&prev_op) {
let n = (prev_op - OP_1 + 1) as u32;
count = count.saturating_add(n);
} else {
count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
}
} else {
count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
}
} else {
count = count.saturating_add(MAX_PUBKEYS_PER_MULTISIG);
}
}
last_opcode = Some(opcode);
i += 1;
}
count
}
#[spec_locked("11.2.8")]
pub fn count_tapscript_sigops(script: &ByteString) -> u32 {
let mut count = 0u32;
let mut i = 0;
while i < script.len() {
let opcode = script[i];
if opcode > 0 && opcode < OP_PUSHDATA1 {
let len = opcode as usize;
i += 1 + len;
continue;
} else if opcode == OP_PUSHDATA1 {
if i + 1 >= script.len() {
break;
}
let len = script[i + 1] as usize;
i += 2 + len;
continue;
} else if opcode == OP_PUSHDATA2 {
if i + 2 >= script.len() {
break;
}
let len = u16::from_le_bytes([script[i + 1], script[i + 2]]) as usize;
i += 3 + len;
continue;
} else if opcode == OP_PUSHDATA4 {
if i + 4 >= script.len() {
break;
}
let len =
u32::from_le_bytes([script[i + 1], script[i + 2], script[i + 3], script[i + 4]])
as usize;
i += 5 + len;
continue;
}
if opcode == OP_CHECKSIG || opcode == OP_CHECKSIGVERIFY || opcode == OP_CHECKSIGADD {
count = count.saturating_add(1);
}
i += 1;
}
count
}
#[spec_locked("5.2.1")]
pub fn is_pay_to_script_hash(script: &[u8]) -> bool {
script.len() == 23
&& script[0] == OP_HASH160 && script[1] == 0x14 && script[22] == OP_EQUAL }
#[spec_locked("5.2.1")]
fn extract_redeem_script_from_scriptsig(script_sig: &ByteString) -> Option<ByteString> {
let mut i = 0;
let mut last_data: Option<ByteString> = None;
while i < script_sig.len() {
let opcode = script_sig[i];
if opcode <= OP_PUSHDATA4 {
let (len, advance) = if opcode < OP_PUSHDATA1 {
let len = opcode as usize;
(len, 1)
} else if opcode == OP_PUSHDATA1 {
if i + 1 >= script_sig.len() {
return None;
}
let len = script_sig[i + 1] as usize;
(len, 2)
} else if opcode == OP_PUSHDATA2 {
if i + 2 >= script_sig.len() {
return None;
}
let len = u16::from_le_bytes([script_sig[i + 1], script_sig[i + 2]]) as usize;
(len, 3)
} else if opcode == OP_PUSHDATA4 {
if i + 4 >= script_sig.len() {
return None;
}
let len = u32::from_le_bytes([
script_sig[i + 1],
script_sig[i + 2],
script_sig[i + 3],
script_sig[i + 4],
]) as usize;
(len, 5)
} else {
(0, 1)
};
if i + advance + len > script_sig.len() {
return None;
}
last_data = Some(script_sig[i + advance..i + advance + len].to_vec());
i += advance + len;
} else if (OP_1..=OP_16).contains(&opcode) {
last_data = Some(vec![opcode - OP_N_BASE]); i += 1;
} else {
return None;
}
}
last_data
}
#[spec_locked("5.2.2")]
pub fn get_legacy_sigop_count(tx: &Transaction) -> u32 {
let mut count = 0u32;
for input in &tx.inputs {
count = count.saturating_add(count_sigops_in_script(&input.script_sig, false));
}
for output in &tx.outputs {
count = count.saturating_add(count_sigops_in_script(&output.script_pubkey, false));
}
count
}
#[spec_locked("5.2.2")]
pub fn get_p2sh_sigop_count<U: UtxoLookup>(tx: &Transaction, utxo_lookup: &U) -> Result<u32> {
use crate::transaction::is_coinbase;
if is_coinbase(tx) {
return Ok(0);
}
let mut count = 0u32;
for input in &tx.inputs {
if let Some(utxo) = utxo_lookup.get(&input.prevout) {
if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
if let Some(redeem_script) = extract_redeem_script_from_scriptsig(&input.script_sig)
{
count = count.saturating_add(count_sigops_in_script(&redeem_script, true));
}
}
}
}
Ok(count)
}
#[spec_locked("11.1")]
pub(crate) fn count_witness_sigops<U: UtxoLookup>(
tx: &Transaction,
witnesses: &[Witness],
utxo_lookup: &U,
flags: u32,
) -> Result<u64> {
use crate::transaction::is_coinbase;
if (flags & 0x800) == 0 {
return Ok(0);
}
if is_coinbase(tx) {
return Ok(0);
}
let mut count = 0u64;
for (i, input) in tx.inputs.iter().enumerate() {
if let Some(utxo) = utxo_lookup.get(&input.prevout) {
let script_pubkey = &utxo.script_pubkey;
if script_pubkey.len() == 22 && script_pubkey[0] == OP_0 && script_pubkey[1] == 0x14 {
if let Some(witness) = witnesses.get(i) {
if !witness.is_empty() {
count = count.saturating_add(1);
}
}
}
else if script_pubkey.len() == 34
&& script_pubkey[0] == OP_0
&& script_pubkey[1] == 0x20
{
if let Some(witness) = witnesses.get(i) {
if let Some(witness_script) = witness.last() {
count = count
.saturating_add(count_sigops_in_script(witness_script, true) as u64);
}
}
}
}
}
Ok(count)
}
#[spec_locked("5.2.2")]
pub fn get_legacy_sigop_count_accurate(tx: &Transaction) -> u32 {
let mut count = 0u32;
for input in &tx.inputs {
count = count.saturating_add(count_sigops_in_script(&input.script_sig, true));
}
for output in &tx.outputs {
count = count.saturating_add(count_sigops_in_script(&output.script_pubkey, true));
}
count
}
pub fn get_transaction_sigop_count<U: UtxoLookup>(
tx: &Transaction,
utxo_lookup: &U,
witnesses: Option<&[Witness]>,
flags: u32,
) -> Result<u64> {
let legacy = get_legacy_sigop_count(tx) as u64;
let p2sh = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
let witness = witnesses
.map(|w| count_witness_sigops(tx, w, utxo_lookup, flags))
.unwrap_or(Ok(0))?;
Ok(legacy.saturating_add(p2sh).saturating_add(witness))
}
pub fn get_transaction_sigop_count_for_bip54<U: UtxoLookup>(
tx: &Transaction,
utxo_lookup: &U,
witnesses: Option<&[Witness]>,
flags: u32,
) -> Result<u64> {
let legacy = get_legacy_sigop_count_accurate(tx) as u64;
let p2sh = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
let witness = witnesses
.map(|w| count_witness_sigops(tx, w, utxo_lookup, flags))
.unwrap_or(Ok(0))?;
Ok(legacy.saturating_add(p2sh).saturating_add(witness))
}
#[spec_locked("5.2.2")]
pub fn get_transaction_sigop_cost<U: UtxoLookup>(
tx: &Transaction,
utxo_lookup: &U,
witness: Option<&Witness>,
flags: u32,
) -> Result<u64> {
let witness_slices = witness.map(std::slice::from_ref);
get_transaction_sigop_cost_with_witness_slices(tx, utxo_lookup, witness_slices, flags)
}
#[spec_locked("5.2.2")]
pub fn get_transaction_sigop_cost_with_utxos(
tx: &Transaction,
utxos: &[Option<&UTXO>],
witnesses: Option<&[Witness]>,
flags: u32,
) -> Result<u64> {
let legacy_count = get_legacy_sigop_count(tx) as u64;
let mut total_cost = legacy_count.saturating_mul(WITNESS_SCALE_FACTOR);
use crate::transaction::is_coinbase;
if is_coinbase(tx) {
return Ok(total_cost);
}
if (flags & 0x01) != 0 {
let mut p2sh_count = 0u32;
for (input, utxo_opt) in tx.inputs.iter().zip(utxos.iter()) {
if let Some(utxo) = utxo_opt {
if is_pay_to_script_hash(utxo.script_pubkey.as_ref()) {
if let Some(redeem_script) =
extract_redeem_script_from_scriptsig(&input.script_sig)
{
p2sh_count =
p2sh_count.saturating_add(count_sigops_in_script(&redeem_script, true));
}
}
}
}
total_cost = total_cost
.saturating_add(p2sh_count.saturating_mul(WITNESS_SCALE_FACTOR as u32) as u64);
}
if let Some(witnesses) = witnesses {
if (flags & 0x800) != 0 {
for (i, (input, utxo_opt)) in tx.inputs.iter().zip(utxos.iter()).enumerate() {
if let Some(utxo) = utxo_opt {
let script_pubkey = utxo.script_pubkey.as_ref();
if script_pubkey.len() == 22
&& script_pubkey[0] == OP_0
&& script_pubkey[1] == 0x14
{
if let Some(witness) = witnesses.get(i) {
if !witness.is_empty() {
total_cost = total_cost.saturating_add(1);
}
}
} else if script_pubkey.len() == 34
&& script_pubkey[0] == OP_0
&& script_pubkey[1] == 0x20
{
if let Some(witness) = witnesses.get(i) {
if let Some(witness_script) = witness.last() {
total_cost = total_cost.saturating_add(count_sigops_in_script(
witness_script,
true,
)
as u64);
}
}
}
}
}
}
}
Ok(total_cost)
}
#[spec_locked("5.2.2")]
pub fn get_transaction_sigop_cost_with_witness_slices<U: UtxoLookup>(
tx: &Transaction,
utxo_lookup: &U,
witnesses: Option<&[Witness]>,
flags: u32,
) -> Result<u64> {
let legacy_count = get_legacy_sigop_count(tx) as u64;
let mut total_cost = legacy_count.saturating_mul(WITNESS_SCALE_FACTOR);
use crate::transaction::is_coinbase;
if is_coinbase(tx) {
return Ok(total_cost);
}
if (flags & 0x01) != 0 {
let p2sh_count = get_p2sh_sigop_count(tx, utxo_lookup)? as u64;
total_cost = total_cost.saturating_add(p2sh_count.saturating_mul(WITNESS_SCALE_FACTOR));
}
if let Some(witnesses) = witnesses {
let witness_count = count_witness_sigops(tx, witnesses, utxo_lookup, flags)?;
total_cost = total_cost.saturating_add(witness_count);
}
Ok(total_cost)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_sigops_checksig() {
let script = vec![OP_1, OP_1, OP_CHECKSIG]; assert_eq!(count_sigops_in_script(&script, false), 1);
}
#[test]
fn test_count_sigops_checksigverify() {
let script = vec![OP_1, OP_1, OP_CHECKSIGVERIFY]; assert_eq!(count_sigops_in_script(&script, false), 1);
}
#[test]
fn test_count_sigops_multisig() {
let script = vec![OP_1, OP_2, OP_CHECKMULTISIG]; assert_eq!(count_sigops_in_script(&script, false), 20);
assert_eq!(count_sigops_in_script(&script, true), 2);
}
#[test]
fn test_get_legacy_sigop_count() {
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32].into(),
index: 0,
},
script_sig: vec![OP_1, OP_CHECKSIG], sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 1000,
script_pubkey: vec![OP_1, OP_CHECKSIGVERIFY].into(), }]
.into(),
lock_time: 0,
};
assert_eq!(get_legacy_sigop_count(&tx), 2);
}
#[test]
fn test_is_pay_to_script_hash() {
let mut p2sh_script = vec![OP_HASH160, 0x14]; p2sh_script.extend_from_slice(&[0u8; 20]);
p2sh_script.push(OP_EQUAL);
assert!(is_pay_to_script_hash(&p2sh_script));
assert!(!is_pay_to_script_hash(&vec![OP_HASH160, 0x14]));
let p2pkh = vec![OP_DUP, OP_HASH160, 0x14]; assert!(!is_pay_to_script_hash(&p2pkh));
}
#[test]
fn test_pushdata1_containing_checksig_byte_not_counted() {
let script = vec![OP_PUSHDATA1, 0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG];
assert_eq!(
count_sigops_in_script(&script, false),
0,
"Push data containing 0xAC must NOT be counted as sigops"
);
}
#[test]
fn test_pushdata2_containing_checksig_byte_not_counted() {
let script = vec![
OP_PUSHDATA2,
0x04,
0x00,
OP_CHECKSIG,
OP_CHECKSIGVERIFY,
OP_CHECKMULTISIG,
OP_CHECKMULTISIGVERIFY,
];
assert_eq!(
count_sigops_in_script(&script, false),
0,
"Push data containing sigop-like bytes must NOT be counted"
);
}
#[test]
fn test_pushdata4_containing_checksig_byte_not_counted() {
let script = vec![
OP_PUSHDATA4,
0x02,
0x00,
0x00,
0x00,
OP_CHECKSIG,
OP_CHECKSIG,
];
assert_eq!(
count_sigops_in_script(&script, false),
0,
"OP_PUSHDATA4 data containing 0xAC must NOT be counted"
);
}
#[test]
fn test_direct_push_containing_checksig_byte_not_counted() {
let script = vec![
0x05,
OP_CHECKSIG,
OP_CHECKSIG,
OP_CHECKSIG,
OP_CHECKSIG,
OP_CHECKSIG,
];
assert_eq!(
count_sigops_in_script(&script, false),
0,
"Direct push data containing 0xAC must NOT be counted as sigops"
);
}
#[test]
fn test_push_data_then_real_checksig() {
let script = vec![0x03, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG, OP_CHECKSIG]; assert_eq!(
count_sigops_in_script(&script, false),
1,
"Only real OP_CHECKSIG after push data should count"
);
}
#[test]
fn test_pushdata1_then_real_multisig() {
let script = vec![
OP_PUSHDATA1,
0x02,
OP_CHECKSIG,
OP_CHECKSIG,
OP_2,
OP_CHECKMULTISIG,
];
assert_eq!(
count_sigops_in_script(&script, false),
20,
"Only real OP_CHECKMULTISIG should count (inaccurate=20)"
);
assert_eq!(
count_sigops_in_script(&script, true),
2,
"Accurate mode: OP_2 before OP_CHECKMULTISIG = 2 sigops"
);
}
#[test]
fn test_empty_script_zero_sigops() {
let script: Vec<u8> = vec![];
assert_eq!(count_sigops_in_script(&script, false), 0);
assert_eq!(count_sigops_in_script(&script, true), 0);
}
#[test]
fn test_truncated_pushdata1_does_not_panic() {
let script = vec![OP_PUSHDATA1];
assert_eq!(count_sigops_in_script(&script, false), 0);
}
#[test]
fn test_truncated_pushdata2_does_not_panic() {
let script = vec![OP_PUSHDATA2, 0x01];
assert_eq!(count_sigops_in_script(&script, false), 0);
}
#[test]
fn test_truncated_pushdata4_does_not_panic() {
let script = vec![OP_PUSHDATA4, 0x01, 0x00, 0x00];
assert_eq!(count_sigops_in_script(&script, false), 0);
}
#[test]
fn test_large_push_data_with_many_checksig_bytes() {
let mut script = vec![OP_PUSHDATA2, 100, 0x00]; script.extend_from_slice(&[OP_CHECKSIG; 100]); assert_eq!(
count_sigops_in_script(&script, false),
0,
"100 bytes of OP_CHECKSIG in push data must count as 0 sigops"
);
}
#[test]
fn test_multiple_sigop_opcodes() {
let script = vec![0xac, 0xac, 0xad];
assert_eq!(count_sigops_in_script(&script, false), 3);
}
#[test]
fn test_multisig_accurate_op_16() {
let script = vec![0x60, 0xae];
assert_eq!(count_sigops_in_script(&script, true), 16);
}
#[test]
fn test_multisig_accurate_op_1() {
let script = vec![0x51, 0xae];
assert_eq!(count_sigops_in_script(&script, true), 1);
}
#[test]
fn get_transaction_sigop_cost_with_utxos_matches_witness_slices_p2tr_excluded_from_block_cost()
{
use crate::segwit::Witness;
let prev = OutPoint {
hash: [7u8; 32].into(),
index: 0,
};
let mut spk = vec![OP_1, 0x20];
spk.extend_from_slice(&[9u8; 32]);
let utxo = UTXO {
value: 50_000,
script_pubkey: spk.into(),
height: 700_000,
is_coinbase: false,
};
let mut set: UtxoSet = Default::default();
utxo_set_insert(&mut set, prev, utxo);
let tapscript = vec![OP_CHECKSIG];
let witness_two: Witness = vec![tapscript.clone(), vec![0u8; 32]];
let witness_three: Witness = vec![tapscript, vec![0x50], vec![0u8; 32]];
let tx = Transaction {
version: 2,
inputs: vec![TransactionInput {
prevout: prev,
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 10_000,
script_pubkey: vec![OP_0].into(),
}]
.into(),
lock_time: 0,
};
let flags = 0x800 | 0x8000 | 0x01;
let uref = set.get(&prev).map(|a| a.as_ref());
let utxo_refs: Vec<Option<&UTXO>> = vec![uref];
for witnesses in [&witness_two, &witness_three] {
let w = vec![witnesses.clone()];
let with_slices = get_transaction_sigop_cost_with_witness_slices(
&tx,
&set,
Some(w.as_slice()),
flags,
)
.unwrap();
let with_utxos =
get_transaction_sigop_cost_with_utxos(&tx, &utxo_refs, Some(w.as_slice()), flags)
.unwrap();
assert_eq!(
with_utxos, with_slices,
"sigop cost must match between utxo prefetch and overlay lookup"
);
assert_eq!(
with_slices, 0,
"tapscript in P2TR witness must not add to block sigop cost"
);
}
}
}