use zcash_script::opcode::PossiblyBad;
use zcash_script::script::Evaluable as _;
use zcash_script::{script, solver, Opcode};
use zebra_chain::{transaction::Transaction, transparent};
pub(super) const MAX_P2SH_SIGOPS: u32 = 15;
pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
pub(super) const MAX_STANDARD_SCRIPTSIG_SIZE: usize = 1650;
pub(super) const MAX_STANDARD_MULTISIG_PUBKEYS: usize = 3;
pub(super) fn standard_script_kind(
lock_script: &transparent::Script,
) -> Option<solver::ScriptKind> {
let code = script::Code(lock_script.as_raw_bytes().to_vec());
let component = code.to_component().ok()?.refine().ok()?;
solver::standard(&component)
}
fn extract_p2sh_redeemed_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
let code = script::Code(unlock_script.as_raw_bytes().to_vec());
let mut last_push_data: Option<Vec<u8>> = None;
for opcode in code.parse() {
if let Ok(PossiblyBad::Good(Opcode::PushValue(pv))) = opcode {
last_push_data = Some(pv.value());
}
}
last_push_data
}
fn count_script_push_ops(script_bytes: &[u8]) -> usize {
let code = script::Code(script_bytes.to_vec());
code.parse()
.filter(|op| matches!(op, Ok(PossiblyBad::Good(Opcode::PushValue(_)))))
.count()
}
fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option<usize> {
match kind {
solver::ScriptKind::PubKey { .. } => Some(1),
solver::ScriptKind::PubKeyHash { .. } => Some(2),
solver::ScriptKind::ScriptHash { .. } => Some(1),
solver::ScriptKind::MultiSig { required, .. } => Some(*required as usize + 1),
solver::ScriptKind::NullData { .. } => None,
}
}
fn p2sh_redeemed_script_sigop_count(
input: &transparent::Input,
spent_output: &transparent::Output,
) -> Option<u32> {
let unlock_script = match input {
transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
transparent::Input::Coinbase { .. } => return None,
};
let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
if !lock_code.is_pay_to_script_hash() {
return None;
}
let redeemed_bytes = extract_p2sh_redeemed_script(unlock_script)?;
let redeemed = script::Code(redeemed_bytes);
Some(redeemed.sig_op_count(true))
}
pub(super) fn p2sh_sigop_count(tx: &Transaction, spent_outputs: &[transparent::Output]) -> u32 {
debug_assert_eq!(
tx.inputs().len(),
spent_outputs.len(),
"spent_outputs must align with transaction inputs"
);
tx.inputs()
.iter()
.zip(spent_outputs.iter())
.filter_map(|(input, spent_output)| p2sh_redeemed_script_sigop_count(input, spent_output))
.sum()
}
pub(super) fn are_inputs_standard(tx: &Transaction, spent_outputs: &[transparent::Output]) -> bool {
debug_assert_eq!(
tx.inputs().len(),
spent_outputs.len(),
"spent_outputs must align with transaction inputs"
);
for (input, spent_output) in tx.inputs().iter().zip(spent_outputs.iter()) {
let unlock_script = match input {
transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
transparent::Input::Coinbase { .. } => continue,
};
let script_kind = match standard_script_kind(&spent_output.lock_script) {
Some(kind) => kind,
None => return false,
};
let mut n_args_expected = match script_sig_args_expected(&script_kind) {
Some(n) => n,
None => return false,
};
let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
if matches!(script_kind, solver::ScriptKind::ScriptHash { .. }) {
let Some(redeemed_bytes) = extract_p2sh_redeemed_script(unlock_script) else {
return false;
};
let redeemed_code = script::Code(redeemed_bytes);
let redeemed_kind = {
let component = redeemed_code
.to_component()
.ok()
.and_then(|c| c.refine().ok());
component.and_then(|c| solver::standard(&c))
};
match redeemed_kind {
Some(ref inner_kind) => {
match script_sig_args_expected(inner_kind) {
Some(inner) => n_args_expected += inner,
None => return false,
}
}
None => {
let sigops = redeemed_code.sig_op_count(true);
if sigops > MAX_P2SH_SIGOPS {
return false;
}
continue;
}
}
}
if stack_size != n_args_expected {
return false;
}
}
true
}
#[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 zebra_chain::{
block::Height,
transaction::{self, LockTime, Transaction},
};
use super::*;
fn multisig_lock_script(required: u8, pubkeys: &[&[u8; 33]]) -> transparent::Script {
let mut s = Vec::new();
s.push(0x50 + required);
for pk in pubkeys {
s.push(0x21); s.extend_from_slice(*pk);
}
s.push(0x50 + pubkeys.len() as u8);
s.push(0xae);
transparent::Script::new(&s)
}
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 count_script_push_ops_counts_pushes() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x00, 0x01, 0xaa, 0x01, 0xbb];
let count = count_script_push_ops(&script_bytes);
assert_eq!(count, 3, "should count 3 push operations");
}
#[test]
fn count_script_push_ops_empty_script() {
let _init_guard = zebra_test::init();
let count = count_script_push_ops(&[]);
assert_eq!(count, 0, "empty script should have 0 push ops");
}
#[test]
fn count_script_push_ops_pushdata1() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x4c, 0x03, 0xaa, 0xbb, 0xcc];
let count = count_script_push_ops(&script_bytes);
assert_eq!(count, 1, "OP_PUSHDATA1 should count as 1 push operation");
}
#[test]
fn count_script_push_ops_pushdata2() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x4d, 0x03, 0x00, 0xaa, 0xbb, 0xcc];
let count = count_script_push_ops(&script_bytes);
assert_eq!(count, 1, "OP_PUSHDATA2 should count as 1 push operation");
}
#[test]
fn count_script_push_ops_pushdata4() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x4e, 0x02, 0x00, 0x00, 0x00, 0xaa, 0xbb];
let count = count_script_push_ops(&script_bytes);
assert_eq!(count, 1, "OP_PUSHDATA4 should count as 1 push operation");
}
#[test]
fn count_script_push_ops_mixed_push_types() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x00, 0x01, 0xaa, 0x4c, 0x01, 0xbb];
let count = count_script_push_ops(&script_bytes);
assert_eq!(
count, 3,
"mixed push types should each count as 1 push operation"
);
}
#[test]
fn count_script_push_ops_truncated_script() {
let _init_guard = zebra_test::init();
let script_bytes = vec![0x0a, 0xaa, 0xbb, 0xcc];
let count = count_script_push_ops(&script_bytes);
assert_eq!(
count, 0,
"truncated script should count 0 successful push operations"
);
}
#[test]
fn extract_p2sh_redeemed_script_extracts_last_push() {
let _init_guard = zebra_test::init();
let unlock_script = transparent::Script::new(&[0x03, 0x61, 0x62, 0x63, 0x02, 0x64, 0x65]);
let redeemed = extract_p2sh_redeemed_script(&unlock_script);
assert_eq!(
redeemed,
Some(vec![0x64, 0x65]),
"should extract the last push data"
);
}
#[test]
fn extract_p2sh_redeemed_script_empty_script() {
let _init_guard = zebra_test::init();
let unlock_script = transparent::Script::new(&[]);
let redeemed = extract_p2sh_redeemed_script(&unlock_script);
assert!(redeemed.is_none(), "empty scriptSig should return None");
}
#[test]
fn script_sig_args_expected_values() {
let _init_guard = zebra_test::init();
let pkh_kind = solver::ScriptKind::PubKeyHash { hash: [0xaa; 20] };
assert_eq!(script_sig_args_expected(&pkh_kind), Some(2));
let sh_kind = solver::ScriptKind::ScriptHash { hash: [0xbb; 20] };
assert_eq!(script_sig_args_expected(&sh_kind), Some(1));
let nd_kind = solver::ScriptKind::NullData { data: vec![] };
assert_eq!(script_sig_args_expected(&nd_kind), None);
let p2pk_script = p2pk_lock_script(&[0x02; 33]);
let p2pk_kind =
standard_script_kind(&p2pk_script).expect("P2PK should be a standard script kind");
assert_eq!(script_sig_args_expected(&p2pk_kind), Some(1));
let ms_script = multisig_lock_script(1, &[&[0x02; 33]]);
let ms_kind = standard_script_kind(&ms_script)
.expect("1-of-1 multisig should be a standard script kind");
assert_eq!(script_sig_args_expected(&ms_kind), Some(2));
}
#[test]
fn are_inputs_standard_accepts_valid_p2pkh() {
let _init_guard = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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"
);
}
}