Skip to main content

zebrad/components/mempool/storage/
policy.rs

1//! Mempool transaction standardness policy checks.
2//!
3//! These functions implement zcashd's mempool policy for rejecting non-standard
4//! transactions. They mirror the logic in zcashd's `IsStandardTx()` and
5//! `AreInputsStandard()`.
6
7use zcash_script::opcode::PossiblyBad;
8use zcash_script::script::Evaluable as _;
9use zcash_script::{script, solver, Opcode};
10use zebra_chain::{transaction::Transaction, transparent};
11
12/// Maximum sigops allowed in a P2SH redeemed script (zcashd `MAX_P2SH_SIGOPS`).
13/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.h#L20>
14pub(super) const MAX_P2SH_SIGOPS: u32 = 15;
15
16/// Maximum number of signature operations allowed per standard transaction (zcashd `MAX_STANDARD_TX_SIGOPS`).
17/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.h#L22>
18pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
19
20/// Maximum size in bytes of a standard transaction's scriptSig (zcashd `MAX_STANDARD_SCRIPTSIG_SIZE`).
21/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L92-L99>
22pub(super) const MAX_STANDARD_SCRIPTSIG_SIZE: usize = 1650;
23
24/// Maximum number of public keys allowed in a standard multisig script.
25/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L46-L48>
26pub(super) const MAX_STANDARD_MULTISIG_PUBKEYS: usize = 3;
27
28/// Classify a script using the `zcash_script` solver.
29///
30/// Returns `Some(kind)` for standard script types, `None` for non-standard.
31pub(super) fn standard_script_kind(
32    lock_script: &transparent::Script,
33) -> Option<solver::ScriptKind> {
34    let code = script::Code(lock_script.as_raw_bytes().to_vec());
35    let component = code.to_component().ok()?.refine().ok()?;
36    solver::standard(&component)
37}
38
39/// Extract the redeemed script bytes from a P2SH scriptSig.
40///
41/// The redeemed script is the last data push in the scriptSig.
42/// Returns `None` if the scriptSig has no push operations.
43///
44/// # Precondition
45///
46/// The scriptSig should be push-only (enforced by `reject_if_non_standard_tx`
47/// before this function is called). Non-push opcodes are silently ignored.
48fn extract_p2sh_redeemed_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
49    let code = script::Code(unlock_script.as_raw_bytes().to_vec());
50    let mut last_push_data: Option<Vec<u8>> = None;
51    for opcode in code.parse() {
52        if let Ok(PossiblyBad::Good(Opcode::PushValue(pv))) = opcode {
53            last_push_data = Some(pv.value());
54        }
55    }
56    last_push_data
57}
58
59/// Count the number of push operations in a script.
60///
61/// For a push-only script (already enforced for mempool scriptSigs),
62/// this equals the stack depth after evaluation.
63fn count_script_push_ops(script_bytes: &[u8]) -> usize {
64    let code = script::Code(script_bytes.to_vec());
65    code.parse()
66        .filter(|op| matches!(op, Ok(PossiblyBad::Good(Opcode::PushValue(_)))))
67        .count()
68}
69
70/// Returns the expected number of scriptSig arguments for a given script kind.
71///
72/// TODO: Consider upstreaming to `zcash_script` crate alongside `ScriptKind::req_sigs()`.
73///
74/// Mirrors zcashd's `ScriptSigArgsExpected()`:
75/// <https://github.com/zcash/zcash/blob/v6.11.0/src/script/standard.cpp#L135>
76///
77/// Returns `None` for non-standard types (TX_NONSTANDARD, TX_NULL_DATA).
78fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option<usize> {
79    match kind {
80        solver::ScriptKind::PubKey { .. } => Some(1),
81        solver::ScriptKind::PubKeyHash { .. } => Some(2),
82        solver::ScriptKind::ScriptHash { .. } => Some(1),
83        solver::ScriptKind::MultiSig { required, .. } => Some(*required as usize + 1),
84        solver::ScriptKind::NullData { .. } => None,
85    }
86}
87
88/// Extracts the P2SH redeemed script's sigop count for a single input.
89///
90/// Returns `Some(count)` for P2SH inputs where a redeemed script was found,
91/// `None` for non-P2SH, coinbase, or P2SH inputs with an empty scriptSig.
92fn p2sh_redeemed_script_sigop_count(
93    input: &transparent::Input,
94    spent_output: &transparent::Output,
95) -> Option<u32> {
96    let unlock_script = match input {
97        transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
98        transparent::Input::Coinbase { .. } => return None,
99    };
100
101    let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
102    if !lock_code.is_pay_to_script_hash() {
103        return None;
104    }
105
106    let redeemed_bytes = extract_p2sh_redeemed_script(unlock_script)?;
107    let redeemed = script::Code(redeemed_bytes);
108    Some(redeemed.sig_op_count(true))
109}
110
111/// Returns the total number of P2SH sigops across all inputs of the transaction.
112///
113/// Mirrors zcashd's `GetP2SHSigOpCount()`:
114/// <https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1191>
115///
116/// # Correctness
117///
118/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
119/// If the lengths differ, `zip()` silently truncates the longer iterator, which may
120/// cause incorrect sigop counts.
121pub(super) fn p2sh_sigop_count(tx: &Transaction, spent_outputs: &[transparent::Output]) -> u32 {
122    debug_assert_eq!(
123        tx.inputs().len(),
124        spent_outputs.len(),
125        "spent_outputs must align with transaction inputs"
126    );
127    tx.inputs()
128        .iter()
129        .zip(spent_outputs.iter())
130        .filter_map(|(input, spent_output)| p2sh_redeemed_script_sigop_count(input, spent_output))
131        .sum()
132}
133
134/// Returns `true` if all transparent inputs are standard.
135///
136/// Mirrors zcashd's `AreInputsStandard()`:
137/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L136>
138///
139/// For each input:
140/// 1. The spent output's scriptPubKey must be a known standard type (via the `zcash_script` solver).
141///    Non-standard scripts and OP_RETURN outputs are rejected.
142/// 2. The scriptSig stack depth must match `ScriptSigArgsExpected()`.
143/// 3. For P2SH inputs:
144///    - If the redeemed script is standard, its expected args are added to the total.
145///    - If the redeemed script is non-standard, it must have <= [`MAX_P2SH_SIGOPS`] sigops.
146///
147/// # Correctness
148///
149/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
150/// If the lengths differ, `zip()` silently truncates the longer iterator, which may
151/// cause incorrect standardness decisions.
152pub(super) fn are_inputs_standard(tx: &Transaction, spent_outputs: &[transparent::Output]) -> bool {
153    debug_assert_eq!(
154        tx.inputs().len(),
155        spent_outputs.len(),
156        "spent_outputs must align with transaction inputs"
157    );
158    for (input, spent_output) in tx.inputs().iter().zip(spent_outputs.iter()) {
159        let unlock_script = match input {
160            transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
161            transparent::Input::Coinbase { .. } => continue,
162        };
163
164        // Step 1: Classify the spent output's scriptPubKey via the zcash_script solver.
165        let script_kind = match standard_script_kind(&spent_output.lock_script) {
166            Some(kind) => kind,
167            None => return false,
168        };
169
170        // Step 2: Get expected number of scriptSig arguments.
171        // Returns None for TX_NONSTANDARD and TX_NULL_DATA.
172        let mut n_args_expected = match script_sig_args_expected(&script_kind) {
173            Some(n) => n,
174            None => return false,
175        };
176
177        // Step 3: Count actual push operations in scriptSig.
178        // For push-only scripts (enforced by reject_if_non_standard_tx), this equals the stack depth.
179        let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
180
181        // Step 4: P2SH-specific checks.
182        if matches!(script_kind, solver::ScriptKind::ScriptHash { .. }) {
183            let Some(redeemed_bytes) = extract_p2sh_redeemed_script(unlock_script) else {
184                return false;
185            };
186
187            let redeemed_code = script::Code(redeemed_bytes);
188
189            // Classify the redeemed script using the zcash_script solver.
190            let redeemed_kind = {
191                let component = redeemed_code
192                    .to_component()
193                    .ok()
194                    .and_then(|c| c.refine().ok());
195                component.and_then(|c| solver::standard(&c))
196            };
197
198            match redeemed_kind {
199                Some(ref inner_kind) => {
200                    // Standard redeemed script: add its expected args.
201                    match script_sig_args_expected(inner_kind) {
202                        Some(inner) => n_args_expected += inner,
203                        None => return false,
204                    }
205                }
206                None => {
207                    // Non-standard redeemed script: accept if sigops <= limit.
208                    // Matches zcashd: "Any other Script with less than 15 sigops OK:
209                    // ... extra data left on the stack after execution is OK, too"
210                    let sigops = redeemed_code.sig_op_count(true);
211                    if sigops > MAX_P2SH_SIGOPS {
212                        return false;
213                    }
214
215                    // This input is acceptable; move on to the next input.
216                    continue;
217                }
218            }
219        }
220
221        // Step 5: Reject if scriptSig has wrong number of stack items.
222        if stack_size != n_args_expected {
223            return false;
224        }
225    }
226    true
227}
228
229// -- Test helper functions shared across test modules --
230
231/// Build a P2PKH lock script: OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG
232#[cfg(test)]
233pub(super) fn p2pkh_lock_script(hash: &[u8; 20]) -> transparent::Script {
234    let mut s = vec![0x76, 0xa9, 0x14];
235    s.extend_from_slice(hash);
236    s.push(0x88);
237    s.push(0xac);
238    transparent::Script::new(&s)
239}
240
241/// Build a P2SH lock script: OP_HASH160 <20-byte hash> OP_EQUAL
242#[cfg(test)]
243pub(super) fn p2sh_lock_script(hash: &[u8; 20]) -> transparent::Script {
244    let mut s = vec![0xa9, 0x14];
245    s.extend_from_slice(hash);
246    s.push(0x87);
247    transparent::Script::new(&s)
248}
249
250/// Build a P2PK lock script: <compressed_pubkey> OP_CHECKSIG
251#[cfg(test)]
252pub(super) fn p2pk_lock_script(pubkey: &[u8; 33]) -> transparent::Script {
253    let mut s = Vec::with_capacity(1 + 33 + 1);
254    s.push(0x21); // OP_PUSHBYTES_33
255    s.extend_from_slice(pubkey);
256    s.push(0xac); // OP_CHECKSIG
257    transparent::Script::new(&s)
258}
259
260#[cfg(test)]
261mod tests {
262    use zebra_chain::{
263        block::Height,
264        transaction::{self, LockTime, Transaction},
265    };
266
267    use super::*;
268
269    // -- Helper functions --
270
271    /// Build a bare multisig lock script: OP_<required> <pubkeys...> OP_<total> OP_CHECKMULTISIG
272    fn multisig_lock_script(required: u8, pubkeys: &[&[u8; 33]]) -> transparent::Script {
273        let mut s = Vec::new();
274        // OP_1 through OP_16 are 0x51 through 0x60
275        s.push(0x50 + required);
276        for pk in pubkeys {
277            s.push(0x21); // OP_PUSHBYTES_33
278            s.extend_from_slice(*pk);
279        }
280        // OP_N for total pubkeys, safe because pubkeys.len() <= 3 for standard
281        s.push(0x50 + pubkeys.len() as u8);
282        // OP_CHECKMULTISIG
283        s.push(0xae);
284        transparent::Script::new(&s)
285    }
286
287    /// Build a scriptSig with the specified number of push operations.
288    /// Each push is a 1-byte constant value.
289    fn push_only_script_sig(n_pushes: usize) -> transparent::Script {
290        let mut bytes = Vec::with_capacity(n_pushes * 2);
291        for _ in 0..n_pushes {
292            // OP_PUSHBYTES_1 <byte>
293            bytes.push(0x01);
294            bytes.push(0x42);
295        }
296        transparent::Script::new(&bytes)
297    }
298
299    /// Build a P2SH scriptSig from a list of push data items.
300    /// Each item is pushed as a single OP_PUSHBYTES data push (max 75 bytes).
301    /// The last item should be the redeemed script.
302    fn p2sh_script_sig(push_items: &[&[u8]]) -> transparent::Script {
303        let mut bytes = Vec::new();
304        for item in push_items {
305            assert!(
306                item.len() <= 75,
307                "p2sh_script_sig only supports OP_PUSHBYTES (max 75 bytes), got {}",
308                item.len()
309            );
310            // OP_PUSHBYTES_N where N = item.len(), safe because len <= 75 < 256
311            bytes.push(item.len() as u8);
312            bytes.extend_from_slice(item);
313        }
314        transparent::Script::new(&bytes)
315    }
316
317    /// Build a simple V4 transaction with the given transparent inputs and outputs.
318    fn make_v4_tx(
319        inputs: Vec<transparent::Input>,
320        outputs: Vec<transparent::Output>,
321    ) -> Transaction {
322        Transaction::V4 {
323            inputs,
324            outputs,
325            lock_time: LockTime::min_lock_time_timestamp(),
326            expiry_height: Height(0),
327            joinsplit_data: None,
328            sapling_shielded_data: None,
329        }
330    }
331
332    /// Build a PrevOut input with the given unlock script.
333    fn prevout_input(unlock_script: transparent::Script) -> transparent::Input {
334        transparent::Input::PrevOut {
335            outpoint: transparent::OutPoint {
336                hash: transaction::Hash([0xaa; 32]),
337                index: 0,
338            },
339            unlock_script,
340            sequence: 0xffffffff,
341        }
342    }
343
344    /// Build a transparent output with the given lock script.
345    /// Uses a non-dust value to avoid false positives in standardness checks.
346    fn output_with_script(lock_script: transparent::Script) -> transparent::Output {
347        transparent::Output {
348            value: 100_000u64.try_into().unwrap(),
349            lock_script,
350        }
351    }
352
353    // -- count_script_push_ops tests --
354
355    #[test]
356    fn count_script_push_ops_counts_pushes() {
357        let _init_guard = zebra_test::init();
358        // Script with 3 push operations: OP_0 <push 1 byte> <push 1 byte>
359        let script_bytes = vec![0x00, 0x01, 0xaa, 0x01, 0xbb];
360        let count = count_script_push_ops(&script_bytes);
361        assert_eq!(count, 3, "should count 3 push operations");
362    }
363
364    #[test]
365    fn count_script_push_ops_empty_script() {
366        let _init_guard = zebra_test::init();
367        let count = count_script_push_ops(&[]);
368        assert_eq!(count, 0, "empty script should have 0 push ops");
369    }
370
371    #[test]
372    fn count_script_push_ops_pushdata1() {
373        let _init_guard = zebra_test::init();
374        // OP_PUSHDATA1 <length=3> <3 bytes of data>
375        let script_bytes = vec![0x4c, 0x03, 0xaa, 0xbb, 0xcc];
376        let count = count_script_push_ops(&script_bytes);
377        assert_eq!(count, 1, "OP_PUSHDATA1 should count as 1 push operation");
378    }
379
380    #[test]
381    fn count_script_push_ops_pushdata2() {
382        let _init_guard = zebra_test::init();
383        // OP_PUSHDATA2 <length=3 as 2 little-endian bytes> <3 bytes of data>
384        let script_bytes = vec![0x4d, 0x03, 0x00, 0xaa, 0xbb, 0xcc];
385        let count = count_script_push_ops(&script_bytes);
386        assert_eq!(count, 1, "OP_PUSHDATA2 should count as 1 push operation");
387    }
388
389    #[test]
390    fn count_script_push_ops_pushdata4() {
391        let _init_guard = zebra_test::init();
392        // OP_PUSHDATA4 <length=2 as 4 little-endian bytes> <2 bytes of data>
393        let script_bytes = vec![0x4e, 0x02, 0x00, 0x00, 0x00, 0xaa, 0xbb];
394        let count = count_script_push_ops(&script_bytes);
395        assert_eq!(count, 1, "OP_PUSHDATA4 should count as 1 push operation");
396    }
397
398    #[test]
399    fn count_script_push_ops_mixed_push_types() {
400        let _init_guard = zebra_test::init();
401        // OP_0, then OP_PUSHBYTES_1 <byte>, then OP_PUSHDATA1 <len=1> <byte>
402        let script_bytes = vec![0x00, 0x01, 0xaa, 0x4c, 0x01, 0xbb];
403        let count = count_script_push_ops(&script_bytes);
404        assert_eq!(
405            count, 3,
406            "mixed push types should each count as 1 push operation"
407        );
408    }
409
410    #[test]
411    fn count_script_push_ops_truncated_script() {
412        let _init_guard = zebra_test::init();
413        // OP_PUSHBYTES_10 followed by only 3 bytes (truncated) -- parser should
414        // produce an error for the incomplete push, which is filtered out.
415        let script_bytes = vec![0x0a, 0xaa, 0xbb, 0xcc];
416        let count = count_script_push_ops(&script_bytes);
417        assert_eq!(
418            count, 0,
419            "truncated script should count 0 successful push operations"
420        );
421    }
422
423    // -- extract_p2sh_redeemed_script tests --
424
425    #[test]
426    fn extract_p2sh_redeemed_script_extracts_last_push() {
427        let _init_guard = zebra_test::init();
428        // scriptSig: <sig> <redeemed_script>
429        // OP_PUSHDATA with 3 bytes "abc" then OP_PUSHDATA with 2 bytes "de"
430        let unlock_script = transparent::Script::new(&[0x03, 0x61, 0x62, 0x63, 0x02, 0x64, 0x65]);
431        let redeemed = extract_p2sh_redeemed_script(&unlock_script);
432        assert_eq!(
433            redeemed,
434            Some(vec![0x64, 0x65]),
435            "should extract the last push data"
436        );
437    }
438
439    #[test]
440    fn extract_p2sh_redeemed_script_empty_script() {
441        let _init_guard = zebra_test::init();
442        let unlock_script = transparent::Script::new(&[]);
443        let redeemed = extract_p2sh_redeemed_script(&unlock_script);
444        assert!(redeemed.is_none(), "empty scriptSig should return None");
445    }
446
447    // -- script_sig_args_expected tests --
448
449    #[test]
450    fn script_sig_args_expected_values() {
451        let _init_guard = zebra_test::init();
452
453        // P2PKH expects 2 args (sig + pubkey)
454        let pkh_kind = solver::ScriptKind::PubKeyHash { hash: [0xaa; 20] };
455        assert_eq!(script_sig_args_expected(&pkh_kind), Some(2));
456
457        // P2SH expects 1 arg (the redeemed script)
458        let sh_kind = solver::ScriptKind::ScriptHash { hash: [0xbb; 20] };
459        assert_eq!(script_sig_args_expected(&sh_kind), Some(1));
460
461        // NullData expects None (non-standard to spend)
462        let nd_kind = solver::ScriptKind::NullData { data: vec![] };
463        assert_eq!(script_sig_args_expected(&nd_kind), None);
464
465        // P2PK expects 1 arg (sig only) -- test via script classification
466        let p2pk_script = p2pk_lock_script(&[0x02; 33]);
467        let p2pk_kind =
468            standard_script_kind(&p2pk_script).expect("P2PK should be a standard script kind");
469        assert_eq!(script_sig_args_expected(&p2pk_kind), Some(1));
470
471        // MultiSig 1-of-1 expects 2 args (OP_0 + 1 sig) -- test via script classification
472        let ms_script = multisig_lock_script(1, &[&[0x02; 33]]);
473        let ms_kind = standard_script_kind(&ms_script)
474            .expect("1-of-1 multisig should be a standard script kind");
475        assert_eq!(script_sig_args_expected(&ms_kind), Some(2));
476    }
477
478    // -- are_inputs_standard tests --
479
480    #[test]
481    fn are_inputs_standard_accepts_valid_p2pkh() {
482        let _init_guard = zebra_test::init();
483
484        // P2PKH expects 2 scriptSig pushes: <sig> <pubkey>
485        let script_sig = push_only_script_sig(2);
486        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
487        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
488
489        assert!(
490            are_inputs_standard(&tx, &spent_outputs),
491            "valid P2PKH input with correct stack depth should be standard"
492        );
493    }
494
495    #[test]
496    fn are_inputs_standard_rejects_wrong_stack_depth() {
497        let _init_guard = zebra_test::init();
498
499        // P2PKH expects 2 pushes, but we provide 3
500        let script_sig = push_only_script_sig(3);
501        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
502        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
503
504        assert!(
505            !are_inputs_standard(&tx, &spent_outputs),
506            "P2PKH input with 3 pushes instead of 2 should be non-standard"
507        );
508    }
509
510    #[test]
511    fn are_inputs_standard_rejects_too_few_pushes() {
512        let _init_guard = zebra_test::init();
513
514        // P2PKH expects 2 pushes, but we provide 1
515        let script_sig = push_only_script_sig(1);
516        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
517        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
518
519        assert!(
520            !are_inputs_standard(&tx, &spent_outputs),
521            "P2PKH input with 1 push instead of 2 should be non-standard"
522        );
523    }
524
525    #[test]
526    fn are_inputs_standard_rejects_non_standard_spent_output() {
527        let _init_guard = zebra_test::init();
528
529        // OP_1 OP_2 OP_ADD -- not a recognized standard script type
530        let non_standard_lock = transparent::Script::new(&[0x51, 0x52, 0x93]);
531        let script_sig = push_only_script_sig(1);
532        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
533        let spent_outputs = vec![output_with_script(non_standard_lock)];
534
535        assert!(
536            !are_inputs_standard(&tx, &spent_outputs),
537            "input spending a non-standard script should be non-standard"
538        );
539    }
540
541    #[test]
542    fn are_inputs_standard_accepts_p2sh_with_standard_redeemed_script() {
543        let _init_guard = zebra_test::init();
544
545        // Build a P2SH input where the redeemed script is a P2PKH script.
546        // The redeemed script itself is the serialized P2PKH:
547        //   OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
548        let redeemed_script_bytes = {
549            let mut s = vec![0x76, 0xa9, 0x14];
550            s.extend_from_slice(&[0xcc; 20]);
551            s.push(0x88);
552            s.push(0xac);
553            s
554        };
555
556        // For P2SH with a P2PKH redeemed script:
557        //   script_sig_args_expected(ScriptHash) = 1  (the redeemed script push)
558        //   script_sig_args_expected(PubKeyHash) = 2  (sig + pubkey inside redeemed)
559        //   total expected = 1 + 2 = 3
560        //
561        // scriptSig: <sig_placeholder> <pubkey_placeholder> <redeemed_script>
562        let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
563
564        // The policy check uses is_pay_to_script_hash() which only checks the
565        // script pattern (OP_HASH160 <20 bytes> OP_EQUAL), not the hash value.
566        // Any 20-byte hash works for testing the policy logic.
567        let lock_script = p2sh_lock_script(&[0xdd; 20]);
568        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
569        let spent_outputs = vec![output_with_script(lock_script)];
570
571        assert!(
572            are_inputs_standard(&tx, &spent_outputs),
573            "P2SH input with standard P2PKH redeemed script and correct stack depth should be standard"
574        );
575    }
576
577    #[test]
578    fn are_inputs_standard_rejects_p2sh_with_too_many_sigops() {
579        let _init_guard = zebra_test::init();
580
581        // Build a redeemed script that has more than MAX_P2SH_SIGOPS (15) sigops.
582        // Use 16 consecutive OP_CHECKSIG (0xac) opcodes.
583        let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
584
585        // scriptSig: just push the redeemed script (1 push)
586        // Since the redeemed script is non-standard, are_inputs_standard
587        // checks sigops. With 16 > MAX_P2SH_SIGOPS (15), it should reject.
588        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
589
590        let lock_script = p2sh_lock_script(&[0xdd; 20]);
591        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
592        let spent_outputs = vec![output_with_script(lock_script)];
593
594        assert!(
595            !are_inputs_standard(&tx, &spent_outputs),
596            "P2SH input with redeemed script exceeding MAX_P2SH_SIGOPS should be non-standard"
597        );
598    }
599
600    #[test]
601    fn are_inputs_standard_accepts_p2sh_with_non_standard_low_sigops() {
602        let _init_guard = zebra_test::init();
603
604        // Build a redeemed script that is non-standard but has <= MAX_P2SH_SIGOPS (15).
605        // Use exactly 15 OP_CHECKSIG (0xac) opcodes -- should be accepted.
606        let redeemed_script_bytes: Vec<u8> = vec![0xac; 15];
607
608        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
609
610        let lock_script = p2sh_lock_script(&[0xdd; 20]);
611        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
612        let spent_outputs = vec![output_with_script(lock_script)];
613
614        assert!(
615            are_inputs_standard(&tx, &spent_outputs),
616            "P2SH input with non-standard redeemed script at exactly MAX_P2SH_SIGOPS should be accepted"
617        );
618    }
619
620    // -- p2sh_sigop_count tests --
621
622    #[test]
623    fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
624        let _init_guard = zebra_test::init();
625
626        // Build a P2SH input whose redeemed script has 5 OP_CHECKSIG opcodes.
627        let redeemed_script_bytes: Vec<u8> = vec![0xac; 5];
628
629        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
630
631        let lock_script = p2sh_lock_script(&[0xdd; 20]);
632        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
633        let spent_outputs = vec![output_with_script(lock_script)];
634
635        let count = p2sh_sigop_count(&tx, &spent_outputs);
636        assert_eq!(
637            count, 5,
638            "p2sh_sigop_count should return 5 for a redeemed script with 5 OP_CHECKSIG"
639        );
640    }
641
642    #[test]
643    fn p2sh_sigop_count_returns_zero_for_non_p2sh() {
644        let _init_guard = zebra_test::init();
645
646        // P2PKH spent output -- not P2SH, so p2sh_sigop_count should return 0.
647        let script_sig = push_only_script_sig(2);
648        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
649        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
650
651        let count = p2sh_sigop_count(&tx, &spent_outputs);
652        assert_eq!(
653            count, 0,
654            "p2sh_sigop_count should return 0 for non-P2SH inputs"
655        );
656    }
657
658    #[test]
659    fn p2sh_sigop_count_sums_across_multiple_inputs() {
660        let _init_guard = zebra_test::init();
661
662        // Input 0: P2SH with redeemed script having 3 OP_CHECKSIG
663        let redeemed_1: Vec<u8> = vec![0xac; 3];
664        let script_sig_1 = p2sh_script_sig(&[&redeemed_1]);
665        let lock_1 = p2sh_lock_script(&[0xdd; 20]);
666
667        // Input 1: P2PKH (non-P2SH, contributes 0)
668        let script_sig_2 = push_only_script_sig(2);
669        let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
670
671        // Input 2: P2SH with redeemed script having 7 OP_CHECKSIG
672        let redeemed_3: Vec<u8> = vec![0xac; 7];
673        let script_sig_3 = p2sh_script_sig(&[&redeemed_3]);
674        let lock_3 = p2sh_lock_script(&[0xee; 20]);
675
676        let tx = make_v4_tx(
677            vec![
678                prevout_input(script_sig_1),
679                prevout_input(script_sig_2),
680                prevout_input(script_sig_3),
681            ],
682            vec![],
683        );
684        let spent_outputs = vec![
685            output_with_script(lock_1),
686            output_with_script(lock_2),
687            output_with_script(lock_3),
688        ];
689
690        let count = p2sh_sigop_count(&tx, &spent_outputs);
691        assert_eq!(
692            count, 10,
693            "p2sh_sigop_count should sum sigops across all P2SH inputs (3 + 0 + 7)"
694        );
695    }
696
697    #[test]
698    fn are_inputs_standard_rejects_second_non_standard_input() {
699        let _init_guard = zebra_test::init();
700
701        // Input 0: valid P2PKH (2 pushes)
702        let script_sig_ok = push_only_script_sig(2);
703        let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
704
705        // Input 1: P2PKH with wrong stack depth (3 pushes instead of 2)
706        let script_sig_bad = push_only_script_sig(3);
707        let lock_bad = p2pkh_lock_script(&[0xbb; 20]);
708
709        let tx = make_v4_tx(
710            vec![prevout_input(script_sig_ok), prevout_input(script_sig_bad)],
711            vec![],
712        );
713        let spent_outputs = vec![output_with_script(lock_ok), output_with_script(lock_bad)];
714
715        assert!(
716            !are_inputs_standard(&tx, &spent_outputs),
717            "should reject when second input is non-standard even if first is valid"
718        );
719    }
720}