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