Skip to main content

zebra_chain/transaction/
arbitrary.rs

1//! Arbitrary data generation for transaction proptests
2
3use 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
28/// The maximum number of arbitrary transactions, inputs, or outputs.
29///
30/// This size is chosen to provide interesting behaviour, but not be too large
31/// for debugging.
32pub const MAX_ARBITRARY_ITEMS: usize = 4;
33
34// TODO: if needed, fixup transaction outputs
35//       (currently 0..=9 outputs, consensus rules require 1..)
36impl Transaction {
37    /// Generate a proptest strategy for V1 Transactions
38    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    /// Generate a proptest strategy for V2 Transactions
53    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    /// Generate a proptest strategy for V3 Transactions
72    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    /// Generate a proptest strategy for V4 Transactions
93    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                            // The genesis block should not contain any joinsplits.
118                            None
119                        } else {
120                            joinsplit_data
121                        },
122                        sapling_shielded_data: if ledger_state.height.is_min() {
123                            // The genesis block should not contain any shielded data.
124                            None
125                        } else {
126                            sapling_shielded_data
127                        },
128                    }
129                },
130            )
131            .boxed()
132    }
133
134    /// Generate a proptest strategy for V5 Transactions
135    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                            // The genesis block should not contain any shielded data.
167                            None
168                        } else {
169                            sapling_shielded_data
170                        },
171                        orchard_shielded_data: if ledger_state.height.is_min() {
172                            // The genesis block should not contain any shielded data.
173                            None
174                        } else {
175                            orchard_shielded_data
176                        },
177                    }
178                },
179            )
180            .boxed()
181    }
182
183    /// Proptest Strategy for creating a Vector of transactions where the first
184    /// transaction is always the only coinbase transaction
185    pub fn vec_strategy(
186        mut ledger_state: LedgerState,
187        len: usize,
188    ) -> BoxedStrategy<Vec<Arc<Self>>> {
189        // TODO: fixup coinbase miner subsidy
190        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    /// Apply `f` to the transparent output, `v_sprout_new`, and `v_sprout_old` values
206    /// in this transaction, regardless of version.
207    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    /// Apply `f` to the sapling value balance and orchard value balance
224    /// in this transaction, regardless of version.
225    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    /// Fixup transparent values and shielded value balances,
239    /// so that transaction and chain value pools won't overflow MAX_MONEY.
240    ///
241    /// These fixes are applied to coinbase and non-coinbase transactions.
242    //
243    // TODO: do we want to allow overflow, based on an arbitrary bool?
244    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            // inputs/joinsplits/spends|outputs/actions * pools * transactions
255            let transaction_pool_scaling_divisor =
256                max_arbitrary_items * POOL_COUNT * max_arbitrary_items;
257            // inputs/joinsplits/spends|outputs/actions * transactions * blocks
258            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    /// Fixup transparent values and shielded value balances,
270    /// so that this transaction passes the "non-negative chain value pool" checks.
271    /// (These checks use the sum of unspent outputs for each transparent and shielded pool.)
272    ///
273    /// These fixes are applied to coinbase and non-coinbase transactions.
274    ///
275    /// `chain_value_pools` contains the chain value pool balances,
276    /// as of the previous transaction in this block
277    /// (or the last transaction in the previous block).
278    ///
279    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
280    ///
281    /// Currently, these fixes almost always leave some remaining value in each transparent
282    /// and shielded chain value pool.
283    ///
284    /// Before fixing the chain value balances, this method calls `fix_overflow`
285    /// to make sure that transaction and chain value pools don't overflow MAX_MONEY.
286    ///
287    /// After fixing the chain value balances, this method calls `fix_remaining_value`
288    /// to fix the remaining value in the transaction value pool.
289    ///
290    /// Returns the remaining transaction value, and the updated chain value balances.
291    ///
292    /// # Panics
293    ///
294    /// If any spent [`transparent::Output`] is missing from
295    /// [`transparent::OutPoint`]s.
296    //
297    // TODO: take some extra arbitrary flags, which select between zero and non-zero
298    //       remaining value in each chain value pool
299    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        // a temporary value used to check that inputs don't break the chain value balance
307        // consensus rules
308        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        // update the input chain value pools,
317        // zeroing any inputs that would exceed the input value
318
319        // TODO: consensus rule: normalise sprout JoinSplit values
320        //       so at least one of the values in each JoinSplit is zero
321        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                // set the invalid input value to zero
327                Err(_) => *input = Amount::zero(),
328            }
329        }
330
331        // positive value balances subtract from the chain value pool
332
333        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        // check our calculations are correct
352        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    /// Returns the total input value of this transaction's value pool.
374    ///
375    /// This is the sum of transparent inputs, sprout input values,
376    /// and if positive, the sapling and orchard value balances.
377    ///
378    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
379    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        // TODO: fix callers which cause overflows, check for:
390        //       cached `outputs` that don't go through `fix_overflow`, and
391        //       values much larger than MAX_MONEY
392        //.expect("chain is limited to MAX_MONEY");
393
394        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        // positive value balances add to the transaction value pool
400        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    /// Fixup non-coinbase transparent values and shielded value balances,
420    /// so that this transaction passes the "non-negative remaining transaction value"
421    /// check. (This check uses the sum of inputs minus outputs.)
422    ///
423    /// Returns the remaining transaction value.
424    ///
425    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
426    ///
427    /// Currently, these fixes almost always leave some remaining value in the
428    /// transaction value pool.
429    ///
430    /// # Panics
431    ///
432    /// If any spent [`transparent::Output`] is missing from
433    /// [`transparent::OutPoint`]s.
434    //
435    // TODO: split this method up, after we've implemented chain value balance adjustments
436    //
437    // TODO: take an extra arbitrary bool, which selects between zero and non-zero
438    //       remaining value in the transaction value pool
439    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            // TODO: if needed, fixup coinbase:
445            // - miner subsidy
446            // - founders reward or funding streams (hopefully not?)
447            // - remaining transaction value
448
449            // Act as if the generated test case spends all the miner subsidy, miner fees, and
450            // founders reward / funding stream correctly.
451            return Ok(Amount::zero());
452        }
453
454        let mut remaining_input_value = self.input_value_pool(outputs)?;
455
456        // assign remaining input value to outputs,
457        // zeroing any outputs that would exceed the input value
458
459        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        // check our calculations are correct
500        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
536/// Generates arbitrary [`LockTime`]s.
537impl 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                        // must have at least one spend or output
621                        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                        // must have at least one spend or output
665                        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                    // Since NU6.2, an Orchard proof must have the canonical length for its number of
716                    // actions (`2272 * num_actions + 2720` bytes), otherwise it is rejected as
717                    // non-canonical (GHSA-jfw5-j458-pfv6). The V5 txid is computed by round-tripping
718                    // through `librustzcash`, which enforces this length, so a proof of any other
719                    // size makes the round-trip (and thus `Transaction::hash`) fail. Generate a proof
720                    // of exactly the expected length, which depends on the number of actions.
721                    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
887// Utility functions
888
889/// Convert `trans` into a fake v5 transaction,
890/// converting sapling shielded data from v4 to v5 if possible.
891pub 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
964/// Convert a v4 sapling shielded data into a fake v5 sapling shielded data,
965/// if possible.
966fn 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                // Multiple different anchors, can't convert to v5
989                _ => 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
1010/// Convert a v4 sapling spend into a fake v5 sapling spend.
1011fn 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
1026/// Iterate over V4 transactions in the block test vectors for the specified `network`.
1027pub 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
1035/// Returns an iterator over V5 transactions extracted from the given blocks.
1036pub 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
1049/// Generate an iterator over ([`block::Height`], [`Arc<Transaction>`]).
1050pub 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
1065/// Modify a V5 transaction to insert fake Orchard shielded data.
1066///
1067/// Creates a fake instance of [`orchard::ShieldedData`] with one fake action. Note that both the
1068/// action and the shielded data are invalid and shouldn't be used in tests that require them to be
1069/// valid.
1070///
1071/// A mutable reference to the inserted shielded data is returned, so that the caller can further
1072/// customize it if required.
1073///
1074/// # Panics
1075///
1076/// Panics if the transaction to be modified is not V5.
1077pub fn insert_fake_orchard_shielded_data(
1078    transaction: &mut Transaction,
1079) -> &mut orchard::ShieldedData {
1080    // A single-action dummy bundle with no flags and a zero value balance.
1081    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    // Replace the shielded data in the transaction
1088    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
1103/// Builds a cryptographically-INVALID v6 Orchard-protocol [`orchard::ShieldedData`] for structural
1104/// NU6.3 consensus-rule tests.
1105///
1106/// The bundle has the given `flags` and `value_balance`, and `action_count` copies of a single
1107/// dummy action — so all actions share one nullifier, which is convenient for duplicate-nullifier
1108/// tests. The proof is empty (not canonically sized) and the signatures are not valid, so this MUST
1109/// NOT be used where proof verification or a canonical proof size is required.
1110pub 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        // A canonically-sized (zero-filled) proof, so librustzcash's parser — which enforces the
1133        // canonical proof size — accepts the bundle when the wire deserializer round-trips through
1134        // it. The proof is not cryptographically valid.
1135        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
1146/// Builds a minimal NU6.3 v6 [`Transaction`] carrying the given optional Orchard-v6 and Ironwood
1147/// bundles, for structural consensus-rule tests.
1148///
1149/// The transaction is non-coinbase (no inputs) and otherwise empty. The bundles are not
1150/// cryptographically valid (see [`fake_v6_orchard_shielded_data`]).
1151pub 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}