use crate::error::Result;
use crate::opcodes::*;
use crate::segwit::Witness;
use crate::types::*;
use crate::utxo_overlay::UtxoLookup;
#[cfg(feature = "spec-lock")]
use blvm_spec_lock::spec_locked;
const MAX_PUBKEYS_PER_MULTISIG: u32 = 20;
const WITNESS_SCALE_FACTOR: u64 = 4;
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "CountSigOpsInScript"))]
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
}
#[cfg_attr(feature = "spec-lock", spec_locked("11.2.8", "CountTapscriptSigOps"))]
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
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.1", "IsP2SH"))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result == false || script.len() == 23))]
pub fn is_pay_to_script_hash(script: &[u8]) -> bool {
script.len() == 23
&& script[0] == OP_HASH160 && script[1] == 0x14 && script[22] == OP_EQUAL }
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "F_SigopCountPushOpcode"))]
#[inline]
fn sigop_count_push_opcode(op: u8) -> bool {
op <= OP_16
}
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 !sigop_count_push_opcode(opcode) {
return None;
}
if opcode <= OP_PUSHDATA4 {
let (len, advance) = if opcode < OP_PUSHDATA1 {
(opcode as usize, 1)
} else if opcode == OP_PUSHDATA1 {
if i + 1 >= script_sig.len() {
return None;
}
(script_sig[i + 1] as usize, 2)
} else if opcode == OP_PUSHDATA2 {
if i + 2 >= script_sig.len() {
return None;
}
(
u16::from_le_bytes([script_sig[i + 1], script_sig[i + 2]]) as usize,
3,
)
} else {
if i + 4 >= script_sig.len() {
return None;
}
(
u32::from_le_bytes([
script_sig[i + 1],
script_sig[i + 2],
script_sig[i + 3],
script_sig[i + 4],
]) as usize,
5,
)
};
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 {
last_data = Some(Vec::new());
i += 1;
}
}
last_data
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetLegacySigOpCount"))]
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
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetP2SHSigOpCount"))]
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)
}
fn is_version_zero_witness_program(script: &[u8]) -> bool {
(script.len() == 22 && script[0] == OP_0 && script[1] == 0x14)
|| (script.len() == 34 && script[0] == OP_0 && script[1] == 0x20)
}
fn witness_program_sigops(script: &[u8], witness: Option<&Witness>) -> u64 {
if script.len() == 22 && script[0] == OP_0 && script[1] == 0x14 {
return 1;
}
if script.len() == 34 && script[0] == OP_0 && script[1] == 0x20 {
let Some(witness) = witness.filter(|w| !w.is_empty()) else {
return 0;
};
let Some(witness_script) = witness.last() else {
return 0;
};
return count_sigops_in_script(witness_script, true) as u64;
}
0
}
fn input_witness_sigops(
script_pubkey: &[u8],
script_sig: &ByteString,
witness: Option<&Witness>,
flags: u32,
) -> u64 {
if flags & 0x800 == 0 {
return 0;
}
if is_version_zero_witness_program(script_pubkey) {
return witness_program_sigops(script_pubkey, witness);
}
if flags & 0x01 != 0 && is_pay_to_script_hash(script_pubkey) {
if let Some(redeem) = extract_redeem_script_from_scriptsig(script_sig) {
return witness_program_sigops(&redeem, witness);
}
}
0
}
#[cfg_attr(feature = "spec-lock", spec_locked("11.1", "CountWitnessSigOps"))]
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 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) {
count = count.saturating_add(input_witness_sigops(
utxo.script_pubkey.as_ref(),
&input.script_sig,
witnesses.get(i),
flags,
));
}
}
Ok(count)
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetLegacySigOpCount"))]
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))
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetTransactionSigOpCost"))]
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)
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetTransactionSigOpCost"))]
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 {
for (i, (input, utxo_opt)) in tx.inputs.iter().zip(utxos.iter()).enumerate() {
if let Some(utxo) = utxo_opt {
total_cost = total_cost.saturating_add(input_witness_sigops(
utxo.script_pubkey.as_ref(),
&input.script_sig,
witnesses.get(i),
flags,
));
}
}
}
Ok(total_cost)
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.2.2", "GetTransactionSigOpCost"))]
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)]
#[path = "sigop_tests.rs"]
mod tests;