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::V4 {
108            inputs,
109            outputs,
110            lock_time: LockTime::min_lock_time_timestamp(),
111            expiry_height: Height(0),
112            joinsplit_data: None,
113            sapling_shielded_data: None,
114        }
115    }
116
117    /// Build a PrevOut input with the given unlock script.
118    fn prevout_input(unlock_script: transparent::Script) -> transparent::Input {
119        transparent::Input::PrevOut {
120            outpoint: transparent::OutPoint {
121                hash: transaction::Hash([0xaa; 32]),
122                index: 0,
123            },
124            unlock_script,
125            sequence: 0xffffffff,
126        }
127    }
128
129    /// Build a transparent output with the given lock script.
130    /// Uses a non-dust value to avoid false positives in standardness checks.
131    fn output_with_script(lock_script: transparent::Script) -> transparent::Output {
132        transparent::Output {
133            value: 100_000u64.try_into().unwrap(),
134            lock_script,
135        }
136    }
137
138    // -- are_inputs_standard tests --
139
140    #[test]
141    fn are_inputs_standard_accepts_valid_p2pkh() {
142        let _init_guard = zebra_test::init();
143
144        // P2PKH expects 2 scriptSig pushes: <sig> <pubkey>
145        let script_sig = push_only_script_sig(2);
146        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
147        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
148
149        assert!(
150            are_inputs_standard(&tx, &spent_outputs),
151            "valid P2PKH input with correct stack depth should be standard"
152        );
153    }
154
155    #[test]
156    fn are_inputs_standard_rejects_wrong_stack_depth() {
157        let _init_guard = zebra_test::init();
158
159        // P2PKH expects 2 pushes, but we provide 3
160        let script_sig = push_only_script_sig(3);
161        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
162        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
163
164        assert!(
165            !are_inputs_standard(&tx, &spent_outputs),
166            "P2PKH input with 3 pushes instead of 2 should be non-standard"
167        );
168    }
169
170    #[test]
171    fn are_inputs_standard_rejects_too_few_pushes() {
172        let _init_guard = zebra_test::init();
173
174        // P2PKH expects 2 pushes, but we provide 1
175        let script_sig = push_only_script_sig(1);
176        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
177        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
178
179        assert!(
180            !are_inputs_standard(&tx, &spent_outputs),
181            "P2PKH input with 1 push instead of 2 should be non-standard"
182        );
183    }
184
185    #[test]
186    fn are_inputs_standard_rejects_non_standard_spent_output() {
187        let _init_guard = zebra_test::init();
188
189        // OP_1 OP_2 OP_ADD -- not a recognized standard script type
190        let non_standard_lock = transparent::Script::new(&[0x51, 0x52, 0x93]);
191        let script_sig = push_only_script_sig(1);
192        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
193        let spent_outputs = vec![output_with_script(non_standard_lock)];
194
195        assert!(
196            !are_inputs_standard(&tx, &spent_outputs),
197            "input spending a non-standard script should be non-standard"
198        );
199    }
200
201    #[test]
202    fn are_inputs_standard_accepts_p2sh_with_standard_redeemed_script() {
203        let _init_guard = zebra_test::init();
204
205        // Build a P2SH input where the redeemed script is a P2PKH script.
206        // The redeemed script itself is the serialized P2PKH:
207        //   OP_DUP OP_HASH160 <20 bytes> OP_EQUALVERIFY OP_CHECKSIG
208        let redeemed_script_bytes = {
209            let mut s = vec![0x76, 0xa9, 0x14];
210            s.extend_from_slice(&[0xcc; 20]);
211            s.push(0x88);
212            s.push(0xac);
213            s
214        };
215
216        // For P2SH with a P2PKH redeemed script:
217        //   script_sig_args_expected(ScriptHash) = 1  (the redeemed script push)
218        //   script_sig_args_expected(PubKeyHash) = 2  (sig + pubkey inside redeemed)
219        //   total expected = 1 + 2 = 3
220        //
221        // scriptSig: <sig_placeholder> <pubkey_placeholder> <redeemed_script>
222        let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
223
224        // The policy check uses is_pay_to_script_hash() which only checks the
225        // script pattern (OP_HASH160 <20 bytes> OP_EQUAL), not the hash value.
226        // Any 20-byte hash works for testing the policy logic.
227        let lock_script = p2sh_lock_script(&[0xdd; 20]);
228        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
229        let spent_outputs = vec![output_with_script(lock_script)];
230
231        assert!(
232            are_inputs_standard(&tx, &spent_outputs),
233            "P2SH input with standard P2PKH redeemed script and correct stack depth should be standard"
234        );
235    }
236
237    #[test]
238    fn are_inputs_standard_rejects_p2sh_with_too_many_sigops() {
239        let _init_guard = zebra_test::init();
240
241        // Build a redeemed script that has more than MAX_P2SH_SIGOPS (15) sigops.
242        // Use 16 consecutive OP_CHECKSIG (0xac) opcodes.
243        let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
244
245        // scriptSig: just push the redeemed script (1 push)
246        // Since the redeemed script is non-standard, are_inputs_standard
247        // checks sigops. With 16 > MAX_P2SH_SIGOPS (15), it should reject.
248        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
249
250        let lock_script = p2sh_lock_script(&[0xdd; 20]);
251        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
252        let spent_outputs = vec![output_with_script(lock_script)];
253
254        assert!(
255            !are_inputs_standard(&tx, &spent_outputs),
256            "P2SH input with redeemed script exceeding MAX_P2SH_SIGOPS should be non-standard"
257        );
258    }
259
260    #[test]
261    fn are_inputs_standard_accepts_p2sh_with_non_standard_low_sigops() {
262        let _init_guard = zebra_test::init();
263
264        // Build a redeemed script that is non-standard but has <= MAX_P2SH_SIGOPS (15).
265        // Use exactly 15 OP_CHECKSIG (0xac) opcodes -- should be accepted.
266        let redeemed_script_bytes: Vec<u8> = vec![0xac; 15];
267
268        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
269
270        let lock_script = p2sh_lock_script(&[0xdd; 20]);
271        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
272        let spent_outputs = vec![output_with_script(lock_script)];
273
274        assert!(
275            are_inputs_standard(&tx, &spent_outputs),
276            "P2SH input with non-standard redeemed script at exactly MAX_P2SH_SIGOPS should be accepted"
277        );
278    }
279
280    // -- p2sh_sigop_count tests --
281
282    #[test]
283    fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
284        let _init_guard = zebra_test::init();
285
286        // Build a P2SH input whose redeemed script has 5 OP_CHECKSIG opcodes.
287        let redeemed_script_bytes: Vec<u8> = vec![0xac; 5];
288
289        let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
290
291        let lock_script = p2sh_lock_script(&[0xdd; 20]);
292        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
293        let spent_outputs = vec![output_with_script(lock_script)];
294
295        let count = p2sh_sigop_count(&tx, &spent_outputs);
296        assert_eq!(
297            count, 5,
298            "p2sh_sigop_count should return 5 for a redeemed script with 5 OP_CHECKSIG"
299        );
300    }
301
302    #[test]
303    fn p2sh_sigop_count_returns_zero_for_non_p2sh() {
304        let _init_guard = zebra_test::init();
305
306        // P2PKH spent output -- not P2SH, so p2sh_sigop_count should return 0.
307        let script_sig = push_only_script_sig(2);
308        let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
309        let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
310
311        let count = p2sh_sigop_count(&tx, &spent_outputs);
312        assert_eq!(
313            count, 0,
314            "p2sh_sigop_count should return 0 for non-P2SH inputs"
315        );
316    }
317
318    #[test]
319    fn p2sh_sigop_count_sums_across_multiple_inputs() {
320        let _init_guard = zebra_test::init();
321
322        // Input 0: P2SH with redeemed script having 3 OP_CHECKSIG
323        let redeemed_1: Vec<u8> = vec![0xac; 3];
324        let script_sig_1 = p2sh_script_sig(&[&redeemed_1]);
325        let lock_1 = p2sh_lock_script(&[0xdd; 20]);
326
327        // Input 1: P2PKH (non-P2SH, contributes 0)
328        let script_sig_2 = push_only_script_sig(2);
329        let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
330
331        // Input 2: P2SH with redeemed script having 7 OP_CHECKSIG
332        let redeemed_3: Vec<u8> = vec![0xac; 7];
333        let script_sig_3 = p2sh_script_sig(&[&redeemed_3]);
334        let lock_3 = p2sh_lock_script(&[0xee; 20]);
335
336        let tx = make_v4_tx(
337            vec![
338                prevout_input(script_sig_1),
339                prevout_input(script_sig_2),
340                prevout_input(script_sig_3),
341            ],
342            vec![],
343        );
344        let spent_outputs = vec![
345            output_with_script(lock_1),
346            output_with_script(lock_2),
347            output_with_script(lock_3),
348        ];
349
350        let count = p2sh_sigop_count(&tx, &spent_outputs);
351        assert_eq!(
352            count, 10,
353            "p2sh_sigop_count should sum sigops across all P2SH inputs (3 + 0 + 7)"
354        );
355    }
356
357    #[test]
358    fn are_inputs_standard_rejects_second_non_standard_input() {
359        let _init_guard = zebra_test::init();
360
361        // Input 0: valid P2PKH (2 pushes)
362        let script_sig_ok = push_only_script_sig(2);
363        let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
364
365        // Input 1: P2PKH with wrong stack depth (3 pushes instead of 2)
366        let script_sig_bad = push_only_script_sig(3);
367        let lock_bad = p2pkh_lock_script(&[0xbb; 20]);
368
369        let tx = make_v4_tx(
370            vec![prevout_input(script_sig_ok), prevout_input(script_sig_bad)],
371            vec![],
372        );
373        let spent_outputs = vec![output_with_script(lock_ok), output_with_script(lock_bad)];
374
375        assert!(
376            !are_inputs_standard(&tx, &spent_outputs),
377            "should reject when second input is non-standard even if first is valid"
378        );
379    }
380}