#[cfg(test)]
use zakura_chain::transparent;
pub(super) use zakura_consensus::transaction::check::{
are_inputs_standard, standard_script_kind, MAX_STANDARD_SCRIPTSIG_SIZE,
};
pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
pub(super) const MAX_STANDARD_MULTISIG_PUBKEYS: usize = 3;
#[cfg(test)]
pub(super) use zakura_script::p2sh_sigop_count;
#[cfg(test)]
pub(super) fn p2pkh_lock_script(hash: &[u8; 20]) -> transparent::Script {
let mut s = vec![0x76, 0xa9, 0x14];
s.extend_from_slice(hash);
s.push(0x88);
s.push(0xac);
transparent::Script::new(&s)
}
#[cfg(test)]
pub(super) fn p2sh_lock_script(hash: &[u8; 20]) -> transparent::Script {
let mut s = vec![0xa9, 0x14];
s.extend_from_slice(hash);
s.push(0x87);
transparent::Script::new(&s)
}
#[cfg(test)]
pub(super) fn p2pk_lock_script(pubkey: &[u8; 33]) -> transparent::Script {
let mut s = Vec::with_capacity(1 + 33 + 1);
s.push(0x21); s.extend_from_slice(pubkey);
s.push(0xac); transparent::Script::new(&s)
}
#[cfg(test)]
mod tests {
use zakura_chain::{
block::Height,
transaction::{self, LockTime, Transaction},
};
use super::*;
fn push_only_script_sig(n_pushes: usize) -> transparent::Script {
let mut bytes = Vec::with_capacity(n_pushes * 2);
for _ in 0..n_pushes {
bytes.push(0x01);
bytes.push(0x42);
}
transparent::Script::new(&bytes)
}
fn p2sh_script_sig(push_items: &[&[u8]]) -> transparent::Script {
let mut bytes = Vec::new();
for item in push_items {
assert!(
item.len() <= 75,
"p2sh_script_sig only supports OP_PUSHBYTES (max 75 bytes), got {}",
item.len()
);
bytes.push(item.len() as u8);
bytes.extend_from_slice(item);
}
transparent::Script::new(&bytes)
}
fn make_v4_tx(
inputs: Vec<transparent::Input>,
outputs: Vec<transparent::Output>,
) -> Transaction {
Transaction::V4 {
inputs,
outputs,
lock_time: LockTime::min_lock_time_timestamp(),
expiry_height: Height(0),
joinsplit_data: None,
sapling_shielded_data: None,
}
}
fn prevout_input(unlock_script: transparent::Script) -> transparent::Input {
transparent::Input::PrevOut {
outpoint: transparent::OutPoint {
hash: transaction::Hash([0xaa; 32]),
index: 0,
},
unlock_script,
sequence: 0xffffffff,
}
}
fn output_with_script(lock_script: transparent::Script) -> transparent::Output {
transparent::Output {
value: 100_000u64.try_into().unwrap(),
lock_script,
}
}
#[test]
fn are_inputs_standard_accepts_valid_p2pkh() {
let _init_guard = zakura_test::init();
let script_sig = push_only_script_sig(2);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
assert!(
are_inputs_standard(&tx, &spent_outputs),
"valid P2PKH input with correct stack depth should be standard"
);
}
#[test]
fn are_inputs_standard_rejects_wrong_stack_depth() {
let _init_guard = zakura_test::init();
let script_sig = push_only_script_sig(3);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
assert!(
!are_inputs_standard(&tx, &spent_outputs),
"P2PKH input with 3 pushes instead of 2 should be non-standard"
);
}
#[test]
fn are_inputs_standard_rejects_too_few_pushes() {
let _init_guard = zakura_test::init();
let script_sig = push_only_script_sig(1);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
assert!(
!are_inputs_standard(&tx, &spent_outputs),
"P2PKH input with 1 push instead of 2 should be non-standard"
);
}
#[test]
fn are_inputs_standard_rejects_non_standard_spent_output() {
let _init_guard = zakura_test::init();
let non_standard_lock = transparent::Script::new(&[0x51, 0x52, 0x93]);
let script_sig = push_only_script_sig(1);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(non_standard_lock)];
assert!(
!are_inputs_standard(&tx, &spent_outputs),
"input spending a non-standard script should be non-standard"
);
}
#[test]
fn are_inputs_standard_accepts_p2sh_with_standard_redeemed_script() {
let _init_guard = zakura_test::init();
let redeemed_script_bytes = {
let mut s = vec![0x76, 0xa9, 0x14];
s.extend_from_slice(&[0xcc; 20]);
s.push(0x88);
s.push(0xac);
s
};
let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
let lock_script = p2sh_lock_script(&[0xdd; 20]);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(lock_script)];
assert!(
are_inputs_standard(&tx, &spent_outputs),
"P2SH input with standard P2PKH redeemed script and correct stack depth should be standard"
);
}
#[test]
fn are_inputs_standard_rejects_p2sh_with_too_many_sigops() {
let _init_guard = zakura_test::init();
let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
let lock_script = p2sh_lock_script(&[0xdd; 20]);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(lock_script)];
assert!(
!are_inputs_standard(&tx, &spent_outputs),
"P2SH input with redeemed script exceeding MAX_P2SH_SIGOPS should be non-standard"
);
}
#[test]
fn are_inputs_standard_accepts_p2sh_with_non_standard_low_sigops() {
let _init_guard = zakura_test::init();
let redeemed_script_bytes: Vec<u8> = vec![0xac; 15];
let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
let lock_script = p2sh_lock_script(&[0xdd; 20]);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(lock_script)];
assert!(
are_inputs_standard(&tx, &spent_outputs),
"P2SH input with non-standard redeemed script at exactly MAX_P2SH_SIGOPS should be accepted"
);
}
#[test]
fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
let _init_guard = zakura_test::init();
let redeemed_script_bytes: Vec<u8> = vec![0xac; 5];
let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
let lock_script = p2sh_lock_script(&[0xdd; 20]);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(lock_script)];
let count = p2sh_sigop_count(&tx, &spent_outputs);
assert_eq!(
count, 5,
"p2sh_sigop_count should return 5 for a redeemed script with 5 OP_CHECKSIG"
);
}
#[test]
fn p2sh_sigop_count_returns_zero_for_non_p2sh() {
let _init_guard = zakura_test::init();
let script_sig = push_only_script_sig(2);
let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
let count = p2sh_sigop_count(&tx, &spent_outputs);
assert_eq!(
count, 0,
"p2sh_sigop_count should return 0 for non-P2SH inputs"
);
}
#[test]
fn p2sh_sigop_count_sums_across_multiple_inputs() {
let _init_guard = zakura_test::init();
let redeemed_1: Vec<u8> = vec![0xac; 3];
let script_sig_1 = p2sh_script_sig(&[&redeemed_1]);
let lock_1 = p2sh_lock_script(&[0xdd; 20]);
let script_sig_2 = push_only_script_sig(2);
let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
let redeemed_3: Vec<u8> = vec![0xac; 7];
let script_sig_3 = p2sh_script_sig(&[&redeemed_3]);
let lock_3 = p2sh_lock_script(&[0xee; 20]);
let tx = make_v4_tx(
vec![
prevout_input(script_sig_1),
prevout_input(script_sig_2),
prevout_input(script_sig_3),
],
vec![],
);
let spent_outputs = vec![
output_with_script(lock_1),
output_with_script(lock_2),
output_with_script(lock_3),
];
let count = p2sh_sigop_count(&tx, &spent_outputs);
assert_eq!(
count, 10,
"p2sh_sigop_count should sum sigops across all P2SH inputs (3 + 0 + 7)"
);
}
#[test]
fn are_inputs_standard_rejects_second_non_standard_input() {
let _init_guard = zakura_test::init();
let script_sig_ok = push_only_script_sig(2);
let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
let script_sig_bad = push_only_script_sig(3);
let lock_bad = p2pkh_lock_script(&[0xbb; 20]);
let tx = make_v4_tx(
vec![prevout_input(script_sig_ok), prevout_input(script_sig_bad)],
vec![],
);
let spent_outputs = vec![output_with_script(lock_ok), output_with_script(lock_bad)];
assert!(
!are_inputs_standard(&tx, &spent_outputs),
"should reject when second input is non-standard even if first is valid"
);
}
}