zebrad/components/mempool/storage/
policy.rs1use zcash_script::opcode::PossiblyBad;
8use zcash_script::{script, solver, Opcode};
9use zebra_chain::{transaction::Transaction, transparent};
10
11pub(super) const MAX_P2SH_SIGOPS: u32 = 15;
14
15pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
18
19pub(super) const MAX_STANDARD_SCRIPTSIG_SIZE: usize = 1650;
22
23pub(super) const MAX_STANDARD_MULTISIG_PUBKEYS: usize = 3;
26
27pub(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
38fn 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
58fn 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
69fn 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
90pub(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 let script_kind = match standard_script_kind(&spent_output.lock_script) {
122 Some(kind) => kind,
123 None => return false,
124 };
125
126 let mut n_args_expected = match script_sig_args_expected(&script_kind) {
129 Some(n) => n,
130 None => return false,
131 };
132
133 let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
136
137 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 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 match script_sig_args_expected(inner_kind) {
158 Some(inner) => n_args_expected += inner,
159 None => return false,
160 }
161 }
162 None => {
163 let sigops = redeemed_code.sig_op_count(true);
167 if sigops > MAX_P2SH_SIGOPS {
168 return false;
169 }
170
171 continue;
173 }
174 }
175 }
176
177 if stack_size != n_args_expected {
179 return false;
180 }
181 }
182 true
183}
184
185#[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#[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#[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); s.extend_from_slice(pubkey);
212 s.push(0xac); 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 fn multisig_lock_script(required: u8, pubkeys: &[&[u8; 33]]) -> transparent::Script {
229 let mut s = Vec::new();
230 s.push(0x50 + required);
232 for pk in pubkeys {
233 s.push(0x21); s.extend_from_slice(*pk);
235 }
236 s.push(0x50 + pubkeys.len() as u8);
238 s.push(0xae);
240 transparent::Script::new(&s)
241 }
242
243 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 bytes.push(0x01);
250 bytes.push(0x42);
251 }
252 transparent::Script::new(&bytes)
253 }
254
255 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 bytes.push(item.len() as u8);
268 bytes.extend_from_slice(item);
269 }
270 transparent::Script::new(&bytes)
271 }
272
273 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 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 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 #[test]
312 fn count_script_push_ops_counts_pushes() {
313 let _init_guard = zebra_test::init();
314 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 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 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 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 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 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 #[test]
382 fn extract_p2sh_redeemed_script_extracts_last_push() {
383 let _init_guard = zebra_test::init();
384 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 #[test]
406 fn script_sig_args_expected_values() {
407 let _init_guard = zebra_test::init();
408
409 let pkh_kind = solver::ScriptKind::PubKeyHash { hash: [0xaa; 20] };
411 assert_eq!(script_sig_args_expected(&pkh_kind), Some(2));
412
413 let sh_kind = solver::ScriptKind::ScriptHash { hash: [0xbb; 20] };
415 assert_eq!(script_sig_args_expected(&sh_kind), Some(1));
416
417 let nd_kind = solver::ScriptKind::NullData { data: vec![] };
419 assert_eq!(script_sig_args_expected(&nd_kind), None);
420
421 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 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 #[test]
437 fn are_inputs_standard_accepts_valid_p2pkh() {
438 let _init_guard = zebra_test::init();
439
440 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 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 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 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 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 let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
519
520 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 let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
540
541 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 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 #[test]
579 fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
580 let _init_guard = zebra_test::init();
581
582 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 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 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 let script_sig_2 = push_only_script_sig(2);
625 let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
626
627 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 let script_sig_ok = push_only_script_sig(2);
659 let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
660
661 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}