1use std::{cmp::max, collections::HashMap, ops::Neg, sync::Arc};
4
5use chrono::{TimeZone, Utc};
6use proptest::{array, collection::vec, option, prelude::*, test_runner::TestRunner};
7use reddsa::{orchard::Binding, Signature};
8
9use crate::{
10 amount::{self, Amount, NegativeAllowed, NonNegative},
11 block::{self, arbitrary::MAX_PARTIAL_CHAIN_BLOCKS},
12 orchard,
13 parameters::{Network, NetworkUpgrade},
14 primitives::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof},
15 sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor},
16 serialization::{self, ZcashDeserializeInto},
17 sprout, transparent,
18 value_balance::{ValueBalance, ValueBalanceError},
19 LedgerState,
20};
21
22use itertools::Itertools;
23
24use super::{
25 FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction, UnminedTx, VerifiedUnminedTx,
26};
27
28pub const MAX_ARBITRARY_ITEMS: usize = 4;
33
34impl Transaction {
37 pub fn v1_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
39 (
40 transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
41 vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
42 any::<LockTime>(),
43 )
44 .prop_map(|(inputs, outputs, lock_time)| Transaction::V1 {
45 inputs,
46 outputs,
47 lock_time,
48 })
49 .boxed()
50 }
51
52 pub fn v2_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
54 (
55 transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
56 vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
57 any::<LockTime>(),
58 option::of(any::<JoinSplitData<Bctv14Proof>>()),
59 )
60 .prop_map(
61 |(inputs, outputs, lock_time, joinsplit_data)| Transaction::V2 {
62 inputs,
63 outputs,
64 lock_time,
65 joinsplit_data,
66 },
67 )
68 .boxed()
69 }
70
71 pub fn v3_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
73 (
74 transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
75 vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
76 any::<LockTime>(),
77 any::<block::Height>(),
78 option::of(any::<JoinSplitData<Bctv14Proof>>()),
79 )
80 .prop_map(
81 |(inputs, outputs, lock_time, expiry_height, joinsplit_data)| Transaction::V3 {
82 inputs,
83 outputs,
84 lock_time,
85 expiry_height,
86 joinsplit_data,
87 },
88 )
89 .boxed()
90 }
91
92 pub fn v4_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
94 (
95 transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
96 vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
97 any::<LockTime>(),
98 any::<block::Height>(),
99 option::of(any::<JoinSplitData<Groth16Proof>>()),
100 option::of(any::<sapling::ShieldedData<sapling::PerSpendAnchor>>()),
101 )
102 .prop_map(
103 move |(
104 inputs,
105 outputs,
106 lock_time,
107 expiry_height,
108 joinsplit_data,
109 sapling_shielded_data,
110 )| {
111 Transaction::V4 {
112 inputs,
113 outputs,
114 lock_time,
115 expiry_height,
116 joinsplit_data: if ledger_state.height.is_min() {
117 None
119 } else {
120 joinsplit_data
121 },
122 sapling_shielded_data: if ledger_state.height.is_min() {
123 None
125 } else {
126 sapling_shielded_data
127 },
128 }
129 },
130 )
131 .boxed()
132 }
133
134 pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
136 (
137 NetworkUpgrade::nu5_branch_id_strategy(),
138 any::<LockTime>(),
139 any::<block::Height>(),
140 transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
141 vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
142 option::of(any::<sapling::ShieldedData<sapling::SharedAnchor>>()),
143 option::of(any::<orchard::ShieldedData>()),
144 )
145 .prop_map(
146 move |(
147 network_upgrade,
148 lock_time,
149 expiry_height,
150 inputs,
151 outputs,
152 sapling_shielded_data,
153 orchard_shielded_data,
154 )| {
155 Transaction::V5 {
156 network_upgrade: if ledger_state.transaction_has_valid_network_upgrade() {
157 ledger_state.network_upgrade()
158 } else {
159 network_upgrade
160 },
161 lock_time,
162 expiry_height,
163 inputs,
164 outputs,
165 sapling_shielded_data: if ledger_state.height.is_min() {
166 None
168 } else {
169 sapling_shielded_data
170 },
171 orchard_shielded_data: if ledger_state.height.is_min() {
172 None
174 } else {
175 orchard_shielded_data
176 },
177 }
178 },
179 )
180 .boxed()
181 }
182
183 pub fn vec_strategy(
186 mut ledger_state: LedgerState,
187 len: usize,
188 ) -> BoxedStrategy<Vec<Arc<Self>>> {
189 let coinbase = Transaction::arbitrary_with(ledger_state.clone()).prop_map(Arc::new);
191 ledger_state.has_coinbase = false;
192 let remainder = vec(
193 Transaction::arbitrary_with(ledger_state).prop_map(Arc::new),
194 0..=len,
195 );
196
197 (coinbase, remainder)
198 .prop_map(|(first, mut remainder)| {
199 remainder.insert(0, first);
200 remainder
201 })
202 .boxed()
203 }
204
205 pub fn for_each_value_mut<F>(&mut self, mut f: F)
208 where
209 F: FnMut(&mut Amount<NonNegative>),
210 {
211 for output_value in self.output_values_mut() {
212 f(output_value);
213 }
214
215 for sprout_added_value in self.output_values_to_sprout_mut() {
216 f(sprout_added_value);
217 }
218 for sprout_removed_value in self.input_values_from_sprout_mut() {
219 f(sprout_removed_value);
220 }
221 }
222
223 pub fn for_each_value_balance_mut<F>(&mut self, mut f: F)
226 where
227 F: FnMut(&mut Amount<NegativeAllowed>),
228 {
229 if let Some(sapling_value_balance) = self.sapling_value_balance_mut() {
230 f(sapling_value_balance);
231 }
232
233 if let Some(orchard_value_balance) = self.orchard_value_balance_mut() {
234 f(orchard_value_balance);
235 }
236 }
237
238 pub fn fix_overflow(&mut self) {
245 fn scale_to_avoid_overflow<C: amount::Constraint>(amount: &mut Amount<C>)
246 where
247 Amount<C>: Copy,
248 {
249 const POOL_COUNT: u64 = 4;
250
251 let max_arbitrary_items: u64 = MAX_ARBITRARY_ITEMS.try_into().unwrap();
252 let max_partial_chain_blocks: u64 = MAX_PARTIAL_CHAIN_BLOCKS.try_into().unwrap();
253
254 let transaction_pool_scaling_divisor =
256 max_arbitrary_items * POOL_COUNT * max_arbitrary_items;
257 let chain_pool_scaling_divisor =
259 max_arbitrary_items * max_arbitrary_items * max_partial_chain_blocks;
260 let scaling_divisor = max(transaction_pool_scaling_divisor, chain_pool_scaling_divisor);
261
262 *amount = (*amount / scaling_divisor).expect("divisor is not zero");
263 }
264
265 self.for_each_value_mut(scale_to_avoid_overflow);
266 self.for_each_value_balance_mut(scale_to_avoid_overflow);
267 }
268
269 pub fn fix_chain_value_pools(
300 &mut self,
301 chain_value_pools: ValueBalance<NonNegative>,
302 outputs: &HashMap<transparent::OutPoint, transparent::Output>,
303 ) -> Result<(Amount<NonNegative>, ValueBalance<NonNegative>), ValueBalanceError> {
304 self.fix_overflow();
305
306 let mut input_chain_value_pools = chain_value_pools;
309
310 for input in self.inputs() {
311 input_chain_value_pools = input_chain_value_pools
312 .add_transparent_input(input, outputs)
313 .expect("find_valid_utxo_for_spend only spends unspent transparent outputs");
314 }
315
316 for input in self.input_values_from_sprout_mut() {
322 match input_chain_value_pools
323 .add_chain_value_pool_change(ValueBalance::from_sprout_amount(input.neg()))
324 {
325 Ok(new_chain_pools) => input_chain_value_pools = new_chain_pools,
326 Err(_) => *input = Amount::zero(),
328 }
329 }
330
331 let sapling_input = self.sapling_value_balance().constrain::<NonNegative>();
334 if let Ok(sapling_input) = sapling_input {
335 match input_chain_value_pools.add_chain_value_pool_change(-sapling_input) {
336 Ok(new_chain_pools) => input_chain_value_pools = new_chain_pools,
337 Err(_) => *self.sapling_value_balance_mut().unwrap() = Amount::zero(),
338 }
339 }
340
341 let orchard_input = self.orchard_value_balance().constrain::<NonNegative>();
342 if let Ok(orchard_input) = orchard_input {
343 match input_chain_value_pools.add_chain_value_pool_change(-orchard_input) {
344 Ok(new_chain_pools) => input_chain_value_pools = new_chain_pools,
345 Err(_) => *self.orchard_value_balance_mut().unwrap() = Amount::zero(),
346 }
347 }
348
349 let remaining_transaction_value = self.fix_remaining_value(outputs)?;
350
351 let transaction_chain_value_pool_change =
353 self
354 .value_balance_from_outputs(outputs)
355 .expect("chain value pool and remaining transaction value fixes produce valid transaction value balances")
356 .neg();
357
358 let chain_value_pools = chain_value_pools
359 .add_transaction(self, outputs)
360 .unwrap_or_else(|err| {
361 panic!(
362 "unexpected chain value pool error: {err:?}, \n\
363 original chain value pools: {chain_value_pools:?}, \n\
364 transaction chain value change: {transaction_chain_value_pool_change:?}, \n\
365 input-only transaction chain value pools: {input_chain_value_pools:?}, \n\
366 calculated remaining transaction value: {remaining_transaction_value:?}",
367 )
368 });
369
370 Ok((remaining_transaction_value, chain_value_pools))
371 }
372
373 fn input_value_pool(
380 &self,
381 outputs: &HashMap<transparent::OutPoint, transparent::Output>,
382 ) -> Result<Amount<NonNegative>, ValueBalanceError> {
383 let transparent_inputs = self
384 .inputs()
385 .iter()
386 .map(|input| input.value_from_outputs(outputs))
387 .sum::<Result<Amount<NonNegative>, amount::Error>>()
388 .map_err(ValueBalanceError::Transparent)?;
389 let sprout_inputs = self
395 .input_values_from_sprout()
396 .sum::<Result<Amount<NonNegative>, amount::Error>>()
397 .expect("chain is limited to MAX_MONEY");
398
399 let sapling_input = self
401 .sapling_value_balance()
402 .sapling_amount()
403 .constrain::<NonNegative>()
404 .unwrap_or_else(|_| Amount::zero());
405
406 let orchard_input = self
407 .orchard_value_balance()
408 .orchard_amount()
409 .constrain::<NonNegative>()
410 .unwrap_or_else(|_| Amount::zero());
411
412 let transaction_input_value_pool =
413 (transparent_inputs + sprout_inputs + sapling_input + orchard_input)
414 .expect("chain is limited to MAX_MONEY");
415
416 Ok(transaction_input_value_pool)
417 }
418
419 pub fn fix_remaining_value(
440 &mut self,
441 outputs: &HashMap<transparent::OutPoint, transparent::Output>,
442 ) -> Result<Amount<NonNegative>, ValueBalanceError> {
443 if self.is_coinbase() {
444 return Ok(Amount::zero());
452 }
453
454 let mut remaining_input_value = self.input_value_pool(outputs)?;
455
456 for output_value in self.output_values_mut() {
460 if remaining_input_value >= *output_value {
461 remaining_input_value = (remaining_input_value - *output_value)
462 .expect("input >= output so result is always non-negative");
463 } else {
464 *output_value = Amount::zero();
465 }
466 }
467
468 for output_value in self.output_values_to_sprout_mut() {
469 if remaining_input_value >= *output_value {
470 remaining_input_value = (remaining_input_value - *output_value)
471 .expect("input >= output so result is always non-negative");
472 } else {
473 *output_value = Amount::zero();
474 }
475 }
476
477 if let Some(value_balance) = self.sapling_value_balance_mut() {
478 if let Ok(output_value) = value_balance.neg().constrain::<NonNegative>() {
479 if remaining_input_value >= output_value {
480 remaining_input_value = (remaining_input_value - output_value)
481 .expect("input >= output so result is always non-negative");
482 } else {
483 *value_balance = Amount::zero();
484 }
485 }
486 }
487
488 if let Some(value_balance) = self.orchard_value_balance_mut() {
489 if let Ok(output_value) = value_balance.neg().constrain::<NonNegative>() {
490 if remaining_input_value >= output_value {
491 remaining_input_value = (remaining_input_value - output_value)
492 .expect("input >= output so result is always non-negative");
493 } else {
494 *value_balance = Amount::zero();
495 }
496 }
497 }
498
499 let remaining_transaction_value = self
501 .value_balance_from_outputs(outputs)
502 .expect("chain is limited to MAX_MONEY")
503 .remaining_transaction_value()
504 .unwrap_or_else(|err| {
505 panic!(
506 "unexpected remaining transaction value: {err:?}, \
507 calculated remaining input value: {remaining_input_value:?}"
508 )
509 });
510 assert_eq!(
511 remaining_input_value,
512 remaining_transaction_value,
513 "fix_remaining_value and remaining_transaction_value calculated different remaining values"
514 );
515
516 Ok(remaining_transaction_value)
517 }
518}
519
520impl Arbitrary for Memo {
521 type Parameters = ();
522
523 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
524 (vec(any::<u8>(), 512))
525 .prop_map(|v| {
526 let mut bytes = [0; 512];
527 bytes.copy_from_slice(v.as_slice());
528 Memo(Box::new(bytes))
529 })
530 .boxed()
531 }
532
533 type Strategy = BoxedStrategy<Self>;
534}
535
536impl Arbitrary for LockTime {
538 type Parameters = ();
539
540 fn arbitrary_with(_args: ()) -> Self::Strategy {
541 prop_oneof![
542 (block::Height::MIN.0..=LockTime::MAX_HEIGHT.0)
543 .prop_map(|n| LockTime::Height(block::Height(n))),
544 (LockTime::MIN_TIMESTAMP..=LockTime::MAX_TIMESTAMP).prop_map(|n| {
545 LockTime::Time(
546 Utc.timestamp_opt(n, 0)
547 .single()
548 .expect("in-range number of seconds and valid nanosecond"),
549 )
550 })
551 ]
552 .boxed()
553 }
554
555 type Strategy = BoxedStrategy<Self>;
556}
557
558impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplitData<P> {
559 type Parameters = ();
560
561 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
562 (
563 any::<sprout::JoinSplit<P>>(),
564 vec(any::<sprout::JoinSplit<P>>(), 0..MAX_ARBITRARY_ITEMS),
565 array::uniform32(any::<u8>()),
566 vec(any::<u8>(), 64),
567 )
568 .prop_map(|(first, rest, pub_key_bytes, sig_bytes)| Self {
569 first,
570 rest,
571 pub_key: ed25519_zebra::VerificationKeyBytes::from(pub_key_bytes),
572 sig: ed25519_zebra::Signature::from({
573 let mut b = [0u8; 64];
574 b.copy_from_slice(sig_bytes.as_slice());
575 b
576 }),
577 })
578 .boxed()
579 }
580
581 type Strategy = BoxedStrategy<Self>;
582}
583
584impl<AnchorV> Arbitrary for sapling::ShieldedData<AnchorV>
585where
586 AnchorV: AnchorVariant + Clone + std::fmt::Debug + 'static,
587 sapling::TransferData<AnchorV>: Arbitrary,
588{
589 type Parameters = ();
590
591 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
592 (
593 any::<Amount>(),
594 any::<sapling::TransferData<AnchorV>>(),
595 vec(any::<u8>(), 64),
596 )
597 .prop_map(|(value_balance, transfers, sig_bytes)| Self {
598 value_balance,
599 transfers,
600 binding_sig: redjubjub::Signature::from({
601 let mut b = [0u8; 64];
602 b.copy_from_slice(sig_bytes.as_slice());
603 b
604 }),
605 })
606 .boxed()
607 }
608
609 type Strategy = BoxedStrategy<Self>;
610}
611
612impl Arbitrary for sapling::TransferData<PerSpendAnchor> {
613 type Parameters = ();
614
615 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
616 vec(any::<sapling::Output>(), 0..MAX_ARBITRARY_ITEMS)
617 .prop_flat_map(|outputs| {
618 (
619 if outputs.is_empty() {
620 vec(
622 any::<sapling::Spend<PerSpendAnchor>>(),
623 1..MAX_ARBITRARY_ITEMS,
624 )
625 } else {
626 vec(
627 any::<sapling::Spend<PerSpendAnchor>>(),
628 0..MAX_ARBITRARY_ITEMS,
629 )
630 },
631 Just(outputs),
632 )
633 })
634 .prop_map(|(spends, outputs)| {
635 if !spends.is_empty() {
636 sapling::TransferData::SpendsAndMaybeOutputs {
637 shared_anchor: FieldNotPresent,
638 spends: spends.try_into().unwrap(),
639 maybe_outputs: outputs,
640 }
641 } else if !outputs.is_empty() {
642 sapling::TransferData::JustOutputs {
643 outputs: outputs.try_into().unwrap(),
644 }
645 } else {
646 unreachable!("there must be at least one generated spend or output")
647 }
648 })
649 .boxed()
650 }
651
652 type Strategy = BoxedStrategy<Self>;
653}
654
655impl Arbitrary for sapling::TransferData<SharedAnchor> {
656 type Parameters = ();
657
658 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
659 vec(any::<sapling::Output>(), 0..MAX_ARBITRARY_ITEMS)
660 .prop_flat_map(|outputs| {
661 (
662 any::<sapling::tree::Root>(),
663 if outputs.is_empty() {
664 vec(
666 any::<sapling::Spend<SharedAnchor>>(),
667 1..MAX_ARBITRARY_ITEMS,
668 )
669 } else {
670 vec(
671 any::<sapling::Spend<SharedAnchor>>(),
672 0..MAX_ARBITRARY_ITEMS,
673 )
674 },
675 Just(outputs),
676 )
677 })
678 .prop_map(|(shared_anchor, spends, outputs)| {
679 if !spends.is_empty() {
680 sapling::TransferData::SpendsAndMaybeOutputs {
681 shared_anchor,
682 spends: spends.try_into().unwrap(),
683 maybe_outputs: outputs,
684 }
685 } else if !outputs.is_empty() {
686 sapling::TransferData::JustOutputs {
687 outputs: outputs.try_into().unwrap(),
688 }
689 } else {
690 unreachable!("there must be at least one generated spend or output")
691 }
692 })
693 .boxed()
694 }
695
696 type Strategy = BoxedStrategy<Self>;
697}
698
699impl Arbitrary for orchard::ShieldedData {
700 type Parameters = ();
701
702 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
703 (
704 any::<orchard::shielded_data::Flags>(),
705 any::<Amount>(),
706 any::<orchard::tree::Root>(),
707 vec(
708 any::<orchard::shielded_data::AuthorizedAction>(),
709 1..MAX_ARBITRARY_ITEMS,
710 ),
711 any::<BindingSignature>(),
712 )
713 .prop_flat_map(
714 |(flags, value_balance, shared_anchor, actions, binding_sig)| {
715 let proof_size = orchard::shielded_data::expected_proof_size(actions.len());
722 (
723 Just(flags),
724 Just(value_balance),
725 Just(shared_anchor),
726 vec(any::<u8>(), proof_size).prop_map(Halo2Proof),
727 Just(actions),
728 Just(binding_sig),
729 )
730 },
731 )
732 .prop_map(
733 |(flags, value_balance, shared_anchor, proof, actions, binding_sig)| Self {
734 flags,
735 value_balance,
736 shared_anchor,
737 proof,
738 actions: actions
739 .try_into()
740 .expect("arbitrary vector size range produces at least one action"),
741 binding_sig: binding_sig.0,
742 },
743 )
744 .boxed()
745 }
746
747 type Strategy = BoxedStrategy<Self>;
748}
749
750#[derive(Copy, Clone, Debug, Eq, PartialEq)]
751struct BindingSignature(pub(crate) Signature<Binding>);
752
753impl Arbitrary for BindingSignature {
754 type Parameters = ();
755
756 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
757 (vec(any::<u8>(), 64))
758 .prop_filter_map(
759 "zero Signature::<Binding> values are invalid",
760 |sig_bytes| {
761 let mut b = [0u8; 64];
762 b.copy_from_slice(sig_bytes.as_slice());
763 if b == [0u8; 64] {
764 return None;
765 }
766 Some(BindingSignature(Signature::<Binding>::from(b)))
767 },
768 )
769 .boxed()
770 }
771
772 type Strategy = BoxedStrategy<Self>;
773}
774
775impl Arbitrary for Transaction {
776 type Parameters = LedgerState;
777
778 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
779 match ledger_state.transaction_version_override() {
780 Some(1) => return Self::v1_strategy(ledger_state),
781 Some(2) => return Self::v2_strategy(ledger_state),
782 Some(3) => return Self::v3_strategy(ledger_state),
783 Some(4) => return Self::v4_strategy(ledger_state),
784 Some(5) => return Self::v5_strategy(ledger_state),
785 Some(_) => unreachable!("invalid transaction version in override"),
786 None => {}
787 }
788
789 match ledger_state.network_upgrade() {
790 NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => {
791 Self::v1_strategy(ledger_state)
792 }
793 NetworkUpgrade::Overwinter => Self::v2_strategy(ledger_state),
794 NetworkUpgrade::Sapling => Self::v3_strategy(ledger_state),
795 NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
796 Self::v4_strategy(ledger_state)
797 }
798 NetworkUpgrade::Nu5
799 | NetworkUpgrade::Nu6
800 | NetworkUpgrade::Nu6_1
801 | NetworkUpgrade::Nu6_2
802 | NetworkUpgrade::Nu6_3
803 | NetworkUpgrade::Nu7 => prop_oneof![
804 Self::v4_strategy(ledger_state.clone()),
805 Self::v5_strategy(ledger_state)
806 ]
807 .boxed(),
808
809 #[cfg(zcash_unstable = "zfuture")]
810 NetworkUpgrade::ZFuture => prop_oneof![
811 Self::v4_strategy(ledger_state.clone()),
812 Self::v5_strategy(ledger_state)
813 ]
814 .boxed(),
815 }
816 }
817
818 type Strategy = BoxedStrategy<Self>;
819}
820
821impl Arbitrary for UnminedTx {
822 type Parameters = ();
823
824 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
825 any::<Transaction>().prop_map_into().boxed()
826 }
827
828 type Strategy = BoxedStrategy<Self>;
829}
830
831impl Arbitrary for VerifiedUnminedTx {
832 type Parameters = ();
833
834 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
835 (
836 any::<UnminedTx>(),
837 any::<Amount<NonNegative>>(),
838 any::<u32>(),
839 any::<u32>(),
840 any::<(u16, u16)>().prop_map(|(unpaid_actions, conventional_actions)| {
841 (
842 unpaid_actions % conventional_actions.saturating_add(1),
843 conventional_actions,
844 )
845 }),
846 any::<f32>(),
847 serialization::arbitrary::datetime_u32(),
848 any::<block::Height>(),
849 )
850 .prop_map(
851 |(
852 transaction,
853 miner_fee,
854 sigops,
855 p2sh_sigops,
856 (conventional_actions, mut unpaid_actions),
857 fee_weight_ratio,
858 time,
859 height,
860 )| {
861 if unpaid_actions > conventional_actions {
862 unpaid_actions = conventional_actions;
863 }
864
865 let conventional_actions = conventional_actions as u32;
866 let unpaid_actions = unpaid_actions as u32;
867
868 Self {
869 transaction,
870 miner_fee,
871 legacy_sigop_count: sigops,
872 p2sh_sigop_count: p2sh_sigops,
873 conventional_actions,
874 unpaid_actions,
875 fee_weight_ratio,
876 time: Some(time),
877 height: Some(height),
878 spent_outputs: std::sync::Arc::new(vec![]),
879 }
880 },
881 )
882 .boxed()
883 }
884 type Strategy = BoxedStrategy<Self>;
885}
886
887pub fn transaction_to_fake_v5(
892 trans: &Transaction,
893 network: &Network,
894 height: block::Height,
895) -> Transaction {
896 use Transaction::*;
897
898 let block_nu = NetworkUpgrade::current(network, height);
899
900 match trans {
901 V1 {
902 inputs,
903 outputs,
904 lock_time,
905 } => V5 {
906 network_upgrade: block_nu,
907 inputs: inputs.to_vec(),
908 outputs: outputs.to_vec(),
909 lock_time: *lock_time,
910 expiry_height: height,
911 sapling_shielded_data: None,
912 orchard_shielded_data: None,
913 },
914 V2 {
915 inputs,
916 outputs,
917 lock_time,
918 ..
919 } => V5 {
920 network_upgrade: block_nu,
921 inputs: inputs.to_vec(),
922 outputs: outputs.to_vec(),
923 lock_time: *lock_time,
924 expiry_height: height,
925 sapling_shielded_data: None,
926 orchard_shielded_data: None,
927 },
928 V3 {
929 inputs,
930 outputs,
931 lock_time,
932 ..
933 } => V5 {
934 network_upgrade: block_nu,
935 inputs: inputs.to_vec(),
936 outputs: outputs.to_vec(),
937 lock_time: *lock_time,
938 expiry_height: height,
939 sapling_shielded_data: None,
940 orchard_shielded_data: None,
941 },
942 V4 {
943 inputs,
944 outputs,
945 lock_time,
946 sapling_shielded_data,
947 ..
948 } => V5 {
949 network_upgrade: block_nu,
950 inputs: inputs.to_vec(),
951 outputs: outputs.to_vec(),
952 lock_time: *lock_time,
953 expiry_height: height,
954 sapling_shielded_data: sapling_shielded_data
955 .clone()
956 .and_then(sapling_shielded_v4_to_fake_v5),
957 orchard_shielded_data: None,
958 },
959 v5 @ V5 { .. } => v5.clone(),
960 v6 @ V6 { .. } => v6.clone(),
961 }
962}
963
964fn sapling_shielded_v4_to_fake_v5(
967 v4_shielded: sapling::ShieldedData<PerSpendAnchor>,
968) -> Option<sapling::ShieldedData<SharedAnchor>> {
969 use sapling::ShieldedData;
970 use sapling::TransferData::*;
971
972 let unique_anchors: Vec<_> = v4_shielded
973 .spends()
974 .map(|spend| spend.per_spend_anchor)
975 .unique()
976 .collect();
977
978 let fake_spends: Vec<_> = v4_shielded
979 .spends()
980 .cloned()
981 .map(sapling_spend_v4_to_fake_v5)
982 .collect();
983
984 let transfers = match v4_shielded.transfers {
985 SpendsAndMaybeOutputs { maybe_outputs, .. } => {
986 let shared_anchor = match unique_anchors.as_slice() {
987 [unique_anchor] => *unique_anchor,
988 _ => return None,
990 };
991
992 SpendsAndMaybeOutputs {
993 shared_anchor,
994 spends: fake_spends.try_into().unwrap(),
995 maybe_outputs,
996 }
997 }
998 JustOutputs { outputs } => JustOutputs { outputs },
999 };
1000
1001 let fake_shielded_v5 = ShieldedData::<SharedAnchor> {
1002 value_balance: v4_shielded.value_balance,
1003 transfers,
1004 binding_sig: v4_shielded.binding_sig,
1005 };
1006
1007 Some(fake_shielded_v5)
1008}
1009
1010fn sapling_spend_v4_to_fake_v5(
1012 v4_spend: sapling::Spend<PerSpendAnchor>,
1013) -> sapling::Spend<SharedAnchor> {
1014 use sapling::Spend;
1015
1016 Spend::<SharedAnchor> {
1017 cv: v4_spend.cv,
1018 per_spend_anchor: FieldNotPresent,
1019 nullifier: v4_spend.nullifier,
1020 rk: v4_spend.rk,
1021 zkproof: v4_spend.zkproof,
1022 spend_auth_sig: v4_spend.spend_auth_sig,
1023 }
1024}
1025
1026pub fn test_transactions(
1028 network: &Network,
1029) -> impl DoubleEndedIterator<Item = (block::Height, Arc<Transaction>)> {
1030 let blocks = network.block_iter();
1031
1032 transactions_from_blocks(blocks)
1033}
1034
1035pub fn v5_transactions<'b>(
1037 blocks: impl DoubleEndedIterator<Item = (&'b u32, &'b &'static [u8])> + 'b,
1038) -> impl DoubleEndedIterator<Item = Transaction> + 'b {
1039 transactions_from_blocks(blocks).filter_map(|(_, tx)| match *tx {
1040 Transaction::V1 { .. }
1041 | Transaction::V2 { .. }
1042 | Transaction::V3 { .. }
1043 | Transaction::V4 { .. } => None,
1044 ref tx @ Transaction::V5 { .. } => Some(tx.clone()),
1045 ref tx @ Transaction::V6 { .. } => Some(tx.clone()),
1046 })
1047}
1048
1049pub fn transactions_from_blocks<'a>(
1051 blocks: impl DoubleEndedIterator<Item = (&'a u32, &'a &'static [u8])> + 'a,
1052) -> impl DoubleEndedIterator<Item = (block::Height, Arc<Transaction>)> + 'a {
1053 blocks.flat_map(|(&block_height, &block_bytes)| {
1054 let block = block_bytes
1055 .zcash_deserialize_into::<block::Block>()
1056 .expect("block is structurally valid");
1057
1058 block
1059 .transactions
1060 .into_iter()
1061 .map(move |transaction| (block::Height(block_height), transaction))
1062 })
1063}
1064
1065pub fn insert_fake_orchard_shielded_data(
1078 transaction: &mut Transaction,
1079) -> &mut orchard::ShieldedData {
1080 let dummy_shielded_data = fake_v6_orchard_shielded_data(
1082 orchard::Flags::empty(),
1083 Amount::try_from(0).expect("invalid transaction amount"),
1084 1,
1085 );
1086
1087 match transaction {
1089 Transaction::V5 {
1090 orchard_shielded_data,
1091 ..
1092 } => {
1093 *orchard_shielded_data = Some(dummy_shielded_data);
1094
1095 orchard_shielded_data
1096 .as_mut()
1097 .expect("shielded data was just inserted")
1098 }
1099 _ => panic!("Fake V5 transaction is not V5"),
1100 }
1101}
1102
1103pub fn fake_v6_orchard_shielded_data(
1111 flags: orchard::Flags,
1112 value_balance: Amount<NegativeAllowed>,
1113 action_count: usize,
1114) -> orchard::ShieldedData {
1115 let mut runner = TestRunner::default();
1116 let dummy_action = orchard::Action::arbitrary()
1117 .new_tree(&mut runner)
1118 .unwrap()
1119 .current();
1120
1121 let dummy_authorized_action = orchard::AuthorizedAction {
1122 action: dummy_action,
1123 spend_auth_sig: Signature::from([0u8; 64]),
1124 };
1125
1126 let actions = vec![dummy_authorized_action; action_count.max(1)];
1127
1128 orchard::ShieldedData {
1129 flags,
1130 value_balance,
1131 shared_anchor: orchard::tree::Root::default(),
1132 proof: Halo2Proof(vec![
1136 0u8;
1137 orchard::shielded_data::expected_proof_size(
1138 action_count.max(1)
1139 )
1140 ]),
1141 actions: actions.try_into().expect("action_count is at least one"),
1142 binding_sig: Signature::from([0u8; 64]),
1143 }
1144}
1145
1146pub fn fake_v6_transaction(
1152 network_upgrade: NetworkUpgrade,
1153 orchard_shielded_data: Option<orchard::ShieldedDataV6>,
1154 ironwood_shielded_data: Option<crate::ironwood::ShieldedData>,
1155) -> Transaction {
1156 Transaction::V6 {
1157 network_upgrade,
1158 lock_time: LockTime::unlocked(),
1159 expiry_height: block::Height(0),
1160 inputs: Vec::new(),
1161 outputs: Vec::new(),
1162 sapling_shielded_data: None,
1163 orchard_shielded_data,
1164 ironwood_shielded_data,
1165 }
1166}