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.
43fn extract_p2sh_redeemed_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
44    let code = script::Code(unlock_script.as_raw_bytes().to_vec());
45    let mut last_push_data: Option<Vec<u8>> = None;
46    for opcode in code.parse() {
47        if let Ok(PossiblyBad::Good(Opcode::PushValue(pv))) = opcode {
48            last_push_data = Some(pv.value());
49        }
50    }
51    last_push_data
52}
53
54/// Count the number of push operations in a script.
55///
56/// For a push-only script (already enforced for mempool scriptSigs),
57/// this equals the stack depth after evaluation.
58fn count_script_push_ops(script_bytes: &[u8]) -> usize {
59    let code = script::Code(script_bytes.to_vec());
60    code.parse()
61        .filter(|op| matches!(op, Ok(PossiblyBad::Good(Opcode::PushValue(_)))))
62        .count()
63}
64
65/// Returns the expected number of scriptSig arguments for a given script kind.
66///
67/// TODO: Consider upstreaming to `zcash_script` crate alongside `ScriptKind::req_sigs()`.
68///
69/// Mirrors zcashd's `ScriptSigArgsExpected()`:
70/// <https://github.com/zcash/zcash/blob/v6.11.0/src/script/standard.cpp#L135>
71///
72/// Returns `None` for non-standard types (TX_NONSTANDARD, TX_NULL_DATA).
73fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option<usize> {
74    match kind {
75        solver::ScriptKind::PubKey { .. } => Some(1),
76        solver::ScriptKind::PubKeyHash { .. } => Some(2),
77        solver::ScriptKind::ScriptHash { .. } => Some(1),
78        solver::ScriptKind::MultiSig { required, .. } => Some(*required as usize + 1),
79        solver::ScriptKind::NullData { .. } => None,
80    }
81}
82
83/// Extracts the P2SH redeemed script's sigop count for a single input.
84///
85/// Returns `Some(count)` for P2SH inputs where a redeemed script was found,
86/// `None` for non-P2SH, coinbase, or P2SH inputs with an empty scriptSig.
87fn p2sh_redeemed_script_sigop_count(
88    input: &transparent::Input,
89    spent_output: &transparent::Output,
90) -> Option<u32> {
91    let unlock_script = match input {
92        transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
93        transparent::Input::Coinbase { .. } => return None,
94    };
95
96    let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
97    if !lock_code.is_pay_to_script_hash() {
98        return None;
99    }
100
101    let redeemed_bytes = extract_p2sh_redeemed_script(unlock_script)?;
102    let redeemed = script::Code(redeemed_bytes);
103    Some(redeemed.sig_op_count(true))
104}
105
106/// Returns the total number of P2SH sigops across all inputs of the transaction.
107///
108/// Mirrors zcashd's `GetP2SHSigOpCount()`:
109/// <https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L1191>
110///
111/// # Panics
112///
113/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
114pub(super) fn p2sh_sigop_count(tx: &Transaction, spent_outputs: &[transparent::Output]) -> u32 {
115    tx.inputs()
116        .iter()
117        .zip(spent_outputs.iter())
118        .filter_map(|(input, spent_output)| p2sh_redeemed_script_sigop_count(input, spent_output))
119        .sum()
120}
121
122/// Returns `true` if all transparent inputs are standard.
123///
124/// Mirrors zcashd's `AreInputsStandard()`:
125/// <https://github.com/zcash/zcash/blob/v6.11.0/src/policy/policy.cpp#L136>
126///
127/// For each input:
128/// 1. The spent output's scriptPubKey must be a known standard type (via the `zcash_script` solver).
129///    Non-standard scripts and OP_RETURN outputs are rejected.
130/// 2. The scriptSig stack depth must match `ScriptSigArgsExpected()`.
131/// 3. For P2SH inputs:
132///    - If the redeemed script is standard, its expected args are added to the total.
133///    - If the redeemed script is non-standard, it must have <= [`MAX_P2SH_SIGOPS`] sigops.
134///
135/// # Panics
136///
137/// Callers must ensure `spent_outputs.len()` matches the number of transparent inputs.
138pub(super) fn are_inputs_standard(tx: &Transaction, spent_outputs: &[transparent::Output]) -> bool {
139    for (input, spent_output) in tx.inputs().iter().zip(spent_outputs.iter()) {
140        let unlock_script = match input {
141            transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
142            transparent::Input::Coinbase { .. } => continue,
143        };
144
145        // Step 1: Classify the spent output's scriptPubKey via the zcash_script solver.
146        let script_kind = match standard_script_kind(&spent_output.lock_script) {
147            Some(kind) => kind,
148            None => return false,
149        };
150
151        // Step 2: Get expected number of scriptSig arguments.
152        // Returns None for TX_NONSTANDARD and TX_NULL_DATA.
153        let mut n_args_expected = match script_sig_args_expected(&script_kind) {
154            Some(n) => n,
155            None => return false,
156        };
157
158        // Step 3: Count actual push operations in scriptSig.
159        // For push-only scripts (enforced by reject_if_non_standard_tx), this equals the stack depth.
160        let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
161
162        // Step 4: P2SH-specific checks.
163        if matches!(script_kind, solver::ScriptKind::ScriptHash { .. }) {
164            let Some(redeemed_bytes) = extract_p2sh_redeemed_script(unlock_script) else {
165                return false;
166            };
167
168            let redeemed_code = script::Code(redeemed_bytes);
169
170            // Classify the redeemed script using the zcash_script solver.
171            let redeemed_kind = {
172                let component = redeemed_code
173                    .to_component()
174                    .ok()
175                    .and_then(|c| c.refine().ok());
176                component.and_then(|c| solver::standard(&c))
177            };
178
179            match redeemed_kind {
180                Some(ref inner_kind) => {
181                    // Standard redeemed script: add its expected args.
182                    match script_sig_args_expected(inner_kind) {
183                        Some(inner) => n_args_expected += inner,
184                        None => return false,
185                    }
186                }
187                None => {
188                    // Non-standard redeemed script: accept if sigops <= limit.
189                    // Matches zcashd: "Any other Script with less than 15 sigops OK:
190                    // ... extra data left on the stack after execution is OK, too"
191                    let sigops = redeemed_code.sig_op_count(true);
192                    if sigops > MAX_P2SH_SIGOPS {
193                        return false;
194                    }
195
196                    // This input is acceptable; move on to the next input.
197                    continue;
198                }
199            }
200        }
201
202        // Step 5: Reject if scriptSig has wrong number of stack items.
203        if stack_size != n_args_expected {
204            return false;
205        }
206    }
207    true
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn count_script_push_ops_counts_pushes() {
216        let _init_guard = zebra_test::init();
217        // Script with 3 push operations: OP_0 <push 1 byte> <push 1 byte>
218        let script_bytes = vec![0x00, 0x01, 0xaa, 0x01, 0xbb];
219        let count = count_script_push_ops(&script_bytes);
220        assert_eq!(count, 3, "should count 3 push operations");
221    }
222
223    #[test]
224    fn count_script_push_ops_empty_script() {
225        let _init_guard = zebra_test::init();
226        let count = count_script_push_ops(&[]);
227        assert_eq!(count, 0, "empty script should have 0 push ops");
228    }
229
230    #[test]
231    fn extract_p2sh_redeemed_script_extracts_last_push() {
232        let _init_guard = zebra_test::init();
233        // scriptSig: <sig> <redeemed_script>
234        // OP_PUSHDATA with 3 bytes "abc" then OP_PUSHDATA with 2 bytes "de"
235        let unlock_script = transparent::Script::new(&[0x03, 0x61, 0x62, 0x63, 0x02, 0x64, 0x65]);
236        let redeemed = extract_p2sh_redeemed_script(&unlock_script);
237        assert_eq!(
238            redeemed,
239            Some(vec![0x64, 0x65]),
240            "should extract the last push data"
241        );
242    }
243
244    #[test]
245    fn extract_p2sh_redeemed_script_empty_script() {
246        let _init_guard = zebra_test::init();
247        let unlock_script = transparent::Script::new(&[]);
248        let redeemed = extract_p2sh_redeemed_script(&unlock_script);
249        assert!(redeemed.is_none(), "empty scriptSig should return None");
250    }
251
252    #[test]
253    fn script_sig_args_expected_values() {
254        let _init_guard = zebra_test::init();
255
256        // P2PKH expects 2 args (sig + pubkey)
257        let pkh_kind = solver::ScriptKind::PubKeyHash { hash: [0xaa; 20] };
258        assert_eq!(script_sig_args_expected(&pkh_kind), Some(2));
259
260        // P2SH expects 1 arg (the redeemed script)
261        let sh_kind = solver::ScriptKind::ScriptHash { hash: [0xbb; 20] };
262        assert_eq!(script_sig_args_expected(&sh_kind), Some(1));
263
264        // NullData expects None (non-standard to spend)
265        let nd_kind = solver::ScriptKind::NullData { data: vec![] };
266        assert_eq!(script_sig_args_expected(&nd_kind), None);
267    }
268}