Skip to main content

zebrad/components/mempool/storage/
policy.rs

1//! Mempool transaction standardness policy constants and helpers.
2//!
3//! These mirror zcashd's mempool policy for rejecting non-standard transactions
4//! (`IsStandardTx()` and `AreInputsStandard()`). The transparent-input checks now live in
5//! `zebra-consensus`, where the transaction verifier applies them before script verification;
6//! they are re-exported here and used by the storage-time policy in the parent module.
7
8#[cfg(test)]
9use zebra_chain::transparent;
10
11// The transparent-input standardness checks (`AreInputsStandard()` and the spent-output
12// classifier) live in `zebra-consensus`, where the transaction verifier also applies them to
13// mempool transactions *before* script verification (`check::mempool_standard_input_scripts`).
14// They are re-exported here for the storage-time policy checks, so the two paths can't drift apart.
15pub(super) use zebra_consensus::transaction::check::{
16    are_inputs_standard, standard_script_kind, MAX_STANDARD_SCRIPTSIG_SIZE,
17};
18
19/// Maximum number of signature operations allowed per standard transaction (zcashd `MAX_STANDARD_TX_SIGOPS`).
20/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.h#L22>
21pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
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#[cfg(test)]
28pub(super) use zebra_script::p2sh_sigop_count;
29
30// -- Test helper functions shared across test modules --
31
32/// Build a P2PKH lock script: OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG
33#[cfg(test)]
34pub(super) fn p2pkh_lock_script(hash: &[u8; 20]) -> transparent::Script {
35    let mut s = vec![0x76, 0xa9, 0x14];
36    s.extend_from_slice(hash);
37    s.push(0x88);
38    s.push(0xac);
39    transparent::Script::new(&s)
40}
41
42/// Build a P2SH lock script: OP_HASH160 <20-byte hash> OP_EQUAL
43#[cfg(test)]
44pub(super) fn p2sh_lock_script(hash: &[u8; 20]) -> transparent::Script {
45    let mut s = vec![0xa9, 0x14];
46    s.extend_from_slice(hash);
47    s.push(0x87);
48    transparent::Script::new(&s)
49}
50
51/// Build a P2PK lock script: <compressed_pubkey> OP_CHECKSIG
52#[cfg(test)]
53pub(super) fn p2pk_lock_script(pubkey: &[u8; 33]) -> transparent::Script {
54    let mut s = Vec::with_capacity(1 + 33 + 1);
55    s.push(0x21); // OP_PUSHBYTES_33
56    s.extend_from_slice(pubkey);
57    s.push(0xac); // OP_CHECKSIG
58    transparent::Script::new(&s)
59}
60
61#[cfg(test)]
62mod tests {
63    use zebra_chain::{
64        block::Height,
65        transaction::{self, LockTime, Transaction},
66    };
67
68    use super::*;
69
70    // -- Helper functions --
71
72    /// Build a scriptSig with the specified number of push operations.
73    /// Each push is a 1-byte constant value.
74    fn push_only_script_sig(n_pushes: usize) -> transparent::Script {
75        let mut bytes = Vec::with_capacity(n_pushes * 2);
76        for _ in 0..n_pushes {
77            // OP_PUSHBYTES_1 <byte>
78            bytes.push(0x01);
79            bytes.push(0x42);
80        }
81        transparent::Script::new(&bytes)
82    }
83
84    /// Build a P2SH scriptSig from a list of push data items.
85    /// Each item is pushed as a single OP_PUSHBYTES data push (max 75 bytes).
86    /// The last item should be the redeemed script.
87    fn p2sh_script_sig(push_items: &[&[u8]]) -> transparent::Script {
88        let mut bytes = Vec::new();
89        for item in push_items {
90            assert!(
91                item.len() <= 75,
92                "p2sh_script_sig only supports OP_PUSHBYTES (max 75 bytes), got {}",
93                item.len()
94            );
95            // OP_PUSHBYTES_N where N = item.len(), safe because len <= 75 < 256
96            bytes.push(item.len() as u8);
97            bytes.extend_from_slice(item);
98        }
99        transparent::Script::new(&bytes)
100    }
101
102    /// Build a simple V4 transaction with the given transparent inputs and outputs.
103    fn make_v4_tx(
104        inputs: Vec<transparent::Input>,
105        outputs: Vec<transparent::Output>,
106    ) -> Transaction {
107        Transaction::test_v4(
108            inputs,
109            outputs,
110            LockTime::min_lock_time_timestamp(),
111            Height(0),
112        )
113    }
114
115    /// Build a PrevOut input with the given unlock script.
116    fn prevout_input(unlock_script: transparent::Script) -> transparent::Input {
117        transparent::Input::PrevOut {
118            outpoint: transparent::OutPoint {
119                hash: transaction::Hash([0xaa; 32]),
120                index: 0,
121            },
122            unlock_script,
123            sequence: 0xffffffff,
124        }
125    }
126
127    /// Build a transparent output with the given lock script.
128    /// Uses a non-dust value to avoid false positives in standardness checks.
129    fn output_with_script(lock_script: transparent::Script) -> transparent::Output {
130        transparent::Output {
131            value: 100_000u64.try_into().unwrap(),
132            lock_script,
133        }
134    }
135
136    // -- are_inputs_standard tests --
137
138    #[test]
139    fn are_inputs_standard_accepts_valid_p2pkh() {
140        let _init_guard = zebra_test::init();
141
142        // P2PKH expects 2 scriptSig pushes: <sig> <pubkey>
143        let script_sig = push_only_script_sig(2);
144        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
145        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
146
147        assert!(
148            are_inputs_standard(&tx, &spent_outputs),
149            "valid P2PKH input with correct stack depth should be standard"
150        );
151    }
152
153    #[test]
154    fn are_inputs_standard_rejects_wrong_stack_depth() {
155        let _init_guard = zebra_test::init();
156
157        // P2PKH expects 2 pushes, but we provide 3
158        let script_sig = push_only_script_sig(3);
159        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
160        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
161
162        assert!(
163            !are_inputs_standard(&tx, &spent_outputs),
164            "P2PKH input with 3 pushes instead of 2 should be non-standard"
165        );
166    }
167
168    #[test]
169    fn are_inputs_standard_rejects_too_few_pushes() {
170        let _init_guard = zebra_test::init();
171
172        // P2PKH expects 2 pushes, but we provide 1
173        let script_sig = push_only_script_sig(1);
174        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
175        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
176
177        assert!(
178            !are_inputs_standard(&tx, &spent_outputs),
179            "P2PKH input with 1 push instead of 2 should be non-standard"
180        );
181    }
182
183    #[test]
184    fn are_inputs_standard_rejects_non_standard_spent_output() {
185        let _init_guard = zebra_test::init();
186
187        // OP_1 OP_2 OP_ADD -- not a recognized standard script type
188        let non_standard_lock = transparent::Script::new(&[0x51, 0x52, 0x93]);
189        let script_sig = push_only_script_sig(1);
190        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
191        let spent_outputs = vec![output_with_script(non_standard_lock)];
192
193        assert!(
194            !are_inputs_standard(&tx, &spent_outputs),
195            "input spending a non-standard script should be non-standard"
196        );
197    }
198
199    #[test]
200    fn are_inputs_standard_accepts_p2sh_with_standard_redeemed_script() {
201        let _init_guard = zebra_test::init();
202
203        // Build a P2SH input where the redeemed script is a P2PKH script.
204        // The redeemed script itself is the serialized P2PKH:
205        //   OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
206        let redeemed_script_bytes = {
207            let mut s = vec![0x76, 0xa9, 0x14];
208            s.extend_from_slice(&[0xcc; 20]);
209            s.push(0x88);
210            s.push(0xac);
211            s
212        };
213
214        // For P2SH with a P2PKH redeemed script:
215        //   script_sig_args_expected(ScriptHash) = 1  (the redeemed script push)
216        //   script_sig_args_expected(PubKeyHash) = 2  (sig + pubkey inside redeemed)
217        //   total expected = 1 + 2 = 3
218        //
219        // scriptSig: <sig_placeholder> <pubkey_placeholder> <redeemed_script>
220        let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
221
222        // The policy check uses is_pay_to_script_hash() which only checks the
223        // script pattern (OP_HASH160 <20 bytes> OP_EQUAL), not the hash value.
224        // Any 20-byte hash works for testing the policy logic.
225        let lock_script = p2sh_lock_script(&[0xdd; 20]);
226        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
227        let spent_outputs = vec![output_with_script(lock_script)];
228
229        assert!(
230            are_inputs_standard(&tx, &spent_outputs),
231            "P2SH input with standard P2PKH redeemed script and correct stack depth should be standard"
232        );
233    }
234
235    #[test]
236    fn are_inputs_standard_rejects_p2sh_with_too_many_sigops() {
237        let _init_guard = zebra_test::init();
238
239        // Build a redeemed script that has more than MAX_P2SH_SIGOPS (15) sigops.
240        // Use 16 consecutive OP_CHECKSIG (0xac) opcodes.
241        let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
242
243        // scriptSig: just push the redeemed script (1 push)
244        // Since the redeemed script is non-standard, are_inputs_standard
245        // checks sigops. With 16 > MAX_P2SH_SIGOPS (15), it should reject.
246        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
247
248        let lock_script = p2sh_lock_script(&[0xdd; 20]);
249        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
250        let spent_outputs = vec![output_with_script(lock_script)];
251
252        assert!(
253            !are_inputs_standard(&tx, &spent_outputs),
254            "P2SH input with redeemed script exceeding MAX_P2SH_SIGOPS should be non-standard"
255        );
256    }
257
258    #[test]
259    fn are_inputs_standard_accepts_p2sh_with_non_standard_low_sigops() {
260        let _init_guard = zebra_test::init();
261
262        // Build a redeemed script that is non-standard but has <= MAX_P2SH_SIGOPS (15).
263        // Use exactly 15 OP_CHECKSIG (0xac) opcodes -- should be accepted.
264        let redeemed_script_bytes: Vec<u8> = vec![0xac; 15];
265
266        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
267
268        let lock_script = p2sh_lock_script(&[0xdd; 20]);
269        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
270        let spent_outputs = vec![output_with_script(lock_script)];
271
272        assert!(
273            are_inputs_standard(&tx, &spent_outputs),
274            "P2SH input with non-standard redeemed script at exactly MAX_P2SH_SIGOPS should be accepted"
275        );
276    }
277
278    // -- p2sh_sigop_count tests --
279
280    #[test]
281    fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
282        let _init_guard = zebra_test::init();
283
284        // Build a P2SH input whose redeemed script has 5 OP_CHECKSIG opcodes.
285        let redeemed_script_bytes: Vec<u8> = vec![0xac; 5];
286
287        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
288
289        let lock_script = p2sh_lock_script(&[0xdd; 20]);
290        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
291        let spent_outputs = vec![output_with_script(lock_script)];
292
293        let count = p2sh_sigop_count(&tx, &spent_outputs);
294        assert_eq!(
295            count, 5,
296            "p2sh_sigop_count should return 5 for a redeemed script with 5 OP_CHECKSIG"
297        );
298    }
299
300    #[test]
301    fn p2sh_sigop_count_returns_zero_for_non_p2sh() {
302        let _init_guard = zebra_test::init();
303
304        // P2PKH spent output -- not P2SH, so p2sh_sigop_count should return 0.
305        let script_sig = push_only_script_sig(2);
306        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
307        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
308
309        let count = p2sh_sigop_count(&tx, &spent_outputs);
310        assert_eq!(
311            count, 0,
312            "p2sh_sigop_count should return 0 for non-P2SH inputs"
313        );
314    }
315
316    #[test]
317    fn p2sh_sigop_count_sums_across_multiple_inputs() {
318        let _init_guard = zebra_test::init();
319
320        // Input 0: P2SH with redeemed script having 3 OP_CHECKSIG
321        let redeemed_1: Vec<u8> = vec![0xac; 3];
322        let script_sig_1 = p2sh_script_sig(&[&redeemed_1]);
323        let lock_1 = p2sh_lock_script(&[0xdd; 20]);
324
325        // Input 1: P2PKH (non-P2SH, contributes 0)
326        let script_sig_2 = push_only_script_sig(2);
327        let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
328
329        // Input 2: P2SH with redeemed script having 7 OP_CHECKSIG
330        let redeemed_3: Vec<u8> = vec![0xac; 7];
331        let script_sig_3 = p2sh_script_sig(&[&redeemed_3]);
332        let lock_3 = p2sh_lock_script(&[0xee; 20]);
333
334        let tx = make_v4_tx(
335            vec![
336                prevout_input(script_sig_1),
337                prevout_input(script_sig_2),
338                prevout_input(script_sig_3),
339            ],
340            vec![],
341        );
342        let spent_outputs = vec![
343            output_with_script(lock_1),
344            output_with_script(lock_2),
345            output_with_script(lock_3),
346        ];
347
348        let count = p2sh_sigop_count(&tx, &spent_outputs);
349        assert_eq!(
350            count, 10,
351            "p2sh_sigop_count should sum sigops across all P2SH inputs (3 + 0 + 7)"
352        );
353    }
354
355    #[test]
356    fn are_inputs_standard_rejects_second_non_standard_input() {
357        let _init_guard = zebra_test::init();
358
359        // Input 0: valid P2PKH (2 pushes)
360        let script_sig_ok = push_only_script_sig(2);
361        let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
362
363        // Input 1: P2PKH with wrong stack depth (3 pushes instead of 2)
364        let script_sig_bad = push_only_script_sig(3);
365        let lock_bad = p2pkh_lock_script(&[0xbb; 20]);
366
367        let tx = make_v4_tx(
368            vec![prevout_input(script_sig_ok), prevout_input(script_sig_bad)],
369            vec![],
370        );
371        let spent_outputs = vec![output_with_script(lock_ok), output_with_script(lock_bad)];
372
373        assert!(
374            !are_inputs_standard(&tx, &spent_outputs),
375            "should reject when second input is non-standard even if first is valid"
376        );
377    }
378}