zebrad/components/mempool/storage/
policy.rs1use zcash_script::opcode::PossiblyBad;
8use zcash_script::script::Evaluable as _;
9use zcash_script::{script, solver, Opcode};
10use zebra_chain::{transaction::Transaction, transparent};
11
12pub(super) const MAX_P2SH_SIGOPS: u32 = 15;
15
16pub(super) const MAX_STANDARD_TX_SIGOPS: u32 = 4000;
19
20pub(super) const MAX_STANDARD_SCRIPTSIG_SIZE: usize = 1650;
23
24pub(super) const MAX_STANDARD_MULTISIG_PUBKEYS: usize = 3;
27
28pub(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
39fn extract_p2sh_redeemed_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
49 let code = script::Code(unlock_script.as_raw_bytes().to_vec());
50 let mut last_push_data: Option<Vec<u8>> = None;
51 for opcode in code.parse() {
52 if let Ok(PossiblyBad::Good(Opcode::PushValue(pv))) = opcode {
53 last_push_data = Some(pv.value());
54 }
55 }
56 last_push_data
57}
58
59fn count_script_push_ops(script_bytes: &[u8]) -> usize {
64 let code = script::Code(script_bytes.to_vec());
65 code.parse()
66 .filter(|op| matches!(op, Ok(PossiblyBad::Good(Opcode::PushValue(_)))))
67 .count()
68}
69
70fn script_sig_args_expected(kind: &solver::ScriptKind) -> Option<usize> {
79 match kind {
80 solver::ScriptKind::PubKey { .. } => Some(1),
81 solver::ScriptKind::PubKeyHash { .. } => Some(2),
82 solver::ScriptKind::ScriptHash { .. } => Some(1),
83 solver::ScriptKind::MultiSig { required, .. } => Some(*required as usize + 1),
84 solver::ScriptKind::NullData { .. } => None,
85 }
86}
87
88fn p2sh_redeemed_script_sigop_count(
93 input: &transparent::Input,
94 spent_output: &transparent::Output,
95) -> Option<u32> {
96 let unlock_script = match input {
97 transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
98 transparent::Input::Coinbase { .. } => return None,
99 };
100
101 let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
102 if !lock_code.is_pay_to_script_hash() {
103 return None;
104 }
105
106 let redeemed_bytes = extract_p2sh_redeemed_script(unlock_script)?;
107 let redeemed = script::Code(redeemed_bytes);
108 Some(redeemed.sig_op_count(true))
109}
110
111pub(super) fn p2sh_sigop_count(tx: &Transaction, spent_outputs: &[transparent::Output]) -> u32 {
122 debug_assert_eq!(
123 tx.inputs().len(),
124 spent_outputs.len(),
125 "spent_outputs must align with transaction inputs"
126 );
127 tx.inputs()
128 .iter()
129 .zip(spent_outputs.iter())
130 .filter_map(|(input, spent_output)| p2sh_redeemed_script_sigop_count(input, spent_output))
131 .sum()
132}
133
134pub(super) fn are_inputs_standard(tx: &Transaction, spent_outputs: &[transparent::Output]) -> bool {
153 debug_assert_eq!(
154 tx.inputs().len(),
155 spent_outputs.len(),
156 "spent_outputs must align with transaction inputs"
157 );
158 for (input, spent_output) in tx.inputs().iter().zip(spent_outputs.iter()) {
159 let unlock_script = match input {
160 transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
161 transparent::Input::Coinbase { .. } => continue,
162 };
163
164 let script_kind = match standard_script_kind(&spent_output.lock_script) {
166 Some(kind) => kind,
167 None => return false,
168 };
169
170 let mut n_args_expected = match script_sig_args_expected(&script_kind) {
173 Some(n) => n,
174 None => return false,
175 };
176
177 let stack_size = count_script_push_ops(unlock_script.as_raw_bytes());
180
181 if matches!(script_kind, solver::ScriptKind::ScriptHash { .. }) {
183 let Some(redeemed_bytes) = extract_p2sh_redeemed_script(unlock_script) else {
184 return false;
185 };
186
187 let redeemed_code = script::Code(redeemed_bytes);
188
189 let redeemed_kind = {
191 let component = redeemed_code
192 .to_component()
193 .ok()
194 .and_then(|c| c.refine().ok());
195 component.and_then(|c| solver::standard(&c))
196 };
197
198 match redeemed_kind {
199 Some(ref inner_kind) => {
200 match script_sig_args_expected(inner_kind) {
202 Some(inner) => n_args_expected += inner,
203 None => return false,
204 }
205 }
206 None => {
207 let sigops = redeemed_code.sig_op_count(true);
211 if sigops > MAX_P2SH_SIGOPS {
212 return false;
213 }
214
215 continue;
217 }
218 }
219 }
220
221 if stack_size != n_args_expected {
223 return false;
224 }
225 }
226 true
227}
228
229#[cfg(test)]
233pub(super) fn p2pkh_lock_script(hash: &[u8; 20]) -> transparent::Script {
234 let mut s = vec![0x76, 0xa9, 0x14];
235 s.extend_from_slice(hash);
236 s.push(0x88);
237 s.push(0xac);
238 transparent::Script::new(&s)
239}
240
241#[cfg(test)]
243pub(super) fn p2sh_lock_script(hash: &[u8; 20]) -> transparent::Script {
244 let mut s = vec![0xa9, 0x14];
245 s.extend_from_slice(hash);
246 s.push(0x87);
247 transparent::Script::new(&s)
248}
249
250#[cfg(test)]
252pub(super) fn p2pk_lock_script(pubkey: &[u8; 33]) -> transparent::Script {
253 let mut s = Vec::with_capacity(1 + 33 + 1);
254 s.push(0x21); s.extend_from_slice(pubkey);
256 s.push(0xac); transparent::Script::new(&s)
258}
259
260#[cfg(test)]
261mod tests {
262 use zebra_chain::{
263 block::Height,
264 transaction::{self, LockTime, Transaction},
265 };
266
267 use super::*;
268
269 fn multisig_lock_script(required: u8, pubkeys: &[&[u8; 33]]) -> transparent::Script {
273 let mut s = Vec::new();
274 s.push(0x50 + required);
276 for pk in pubkeys {
277 s.push(0x21); s.extend_from_slice(*pk);
279 }
280 s.push(0x50 + pubkeys.len() as u8);
282 s.push(0xae);
284 transparent::Script::new(&s)
285 }
286
287 fn push_only_script_sig(n_pushes: usize) -> transparent::Script {
290 let mut bytes = Vec::with_capacity(n_pushes * 2);
291 for _ in 0..n_pushes {
292 bytes.push(0x01);
294 bytes.push(0x42);
295 }
296 transparent::Script::new(&bytes)
297 }
298
299 fn p2sh_script_sig(push_items: &[&[u8]]) -> transparent::Script {
303 let mut bytes = Vec::new();
304 for item in push_items {
305 assert!(
306 item.len() <= 75,
307 "p2sh_script_sig only supports OP_PUSHBYTES (max 75 bytes), got {}",
308 item.len()
309 );
310 bytes.push(item.len() as u8);
312 bytes.extend_from_slice(item);
313 }
314 transparent::Script::new(&bytes)
315 }
316
317 fn make_v4_tx(
319 inputs: Vec<transparent::Input>,
320 outputs: Vec<transparent::Output>,
321 ) -> Transaction {
322 Transaction::V4 {
323 inputs,
324 outputs,
325 lock_time: LockTime::min_lock_time_timestamp(),
326 expiry_height: Height(0),
327 joinsplit_data: None,
328 sapling_shielded_data: None,
329 }
330 }
331
332 fn prevout_input(unlock_script: transparent::Script) -> transparent::Input {
334 transparent::Input::PrevOut {
335 outpoint: transparent::OutPoint {
336 hash: transaction::Hash([0xaa; 32]),
337 index: 0,
338 },
339 unlock_script,
340 sequence: 0xffffffff,
341 }
342 }
343
344 fn output_with_script(lock_script: transparent::Script) -> transparent::Output {
347 transparent::Output {
348 value: 100_000u64.try_into().unwrap(),
349 lock_script,
350 }
351 }
352
353 #[test]
356 fn count_script_push_ops_counts_pushes() {
357 let _init_guard = zebra_test::init();
358 let script_bytes = vec![0x00, 0x01, 0xaa, 0x01, 0xbb];
360 let count = count_script_push_ops(&script_bytes);
361 assert_eq!(count, 3, "should count 3 push operations");
362 }
363
364 #[test]
365 fn count_script_push_ops_empty_script() {
366 let _init_guard = zebra_test::init();
367 let count = count_script_push_ops(&[]);
368 assert_eq!(count, 0, "empty script should have 0 push ops");
369 }
370
371 #[test]
372 fn count_script_push_ops_pushdata1() {
373 let _init_guard = zebra_test::init();
374 let script_bytes = vec![0x4c, 0x03, 0xaa, 0xbb, 0xcc];
376 let count = count_script_push_ops(&script_bytes);
377 assert_eq!(count, 1, "OP_PUSHDATA1 should count as 1 push operation");
378 }
379
380 #[test]
381 fn count_script_push_ops_pushdata2() {
382 let _init_guard = zebra_test::init();
383 let script_bytes = vec![0x4d, 0x03, 0x00, 0xaa, 0xbb, 0xcc];
385 let count = count_script_push_ops(&script_bytes);
386 assert_eq!(count, 1, "OP_PUSHDATA2 should count as 1 push operation");
387 }
388
389 #[test]
390 fn count_script_push_ops_pushdata4() {
391 let _init_guard = zebra_test::init();
392 let script_bytes = vec![0x4e, 0x02, 0x00, 0x00, 0x00, 0xaa, 0xbb];
394 let count = count_script_push_ops(&script_bytes);
395 assert_eq!(count, 1, "OP_PUSHDATA4 should count as 1 push operation");
396 }
397
398 #[test]
399 fn count_script_push_ops_mixed_push_types() {
400 let _init_guard = zebra_test::init();
401 let script_bytes = vec![0x00, 0x01, 0xaa, 0x4c, 0x01, 0xbb];
403 let count = count_script_push_ops(&script_bytes);
404 assert_eq!(
405 count, 3,
406 "mixed push types should each count as 1 push operation"
407 );
408 }
409
410 #[test]
411 fn count_script_push_ops_truncated_script() {
412 let _init_guard = zebra_test::init();
413 let script_bytes = vec![0x0a, 0xaa, 0xbb, 0xcc];
416 let count = count_script_push_ops(&script_bytes);
417 assert_eq!(
418 count, 0,
419 "truncated script should count 0 successful push operations"
420 );
421 }
422
423 #[test]
426 fn extract_p2sh_redeemed_script_extracts_last_push() {
427 let _init_guard = zebra_test::init();
428 let unlock_script = transparent::Script::new(&[0x03, 0x61, 0x62, 0x63, 0x02, 0x64, 0x65]);
431 let redeemed = extract_p2sh_redeemed_script(&unlock_script);
432 assert_eq!(
433 redeemed,
434 Some(vec![0x64, 0x65]),
435 "should extract the last push data"
436 );
437 }
438
439 #[test]
440 fn extract_p2sh_redeemed_script_empty_script() {
441 let _init_guard = zebra_test::init();
442 let unlock_script = transparent::Script::new(&[]);
443 let redeemed = extract_p2sh_redeemed_script(&unlock_script);
444 assert!(redeemed.is_none(), "empty scriptSig should return None");
445 }
446
447 #[test]
450 fn script_sig_args_expected_values() {
451 let _init_guard = zebra_test::init();
452
453 let pkh_kind = solver::ScriptKind::PubKeyHash { hash: [0xaa; 20] };
455 assert_eq!(script_sig_args_expected(&pkh_kind), Some(2));
456
457 let sh_kind = solver::ScriptKind::ScriptHash { hash: [0xbb; 20] };
459 assert_eq!(script_sig_args_expected(&sh_kind), Some(1));
460
461 let nd_kind = solver::ScriptKind::NullData { data: vec![] };
463 assert_eq!(script_sig_args_expected(&nd_kind), None);
464
465 let p2pk_script = p2pk_lock_script(&[0x02; 33]);
467 let p2pk_kind =
468 standard_script_kind(&p2pk_script).expect("P2PK should be a standard script kind");
469 assert_eq!(script_sig_args_expected(&p2pk_kind), Some(1));
470
471 let ms_script = multisig_lock_script(1, &[&[0x02; 33]]);
473 let ms_kind = standard_script_kind(&ms_script)
474 .expect("1-of-1 multisig should be a standard script kind");
475 assert_eq!(script_sig_args_expected(&ms_kind), Some(2));
476 }
477
478 #[test]
481 fn are_inputs_standard_accepts_valid_p2pkh() {
482 let _init_guard = zebra_test::init();
483
484 let script_sig = push_only_script_sig(2);
486 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
487 let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
488
489 assert!(
490 are_inputs_standard(&tx, &spent_outputs),
491 "valid P2PKH input with correct stack depth should be standard"
492 );
493 }
494
495 #[test]
496 fn are_inputs_standard_rejects_wrong_stack_depth() {
497 let _init_guard = zebra_test::init();
498
499 let script_sig = push_only_script_sig(3);
501 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
502 let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
503
504 assert!(
505 !are_inputs_standard(&tx, &spent_outputs),
506 "P2PKH input with 3 pushes instead of 2 should be non-standard"
507 );
508 }
509
510 #[test]
511 fn are_inputs_standard_rejects_too_few_pushes() {
512 let _init_guard = zebra_test::init();
513
514 let script_sig = push_only_script_sig(1);
516 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
517 let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
518
519 assert!(
520 !are_inputs_standard(&tx, &spent_outputs),
521 "P2PKH input with 1 push instead of 2 should be non-standard"
522 );
523 }
524
525 #[test]
526 fn are_inputs_standard_rejects_non_standard_spent_output() {
527 let _init_guard = zebra_test::init();
528
529 let non_standard_lock = transparent::Script::new(&[0x51, 0x52, 0x93]);
531 let script_sig = push_only_script_sig(1);
532 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
533 let spent_outputs = vec![output_with_script(non_standard_lock)];
534
535 assert!(
536 !are_inputs_standard(&tx, &spent_outputs),
537 "input spending a non-standard script should be non-standard"
538 );
539 }
540
541 #[test]
542 fn are_inputs_standard_accepts_p2sh_with_standard_redeemed_script() {
543 let _init_guard = zebra_test::init();
544
545 let redeemed_script_bytes = {
549 let mut s = vec![0x76, 0xa9, 0x14];
550 s.extend_from_slice(&[0xcc; 20]);
551 s.push(0x88);
552 s.push(0xac);
553 s
554 };
555
556 let script_sig = p2sh_script_sig(&[&[0xaa], &[0xbb], &redeemed_script_bytes]);
563
564 let lock_script = p2sh_lock_script(&[0xdd; 20]);
568 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
569 let spent_outputs = vec![output_with_script(lock_script)];
570
571 assert!(
572 are_inputs_standard(&tx, &spent_outputs),
573 "P2SH input with standard P2PKH redeemed script and correct stack depth should be standard"
574 );
575 }
576
577 #[test]
578 fn are_inputs_standard_rejects_p2sh_with_too_many_sigops() {
579 let _init_guard = zebra_test::init();
580
581 let redeemed_script_bytes: Vec<u8> = vec![0xac; 16];
584
585 let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
589
590 let lock_script = p2sh_lock_script(&[0xdd; 20]);
591 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
592 let spent_outputs = vec![output_with_script(lock_script)];
593
594 assert!(
595 !are_inputs_standard(&tx, &spent_outputs),
596 "P2SH input with redeemed script exceeding MAX_P2SH_SIGOPS should be non-standard"
597 );
598 }
599
600 #[test]
601 fn are_inputs_standard_accepts_p2sh_with_non_standard_low_sigops() {
602 let _init_guard = zebra_test::init();
603
604 let redeemed_script_bytes: Vec<u8> = vec![0xac; 15];
607
608 let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
609
610 let lock_script = p2sh_lock_script(&[0xdd; 20]);
611 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
612 let spent_outputs = vec![output_with_script(lock_script)];
613
614 assert!(
615 are_inputs_standard(&tx, &spent_outputs),
616 "P2SH input with non-standard redeemed script at exactly MAX_P2SH_SIGOPS should be accepted"
617 );
618 }
619
620 #[test]
623 fn p2sh_sigop_count_returns_sigops_for_p2sh_input() {
624 let _init_guard = zebra_test::init();
625
626 let redeemed_script_bytes: Vec<u8> = vec![0xac; 5];
628
629 let script_sig = p2sh_script_sig(&[&redeemed_script_bytes]);
630
631 let lock_script = p2sh_lock_script(&[0xdd; 20]);
632 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
633 let spent_outputs = vec![output_with_script(lock_script)];
634
635 let count = p2sh_sigop_count(&tx, &spent_outputs);
636 assert_eq!(
637 count, 5,
638 "p2sh_sigop_count should return 5 for a redeemed script with 5 OP_CHECKSIG"
639 );
640 }
641
642 #[test]
643 fn p2sh_sigop_count_returns_zero_for_non_p2sh() {
644 let _init_guard = zebra_test::init();
645
646 let script_sig = push_only_script_sig(2);
648 let tx = make_v4_tx(vec![prevout_input(script_sig)], vec![]);
649 let spent_outputs = vec![output_with_script(p2pkh_lock_script(&[0xaa; 20]))];
650
651 let count = p2sh_sigop_count(&tx, &spent_outputs);
652 assert_eq!(
653 count, 0,
654 "p2sh_sigop_count should return 0 for non-P2SH inputs"
655 );
656 }
657
658 #[test]
659 fn p2sh_sigop_count_sums_across_multiple_inputs() {
660 let _init_guard = zebra_test::init();
661
662 let redeemed_1: Vec<u8> = vec![0xac; 3];
664 let script_sig_1 = p2sh_script_sig(&[&redeemed_1]);
665 let lock_1 = p2sh_lock_script(&[0xdd; 20]);
666
667 let script_sig_2 = push_only_script_sig(2);
669 let lock_2 = p2pkh_lock_script(&[0xaa; 20]);
670
671 let redeemed_3: Vec<u8> = vec![0xac; 7];
673 let script_sig_3 = p2sh_script_sig(&[&redeemed_3]);
674 let lock_3 = p2sh_lock_script(&[0xee; 20]);
675
676 let tx = make_v4_tx(
677 vec![
678 prevout_input(script_sig_1),
679 prevout_input(script_sig_2),
680 prevout_input(script_sig_3),
681 ],
682 vec![],
683 );
684 let spent_outputs = vec![
685 output_with_script(lock_1),
686 output_with_script(lock_2),
687 output_with_script(lock_3),
688 ];
689
690 let count = p2sh_sigop_count(&tx, &spent_outputs);
691 assert_eq!(
692 count, 10,
693 "p2sh_sigop_count should sum sigops across all P2SH inputs (3 + 0 + 7)"
694 );
695 }
696
697 #[test]
698 fn are_inputs_standard_rejects_second_non_standard_input() {
699 let _init_guard = zebra_test::init();
700
701 let script_sig_ok = push_only_script_sig(2);
703 let lock_ok = p2pkh_lock_script(&[0xaa; 20]);
704
705 let script_sig_bad = push_only_script_sig(3);
707 let lock_bad = p2pkh_lock_script(&[0xbb; 20]);
708
709 let tx = make_v4_tx(
710 vec![prevout_input(script_sig_ok), prevout_input(script_sig_bad)],
711 vec![],
712 );
713 let spent_outputs = vec![output_with_script(lock_ok), output_with_script(lock_bad)];
714
715 assert!(
716 !are_inputs_standard(&tx, &spent_outputs),
717 "should reject when second input is non-standard even if first is valid"
718 );
719 }
720}