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", "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
}
#[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
}
#[spec_locked("5.2.1", "IsP2SH")]
#[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 }
#[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
}
#[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
}
#[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
}
#[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)
}
#[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))
}
#[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)
}
#[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)
}
#[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)]
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],
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(),
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(&[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],
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(),
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"
);
}
}
fn p2sh_script_pubkey() -> Vec<u8> {
let mut script = vec![OP_HASH160, 0x14];
script.extend_from_slice(&[0x11; 20]);
script.push(OP_EQUAL);
script
}
fn direct_push(payload: &[u8]) -> Vec<u8> {
let mut script = vec![payload.len() as u8];
script.extend_from_slice(payload);
script
}
fn spend_cost(
script_pubkey: Vec<u8>,
script_sig: Vec<u8>,
witness: Witness,
flags: u32,
) -> u64 {
let prev = OutPoint {
hash: [3u8; 32],
index: 1,
};
let utxo = UTXO {
value: 50_000,
script_pubkey: script_pubkey.into(),
height: 500_000,
is_coinbase: false,
};
let mut set: UtxoSet = Default::default();
crate::utxo_set_insert(&mut set, prev, utxo);
let tx = Transaction {
version: 2,
inputs: vec![TransactionInput {
prevout: prev,
script_sig,
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 10_000,
script_pubkey: vec![OP_1],
}]
.into(),
lock_time: 0,
};
let witnesses = vec![witness];
let with_slices =
get_transaction_sigop_cost_with_witness_slices(&tx, &set, Some(&witnesses), flags)
.unwrap();
let uref = set.get(&prev).map(|a| a.as_ref());
let with_utxos =
get_transaction_sigop_cost_with_utxos(&tx, &[uref], Some(&witnesses), flags).unwrap();
assert_eq!(with_utxos, with_slices);
with_slices
}
#[test]
fn nested_v0_witness_sigops_count_toward_block_cost() {
let flags = 0x01 | 0x800;
let mut p2wpkh = vec![OP_0, 0x14];
p2wpkh.extend_from_slice(&[0x22; 20]);
assert_eq!(
spend_cost(p2sh_script_pubkey(), direct_push(&p2wpkh), vec![], flags),
1,
"nested 20-byte program costs 1 even with an empty witness"
);
let mut p2wsh = vec![OP_0, 0x20];
p2wsh.extend_from_slice(&[0x33; 32]);
let witness_script = vec![OP_CHECKSIG, OP_CHECKSIG];
assert_eq!(
spend_cost(
p2sh_script_pubkey(),
direct_push(&p2wsh),
vec![witness_script],
flags,
),
2,
"nested 32-byte program costs the witness script's sigops"
);
assert_eq!(
spend_cost(p2sh_script_pubkey(), direct_push(&p2wsh), vec![], flags),
0,
"nested 32-byte program with an empty witness costs 0"
);
let mut native = vec![OP_0, 0x14];
native.extend_from_slice(&[0x44; 20]);
assert_eq!(
spend_cost(native, vec![], vec![], flags),
1,
"native 20-byte program costs 1 even with an empty witness"
);
}
#[test]
fn p2sh_sigops_keep_the_redeem_after_small_pushes() {
let flags = 0x01 | 0x800;
let redeem = vec![OP_CHECKSIG];
let plain = spend_cost(p2sh_script_pubkey(), direct_push(&redeem), vec![], flags);
assert_eq!(plain, 4, "one redeem CHECKSIG costs 4");
for prefix in [OP_1NEGATE, OP_RESERVED, OP_1] {
let mut script_sig = vec![prefix];
script_sig.extend(direct_push(&redeem));
assert_eq!(
spend_cost(p2sh_script_pubkey(), script_sig, vec![], flags),
4,
"prefix 0x{prefix:02x} is a push and must not drop the redeem sigop"
);
}
assert_eq!(
spend_cost(p2sh_script_pubkey(), vec![OP_1NEGATE], vec![], flags),
0,
"a lone OP_1NEGATE has an empty immediate and costs 0"
);
let mut then_non_push = direct_push(&redeem);
then_non_push.push(OP_DUP);
assert_eq!(
spend_cost(p2sh_script_pubkey(), then_non_push, vec![], flags),
0,
"an opcode above OP_16 drops the P2SH sigop count"
);
let mut p2wpkh = vec![OP_0, 0x14];
p2wpkh.extend_from_slice(&[0x22; 20]);
for prefix in [OP_1NEGATE, OP_RESERVED] {
let mut script_sig = vec![prefix];
script_sig.extend(direct_push(&p2wpkh));
assert_eq!(
spend_cost(p2sh_script_pubkey(), script_sig, vec![], flags),
1,
"prefix 0x{prefix:02x} must still count a nested 20-byte program"
);
}
}
}