Skip to main content

zebra_chain/transaction/
arbitrary.rs

1//! Arbitrary data generation for transaction proptests
2
3pub mod shielded;
4
5pub use shielded::{
6    fake_bundle_for_branch, fake_orchard_bundle, fake_orchard_bundle_duplicate_nullifiers,
7    fake_orchard_bundle_with_note, fake_v6_transaction, insert_fake_orchard_shielded_data,
8    outputs_enabled_flags, v6_ironwood_flags_offset, v6_orchard_flags_offset,
9    with_garbage_orchard_authorization, with_orchard_flags, with_orchard_value_balance,
10};
11
12use std::{cmp::max, collections::HashMap, ops::Neg, sync::Arc};
13
14use chrono::{TimeZone, Utc};
15use proptest::{array, collection::vec, option, prelude::*};
16use reddsa::{orchard::Binding, Signature};
17
18use crate::{
19    amount::{self, Amount, NegativeAllowed, NonNegative},
20    block::{self, arbitrary::MAX_PARTIAL_CHAIN_BLOCKS},
21    orchard,
22    parameters::{Network, NetworkUpgrade},
23    primitives::{Halo2Proof, ZkSnarkProof},
24    sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor},
25    serialization::{self, ZcashDeserializeInto},
26    sprout, transparent,
27    value_balance::{ValueBalance, ValueBalanceError},
28    LedgerState,
29};
30
31use zcash_primitives::transaction::TxVersion;
32use zcash_transparent;
33
34use super::{
35    FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction, UnminedTx, VerifiedUnminedTx,
36};
37
38/// Returns the librustzcash consensus branch ID for `network_upgrade`, falling back to
39/// `fallback` when the upgrade has no branch ID that librustzcash recognises.
40///
41/// Shielded bundles must be built for the branch of the transaction that will carry them, since
42/// the branch selects the bundle version and therefore which flags are representable. The
43/// fallback must match the one the corresponding `Transaction::test_v*` constructor uses, or the
44/// bundle would be built for a different branch than the transaction it ends up in. In
45/// particular NU7's branch ID is behind an unstable feature, so a v6 transaction at NU7 falls
46/// back to NU6.3 — the branch where v6 and the Ironwood pool were introduced.
47fn branch_id_of(
48    network_upgrade: NetworkUpgrade,
49    fallback: zcash_protocol::consensus::BranchId,
50) -> zcash_protocol::consensus::BranchId {
51    network_upgrade
52        .branch_id()
53        .and_then(|cbid| zcash_protocol::consensus::BranchId::try_from(cbid).ok())
54        .unwrap_or(fallback)
55}
56
57/// The maximum number of arbitrary transactions, inputs, or outputs.
58///
59/// This size is chosen to provide interesting behaviour, but not be too large
60/// for debugging.
61pub const MAX_ARBITRARY_ITEMS: usize = 4;
62
63// TODO: if needed, fixup transaction outputs
64//       (currently 0..=9 outputs, consensus rules require 1..)
65impl Transaction {
66    /// Generate a proptest strategy for V1 Transactions
67    pub fn v1_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
68        (
69            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
70            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
71            any::<LockTime>(),
72        )
73            .prop_map(|(inputs, outputs, lock_time)| {
74                Transaction::test_v1(inputs, outputs, lock_time)
75            })
76            .boxed()
77    }
78
79    /// Generate a proptest strategy for V2 Transactions
80    ///
81    /// Note: the new Transaction type doesn't support arbitrary Sprout JoinSplit data
82    /// in proptest strategies, so this generates transparent-only V2 transactions.
83    pub fn v2_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
84        (
85            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
86            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
87            any::<LockTime>(),
88        )
89            .prop_map(|(inputs, outputs, lock_time)| {
90                Transaction::test_v2(inputs, outputs, lock_time)
91            })
92            .boxed()
93    }
94
95    /// Generate a proptest strategy for V3 Transactions
96    ///
97    /// Note: the new Transaction type doesn't support arbitrary Sprout JoinSplit data
98    /// in proptest strategies, so this generates transparent-only V3 transactions.
99    pub fn v3_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
100        (
101            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
102            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
103            any::<LockTime>(),
104            any::<block::Height>(),
105        )
106            .prop_map(|(inputs, outputs, lock_time, expiry_height)| {
107                Transaction::test_v3(inputs, outputs, lock_time, expiry_height)
108            })
109            .boxed()
110    }
111
112    /// Generate a proptest strategy for V4 Transactions
113    ///
114    /// Note: the new Transaction type doesn't support arbitrary Sapling/Sprout shielded
115    /// data in proptest strategies, so this generates transparent-only V4 transactions.
116    pub fn v4_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
117        (
118            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
119            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
120            any::<LockTime>(),
121            any::<block::Height>(),
122        )
123            .prop_map(|(inputs, outputs, lock_time, expiry_height)| {
124                Transaction::test_v4(inputs, outputs, lock_time, expiry_height)
125            })
126            .boxed()
127    }
128
129    /// Generate a proptest strategy for V5 Transactions
130    ///
131    /// Note: the new Transaction type doesn't support arbitrary Sapling/Orchard shielded
132    /// data in proptest strategies, so this generates transparent-only V5 transactions.
133    pub fn v5_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
134        (
135            NetworkUpgrade::nu5_branch_id_strategy(),
136            any::<LockTime>(),
137            any::<block::Height>(),
138            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
139            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
140            option::of((1usize..=2usize, any::<u64>())),
141        )
142            .prop_map(
143                move |(network_upgrade, lock_time, expiry_height, inputs, outputs, orchard)| {
144                    let nu = if ledger_state.transaction_has_valid_network_upgrade() {
145                        // Use the ledger's network upgrade if it has a consensus branch ID
146                        // (V5 transactions can only embed known branch IDs).
147                        let ledger_nu = ledger_state.network_upgrade();
148                        if ledger_nu.branch_id().is_some() {
149                            ledger_nu
150                        } else {
151                            network_upgrade
152                        }
153                    } else {
154                        network_upgrade
155                    };
156
157                    // The genesis block must not contain any shielded data.
158                    let orchard_bundle = (!ledger_state.height.is_min())
159                        .then_some(orchard)
160                        .flatten()
161                        .and_then(|(n_actions, seed)| {
162                            shielded::fake_bundle_for_branch(
163                                branch_id_of(nu, zcash_protocol::consensus::BranchId::Nu5),
164                                ::orchard::ValuePool::Orchard,
165                                n_actions,
166                                seed,
167                            )
168                        });
169
170                    Transaction::test_v5_with_orchard(
171                        nu,
172                        inputs,
173                        outputs,
174                        lock_time,
175                        expiry_height,
176                        orchard_bundle,
177                    )
178                },
179            )
180            .boxed()
181    }
182
183    /// Generate a proptest strategy for V6 Transactions
184    ///
185    /// Note: like [`Self::v5_strategy`], the new Transaction type doesn't support arbitrary
186    /// shielded data in proptest strategies, so this generates transparent-only V6 transactions.
187    pub fn v6_strategy(ledger_state: LedgerState) -> BoxedStrategy<Self> {
188        (
189            NetworkUpgrade::nu6_3_branch_id_strategy(),
190            any::<LockTime>(),
191            any::<block::Height>(),
192            transparent::Input::vec_strategy(&ledger_state, MAX_ARBITRARY_ITEMS),
193            vec(any::<transparent::Output>(), 0..MAX_ARBITRARY_ITEMS),
194            option::of((1usize..=2usize, any::<u64>())),
195            option::of((1usize..=2usize, any::<u64>())),
196        )
197            .prop_map(
198                move |(
199                    network_upgrade,
200                    lock_time,
201                    expiry_height,
202                    inputs,
203                    outputs,
204                    orchard,
205                    ironwood,
206                )| {
207                    let nu = if ledger_state.transaction_has_valid_network_upgrade() {
208                        let ledger_nu = ledger_state.network_upgrade();
209                        if ledger_nu.branch_id().is_some() {
210                            ledger_nu
211                        } else {
212                            network_upgrade
213                        }
214                    } else {
215                        network_upgrade
216                    };
217
218                    // The genesis block must not contain any shielded data.
219                    let is_genesis = ledger_state.height.is_min();
220
221                    let orchard_bundle =
222                        (!is_genesis)
223                            .then_some(orchard)
224                            .flatten()
225                            .and_then(|(n_actions, seed)| {
226                                shielded::fake_bundle_for_branch(
227                                    branch_id_of(nu, zcash_protocol::consensus::BranchId::Nu6_3),
228                                    ::orchard::ValuePool::Orchard,
229                                    n_actions,
230                                    seed,
231                                )
232                            });
233
234                    // Offset the Ironwood seeds so the two pools in one transaction never share
235                    // nullifiers (they are disjoint sets, but the tests read more clearly when
236                    // the generated values differ too).
237                    let ironwood_bundle = (!is_genesis).then_some(ironwood).flatten().and_then(
238                        |(n_actions, seed)| {
239                            shielded::fake_bundle_for_branch(
240                                branch_id_of(nu, zcash_protocol::consensus::BranchId::Nu6_3),
241                                ::orchard::ValuePool::Ironwood,
242                                n_actions,
243                                seed ^ 0xEEEE_0000_u64,
244                            )
245                        },
246                    );
247
248                    Transaction::test_v6_with_bundles(
249                        nu,
250                        inputs,
251                        outputs,
252                        lock_time,
253                        expiry_height,
254                        orchard_bundle,
255                        ironwood_bundle,
256                    )
257                },
258            )
259            .boxed()
260    }
261
262    /// Proptest Strategy for creating a Vector of transactions where the first
263    /// transaction is always the only coinbase transaction
264    pub fn vec_strategy(
265        mut ledger_state: LedgerState,
266        len: usize,
267    ) -> BoxedStrategy<Vec<Arc<Self>>> {
268        // TODO: fixup coinbase miner subsidy
269        let coinbase = Transaction::arbitrary_with(ledger_state.clone()).prop_map(Arc::new);
270        ledger_state.has_coinbase = false;
271        let remainder = vec(
272            Transaction::arbitrary_with(ledger_state).prop_map(Arc::new),
273            0..=len,
274        );
275
276        (coinbase, remainder)
277            .prop_map(|(first, mut remainder)| {
278                remainder.insert(0, first);
279                remainder
280            })
281            .boxed()
282    }
283
284    /// Apply `f` to the transparent output values in this transaction.
285    ///
286    /// Note: Sprout/sapling/orchard value mutations are not supported with
287    /// the new Transaction type (proptest strategies generate transparent-only txs).
288    pub fn for_each_value_mut<F>(&mut self, mut f: F)
289    where
290        F: FnMut(&mut Amount<NonNegative>),
291    {
292        let mut outputs = self.outputs();
293        let mut changed = false;
294        for output in &mut outputs {
295            let old = output.value;
296            f(&mut output.value);
297            if output.value != old {
298                changed = true;
299            }
300        }
301        if changed {
302            *self = self.clone().with_transparent_outputs(outputs);
303        }
304    }
305
306    /// Apply `f` to the sapling value balance and orchard value balance.
307    ///
308    /// Note: Not implemented for the new Transaction type since proptest strategies
309    /// generate transparent-only transactions. This is a no-op.
310    pub fn for_each_value_balance_mut<F>(&mut self, _f: F)
311    where
312        F: FnMut(&mut Amount<NegativeAllowed>),
313    {
314        // The shielded bundles our strategies build always have a zero value balance
315        // (see `shielded::fake_bundle_for_branch`), so there is nothing to scale.
316        // If a strategy ever generates a non-zero one, this must rebuild the bundle:
317        // `zcash_primitives` bundles are immutable, so there is no value balance to take
318        // a `&mut` to.
319    }
320
321    /// Fixup transparent values and shielded value balances,
322    /// so that transaction and chain value pools won't overflow MAX_MONEY.
323    ///
324    /// These fixes are applied to coinbase and non-coinbase transactions.
325    //
326    // TODO: do we want to allow overflow, based on an arbitrary bool?
327    pub fn fix_overflow(&mut self) {
328        fn scale_to_avoid_overflow<C: amount::Constraint>(amount: &mut Amount<C>)
329        where
330            Amount<C>: Copy,
331        {
332            // transparent, sprout, sapling, orchard, and ironwood
333            const POOL_COUNT: u64 = 5;
334
335            let max_arbitrary_items: u64 = MAX_ARBITRARY_ITEMS.try_into().unwrap();
336            let max_partial_chain_blocks: u64 = MAX_PARTIAL_CHAIN_BLOCKS.try_into().unwrap();
337
338            // inputs/joinsplits/spends|outputs/actions * pools * transactions
339            let transaction_pool_scaling_divisor =
340                max_arbitrary_items * POOL_COUNT * max_arbitrary_items;
341            // inputs/joinsplits/spends|outputs/actions * transactions * blocks
342            let chain_pool_scaling_divisor =
343                max_arbitrary_items * max_arbitrary_items * max_partial_chain_blocks;
344            let scaling_divisor = max(transaction_pool_scaling_divisor, chain_pool_scaling_divisor);
345
346            *amount = (*amount / scaling_divisor).expect("divisor is not zero");
347        }
348
349        self.for_each_value_mut(scale_to_avoid_overflow);
350        // Shielded value balances are zero in proptest transactions, no fixup needed.
351    }
352
353    /// Fixup transparent values and shielded value balances,
354    /// so that this transaction passes the "non-negative chain value pool" checks.
355    /// (These checks use the sum of unspent outputs for each transparent and shielded pool.)
356    ///
357    /// These fixes are applied to coinbase and non-coinbase transactions.
358    ///
359    /// `chain_value_pools` contains the chain value pool balances,
360    /// as of the previous transaction in this block
361    /// (or the last transaction in the previous block).
362    ///
363    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
364    ///
365    /// Currently, these fixes almost always leave some remaining value in each transparent
366    /// and shielded chain value pool.
367    ///
368    /// Before fixing the chain value balances, this method calls `fix_overflow`
369    /// to make sure that transaction and chain value pools don't overflow MAX_MONEY.
370    ///
371    /// After fixing the chain value balances, this method calls `fix_remaining_value`
372    /// to fix the remaining value in the transaction value pool.
373    ///
374    /// Returns the remaining transaction value, and the updated chain value balances.
375    ///
376    /// # Panics
377    ///
378    /// If any spent [`transparent::Output`] is missing from
379    /// [`transparent::OutPoint`]s.
380    //
381    // TODO: take some extra arbitrary flags, which select between zero and non-zero
382    //       remaining value in each chain value pool
383    pub fn fix_chain_value_pools(
384        &mut self,
385        chain_value_pools: ValueBalance<NonNegative>,
386        outputs: &HashMap<transparent::OutPoint, transparent::Output>,
387    ) -> Result<(Amount<NonNegative>, ValueBalance<NonNegative>), ValueBalanceError> {
388        self.fix_overflow();
389
390        // a temporary value used to check that inputs don't break the chain value balance
391        // consensus rules
392        let mut input_chain_value_pools = chain_value_pools;
393
394        for input in self.inputs() {
395            input_chain_value_pools = input_chain_value_pools
396                .add_transparent_input(input, outputs)
397                .expect("find_valid_utxo_for_spend only spends unspent transparent outputs");
398        }
399
400        // update the input chain value pools,
401        // zeroing any inputs that would exceed the input value
402        //
403        // Note: Sprout, Sapling, Orchard, and Ironwood pool mutations are skipped here
404        // because proptest strategies generate transparent-only transactions.
405        // Those value balances are always zero and never exceed the chain pool.
406
407        let remaining_transaction_value = self.fix_remaining_value(outputs)?;
408
409        // check our calculations are correct
410        let transaction_chain_value_pool_change =
411            self
412            .transparent_value_balance_from_outputs(outputs)
413            .expect("chain value pool and remaining transaction value fixes produce valid transaction value balances")
414            .neg();
415
416        let chain_value_pools = chain_value_pools
417            .add_transaction(self, outputs)
418            .unwrap_or_else(|err| {
419                panic!(
420                    "unexpected chain value pool error: {err:?}, \n\
421                     original chain value pools: {chain_value_pools:?}, \n\
422                     transaction chain value change: {transaction_chain_value_pool_change:?}, \n\
423                     input-only transaction chain value pools: {input_chain_value_pools:?}, \n\
424                     calculated remaining transaction value: {remaining_transaction_value:?}",
425                )
426            });
427
428        Ok((remaining_transaction_value, chain_value_pools))
429    }
430
431    /// Returns the total input value of this transaction's value pool.
432    ///
433    /// This is the sum of transparent inputs, sprout input values,
434    /// and if positive, the sapling, orchard, and ironwood value balances.
435    ///
436    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
437    fn input_value_pool(
438        &self,
439        outputs: &HashMap<transparent::OutPoint, transparent::Output>,
440    ) -> Result<Amount<NonNegative>, ValueBalanceError> {
441        let transparent_inputs = self
442            .inputs()
443            .iter()
444            .map(|input| input.value_from_outputs(outputs))
445            .sum::<Result<Amount<NonNegative>, amount::Error>>()
446            .map_err(ValueBalanceError::Transparent)?;
447        // TODO: fix callers which cause overflows, check for:
448        //       cached `outputs` that don't go through `fix_overflow`, and
449        //       values much larger than MAX_MONEY
450        //.expect("chain is limited to MAX_MONEY");
451
452        // Proptest transactions don't have Sprout joinsplits, so sprout_inputs is always zero.
453        let sprout_inputs = Amount::<NonNegative>::zero();
454
455        // positive value balances add to the transaction value pool
456        let sapling_input = self
457            .sapling_value_balance()
458            .sapling_amount()
459            .constrain::<NonNegative>()
460            .unwrap_or_else(|_| Amount::zero());
461
462        let orchard_input = self
463            .orchard_value_balance()
464            .orchard_amount()
465            .constrain::<NonNegative>()
466            .unwrap_or_else(|_| Amount::zero());
467
468        let ironwood_input = self
469            .ironwood_value_balance()
470            .ironwood_amount()
471            .constrain::<NonNegative>()
472            .unwrap_or_else(|_| Amount::zero());
473
474        let transaction_input_value_pool =
475            (transparent_inputs + sprout_inputs + sapling_input + orchard_input + ironwood_input)
476                .expect("chain is limited to MAX_MONEY");
477
478        Ok(transaction_input_value_pool)
479    }
480
481    /// Fixup non-coinbase transparent values and shielded value balances,
482    /// so that this transaction passes the "non-negative remaining transaction value"
483    /// check. (This check uses the sum of inputs minus outputs.)
484    ///
485    /// Returns the remaining transaction value.
486    ///
487    /// `outputs` must contain all the [`transparent::Output`]s spent in this transaction.
488    ///
489    /// Currently, these fixes almost always leave some remaining value in the
490    /// transaction value pool.
491    ///
492    /// # Panics
493    ///
494    /// If any spent [`transparent::Output`] is missing from
495    /// [`transparent::OutPoint`]s.
496    //
497    // TODO: split this method up, after we've implemented chain value balance adjustments
498    //
499    // TODO: take an extra arbitrary bool, which selects between zero and non-zero
500    //       remaining value in the transaction value pool
501    pub fn fix_remaining_value(
502        &mut self,
503        outputs: &HashMap<transparent::OutPoint, transparent::Output>,
504    ) -> Result<Amount<NonNegative>, ValueBalanceError> {
505        if self.is_coinbase() {
506            // TODO: if needed, fixup coinbase:
507            // - miner subsidy
508            // - founders reward or funding streams (hopefully not?)
509            // - remaining transaction value
510
511            // Act as if the generated test case spends all the miner subsidy, miner fees, and
512            // founders reward / funding stream correctly.
513            return Ok(Amount::zero());
514        }
515
516        let mut remaining_input_value = self.input_value_pool(outputs)?;
517
518        // assign remaining input value to outputs,
519        // zeroing any outputs that would exceed the input value
520        let mut tx_outputs = self.outputs();
521        let mut outputs_changed = false;
522        for output in &mut tx_outputs {
523            if remaining_input_value >= output.value {
524                remaining_input_value = (remaining_input_value - output.value)
525                    .expect("input >= output so result is always non-negative");
526            } else {
527                output.value = Amount::zero();
528                outputs_changed = true;
529            }
530        }
531        if outputs_changed {
532            *self = self.clone().with_transparent_outputs(tx_outputs);
533        }
534
535        // Sprout, Sapling, Orchard, and Ironwood output values are zero in proptest
536        // transactions, so there is nothing to fix up for the shielded pools.
537
538        // check our calculations are correct
539        let remaining_transaction_value = self
540            .transparent_value_balance_from_outputs(outputs)
541            .expect("chain is limited to MAX_MONEY")
542            .remaining_transaction_value()
543            .unwrap_or_else(|err| {
544                panic!(
545                    "unexpected remaining transaction value: {err:?}, \
546                     calculated remaining input value: {remaining_input_value:?}"
547                )
548            });
549        assert_eq!(
550            remaining_input_value,
551            remaining_transaction_value,
552            "fix_remaining_value and remaining_transaction_value calculated different remaining values"
553        );
554
555        Ok(remaining_transaction_value)
556    }
557}
558
559impl Arbitrary for Memo {
560    type Parameters = ();
561
562    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
563        (vec(any::<u8>(), 512))
564            .prop_map(|v| {
565                let mut bytes = [0; 512];
566                bytes.copy_from_slice(v.as_slice());
567                Memo(Box::new(bytes))
568            })
569            .boxed()
570    }
571
572    type Strategy = BoxedStrategy<Self>;
573}
574
575/// Generates arbitrary [`LockTime`]s.
576impl Arbitrary for LockTime {
577    type Parameters = ();
578
579    fn arbitrary_with(_args: ()) -> Self::Strategy {
580        prop_oneof![
581            (block::Height::MIN.0..=LockTime::MAX_HEIGHT.0)
582                .prop_map(|n| LockTime::Height(block::Height(n))),
583            (LockTime::MIN_TIMESTAMP..=LockTime::MAX_TIMESTAMP).prop_map(|n| {
584                LockTime::Time(
585                    Utc.timestamp_opt(n, 0)
586                        .single()
587                        .expect("in-range number of seconds and valid nanosecond"),
588                )
589            })
590        ]
591        .boxed()
592    }
593
594    type Strategy = BoxedStrategy<Self>;
595}
596
597impl<P: ZkSnarkProof + Arbitrary + 'static> Arbitrary for JoinSplitData<P> {
598    type Parameters = ();
599
600    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
601        (
602            any::<sprout::JoinSplit<P>>(),
603            vec(any::<sprout::JoinSplit<P>>(), 0..MAX_ARBITRARY_ITEMS),
604            array::uniform32(any::<u8>()),
605            vec(any::<u8>(), 64),
606        )
607            .prop_map(|(first, rest, pub_key_bytes, sig_bytes)| Self {
608                first,
609                rest,
610                pub_key: ed25519_zebra::VerificationKeyBytes::from(pub_key_bytes),
611                sig: ed25519_zebra::Signature::from({
612                    let mut b = [0u8; 64];
613                    b.copy_from_slice(sig_bytes.as_slice());
614                    b
615                }),
616            })
617            .boxed()
618    }
619
620    type Strategy = BoxedStrategy<Self>;
621}
622
623impl<AnchorV> Arbitrary for sapling::ShieldedData<AnchorV>
624where
625    AnchorV: AnchorVariant + Clone + std::fmt::Debug + 'static,
626    sapling::TransferData<AnchorV>: Arbitrary,
627{
628    type Parameters = ();
629
630    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
631        (
632            any::<Amount>(),
633            any::<sapling::TransferData<AnchorV>>(),
634            vec(any::<u8>(), 64),
635        )
636            .prop_map(|(value_balance, transfers, sig_bytes)| Self {
637                value_balance,
638                transfers,
639                binding_sig: redjubjub::Signature::from({
640                    let mut b = [0u8; 64];
641                    b.copy_from_slice(sig_bytes.as_slice());
642                    b
643                }),
644            })
645            .boxed()
646    }
647
648    type Strategy = BoxedStrategy<Self>;
649}
650
651impl Arbitrary for sapling::TransferData<PerSpendAnchor> {
652    type Parameters = ();
653
654    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
655        vec(any::<sapling::Output>(), 0..MAX_ARBITRARY_ITEMS)
656            .prop_flat_map(|outputs| {
657                (
658                    if outputs.is_empty() {
659                        // must have at least one spend or output
660                        vec(
661                            any::<sapling::Spend<PerSpendAnchor>>(),
662                            1..MAX_ARBITRARY_ITEMS,
663                        )
664                    } else {
665                        vec(
666                            any::<sapling::Spend<PerSpendAnchor>>(),
667                            0..MAX_ARBITRARY_ITEMS,
668                        )
669                    },
670                    Just(outputs),
671                )
672            })
673            .prop_map(|(spends, outputs)| {
674                if !spends.is_empty() {
675                    sapling::TransferData::SpendsAndMaybeOutputs {
676                        shared_anchor: FieldNotPresent,
677                        spends: spends.try_into().unwrap(),
678                        maybe_outputs: outputs,
679                    }
680                } else if !outputs.is_empty() {
681                    sapling::TransferData::JustOutputs {
682                        outputs: outputs.try_into().unwrap(),
683                    }
684                } else {
685                    unreachable!("there must be at least one generated spend or output")
686                }
687            })
688            .boxed()
689    }
690
691    type Strategy = BoxedStrategy<Self>;
692}
693
694impl Arbitrary for sapling::TransferData<SharedAnchor> {
695    type Parameters = ();
696
697    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
698        vec(any::<sapling::Output>(), 0..MAX_ARBITRARY_ITEMS)
699            .prop_flat_map(|outputs| {
700                (
701                    any::<sapling::tree::Root>(),
702                    if outputs.is_empty() {
703                        // must have at least one spend or output
704                        vec(
705                            any::<sapling::Spend<SharedAnchor>>(),
706                            1..MAX_ARBITRARY_ITEMS,
707                        )
708                    } else {
709                        vec(
710                            any::<sapling::Spend<SharedAnchor>>(),
711                            0..MAX_ARBITRARY_ITEMS,
712                        )
713                    },
714                    Just(outputs),
715                )
716            })
717            .prop_map(|(shared_anchor, spends, outputs)| {
718                if !spends.is_empty() {
719                    sapling::TransferData::SpendsAndMaybeOutputs {
720                        shared_anchor,
721                        spends: spends.try_into().unwrap(),
722                        maybe_outputs: outputs,
723                    }
724                } else if !outputs.is_empty() {
725                    sapling::TransferData::JustOutputs {
726                        outputs: outputs.try_into().unwrap(),
727                    }
728                } else {
729                    unreachable!("there must be at least one generated spend or output")
730                }
731            })
732            .boxed()
733    }
734
735    type Strategy = BoxedStrategy<Self>;
736}
737
738impl Arbitrary for orchard::ShieldedData {
739    type Parameters = ();
740
741    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
742        (
743            any::<orchard::shielded_data::Flags>(),
744            any::<Amount>(),
745            any::<orchard::tree::Root>(),
746            vec(
747                any::<orchard::shielded_data::AuthorizedAction>(),
748                1..MAX_ARBITRARY_ITEMS,
749            ),
750            any::<BindingSignature>(),
751        )
752            .prop_flat_map(
753                |(flags, value_balance, shared_anchor, actions, binding_sig)| {
754                    // Since NU6.2, an Orchard proof must have the canonical length for its number of
755                    // actions (`2272 * num_actions + 2720` bytes), otherwise it is rejected as
756                    // non-canonical (GHSA-jfw5-j458-pfv6). The V5 txid is computed by round-tripping
757                    // through `librustzcash`, which enforces this length, so a proof of any other
758                    // size makes the round-trip (and thus `Transaction::hash`) fail. Generate a proof
759                    // of exactly the expected length, which depends on the number of actions.
760                    let proof_size = orchard::shielded_data::expected_proof_size(actions.len());
761                    (
762                        Just(flags),
763                        Just(value_balance),
764                        Just(shared_anchor),
765                        vec(any::<u8>(), proof_size).prop_map(Halo2Proof),
766                        Just(actions),
767                        Just(binding_sig),
768                    )
769                },
770            )
771            .prop_map(
772                |(flags, value_balance, shared_anchor, proof, actions, binding_sig)| Self {
773                    flags,
774                    value_balance,
775                    shared_anchor,
776                    proof,
777                    actions: actions
778                        .try_into()
779                        .expect("arbitrary vector size range produces at least one action"),
780                    binding_sig: binding_sig.0,
781                },
782            )
783            .boxed()
784    }
785
786    type Strategy = BoxedStrategy<Self>;
787}
788
789impl Arbitrary for orchard::ShieldedDataV6 {
790    type Parameters = ();
791
792    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
793        // The v6 Orchard-pool bundle reserves `enableCrossAddress` exactly like v5, so the base
794        // `ShieldedData` strategy (which only generates the pre-NU6.3 flag bits) is reused as-is.
795        // Only the Ironwood bundle permits that flag; see the `ironwood::ShieldedData` strategy.
796        any::<orchard::ShieldedData>()
797            .prop_map(orchard::ShieldedDataV6::new)
798            .boxed()
799    }
800
801    type Strategy = BoxedStrategy<Self>;
802}
803
804#[derive(Copy, Clone, Debug, Eq, PartialEq)]
805struct BindingSignature(pub(crate) Signature<Binding>);
806
807impl Arbitrary for BindingSignature {
808    type Parameters = ();
809
810    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
811        (vec(any::<u8>(), 64))
812            .prop_filter_map(
813                "zero Signature::<Binding> values are invalid",
814                |sig_bytes| {
815                    let mut b = [0u8; 64];
816                    b.copy_from_slice(sig_bytes.as_slice());
817                    if b == [0u8; 64] {
818                        return None;
819                    }
820                    Some(BindingSignature(Signature::<Binding>::from(b)))
821                },
822            )
823            .boxed()
824    }
825
826    type Strategy = BoxedStrategy<Self>;
827}
828
829impl Arbitrary for Transaction {
830    type Parameters = LedgerState;
831
832    fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
833        match ledger_state.transaction_version_override() {
834            Some(1) => return Self::v1_strategy(ledger_state),
835            Some(2) => return Self::v2_strategy(ledger_state),
836            Some(3) => return Self::v3_strategy(ledger_state),
837            Some(4) => return Self::v4_strategy(ledger_state),
838            Some(5) => return Self::v5_strategy(ledger_state),
839            Some(6) => return Self::v6_strategy(ledger_state),
840            Some(_) => unreachable!("invalid transaction version in override"),
841            None => {}
842        }
843
844        match ledger_state.network_upgrade() {
845            NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => {
846                Self::v1_strategy(ledger_state)
847            }
848            NetworkUpgrade::Overwinter => Self::v2_strategy(ledger_state),
849            NetworkUpgrade::Sapling => Self::v3_strategy(ledger_state),
850            NetworkUpgrade::Blossom | NetworkUpgrade::Heartwood | NetworkUpgrade::Canopy => {
851                Self::v4_strategy(ledger_state)
852            }
853            NetworkUpgrade::Nu5
854            | NetworkUpgrade::Nu6
855            | NetworkUpgrade::Nu6_1
856            | NetworkUpgrade::Nu6_2 => prop_oneof![
857                Self::v4_strategy(ledger_state.clone()),
858                Self::v5_strategy(ledger_state)
859            ]
860            .boxed(),
861
862            // V6 transactions are only valid from NU6.3; v4 and v5 remain valid alongside them.
863            NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => prop_oneof![
864                Self::v4_strategy(ledger_state.clone()),
865                Self::v5_strategy(ledger_state.clone()),
866                Self::v6_strategy(ledger_state)
867            ]
868            .boxed(),
869
870            #[cfg(zcash_unstable = "zfuture")]
871            NetworkUpgrade::ZFuture => prop_oneof![
872                Self::v4_strategy(ledger_state.clone()),
873                Self::v5_strategy(ledger_state)
874            ]
875            .boxed(),
876        }
877    }
878
879    type Strategy = BoxedStrategy<Self>;
880}
881
882impl Arbitrary for UnminedTx {
883    type Parameters = ();
884
885    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
886        any::<Transaction>()
887            .prop_map(|tx| UnminedTx::from(Arc::new(tx)))
888            .boxed()
889    }
890
891    type Strategy = BoxedStrategy<Self>;
892}
893
894impl Arbitrary for VerifiedUnminedTx {
895    type Parameters = ();
896
897    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
898        (
899            any::<UnminedTx>(),
900            any::<Amount<NonNegative>>(),
901            any::<u32>(),
902            any::<u32>(),
903            any::<(u16, u16)>().prop_map(|(unpaid_actions, conventional_actions)| {
904                (
905                    unpaid_actions % conventional_actions.saturating_add(1),
906                    conventional_actions,
907                )
908            }),
909            any::<f32>(),
910            serialization::arbitrary::datetime_u32(),
911            any::<block::Height>(),
912        )
913            .prop_map(
914                |(
915                    transaction,
916                    miner_fee,
917                    sigops,
918                    p2sh_sigops,
919                    (conventional_actions, mut unpaid_actions),
920                    fee_weight_ratio,
921                    time,
922                    height,
923                )| {
924                    if unpaid_actions > conventional_actions {
925                        unpaid_actions = conventional_actions;
926                    }
927
928                    let conventional_actions = conventional_actions as u32;
929                    let unpaid_actions = unpaid_actions as u32;
930
931                    Self {
932                        transaction,
933                        miner_fee,
934                        legacy_sigop_count: sigops,
935                        p2sh_sigop_count: p2sh_sigops,
936                        conventional_actions,
937                        unpaid_actions,
938                        fee_weight_ratio,
939                        time: Some(time),
940                        height: Some(height),
941                        spent_outputs: std::sync::Arc::new(vec![]),
942                    }
943                },
944            )
945            .boxed()
946    }
947    type Strategy = BoxedStrategy<Self>;
948}
949
950// Utility functions
951
952/// Convert `trans` into a fake v5 transaction.
953///
954/// Takes the transparent inputs/outputs from `trans` and builds a new V5
955/// transaction at the given height. Used to test V5 sighash/serialization
956/// with real transparent data from the test vector blocks.
957pub fn transaction_to_fake_v5(
958    trans: &Transaction,
959    network: &Network,
960    height: block::Height,
961) -> Transaction {
962    let block_nu = NetworkUpgrade::current(network, height);
963
964    match trans.tx_version() {
965        // V5+ already in the right format; just clone
966        TxVersion::V5 | TxVersion::V6 => trans.clone(),
967        // For V1-V4: build a V5 with the same transparent data
968        _ => {
969            use crate::transaction::compat;
970            use zcash_primitives::transaction::{self as zp_tx, TxVersion};
971            use zcash_protocol::consensus::BranchId;
972
973            let branch_id = block_nu
974                .branch_id()
975                .and_then(|cbid| BranchId::try_from(cbid).ok())
976                .unwrap_or(BranchId::Nu5);
977
978            let inputs = trans.inputs();
979            let outputs = trans.outputs();
980            let vin = inputs.iter().map(compat::input_to_txin).collect();
981            let vout = outputs.iter().map(compat::output_to_txout).collect();
982
983            let lock_time_u32 =
984                compat::lock_time_to_u32(&trans.lock_time().unwrap_or(LockTime::unlocked()));
985
986            let transparent_bundle = Some(zcash_transparent::bundle::Bundle {
987                vin,
988                vout,
989                authorization: zcash_transparent::bundle::Authorized,
990            });
991
992            // For V4, carry over the sapling bundle (already in zcash_primitives format)
993            let sapling_bundle = trans.0.sapling_bundle().cloned();
994
995            let tx_data = zp_tx::TransactionData::from_parts(
996                TxVersion::V5,
997                branch_id,
998                lock_time_u32,
999                zcash_protocol::consensus::BlockHeight::from_u32(height.0),
1000                transparent_bundle,
1001                None,
1002                sapling_bundle,
1003                None,
1004            );
1005
1006            Transaction(tx_data.freeze().expect("rebuilt from valid transaction"))
1007        }
1008        // unreachable but suppress warning for non-nu7 builds
1009        #[allow(unreachable_patterns)]
1010        _ => trans.clone(),
1011    }
1012}
1013
1014/// Iterate over transactions in the block test vectors for the specified `network`.
1015pub fn test_transactions(
1016    network: &Network,
1017) -> impl DoubleEndedIterator<Item = (block::Height, Arc<Transaction>)> {
1018    let blocks = network.block_iter();
1019
1020    transactions_from_blocks(blocks)
1021}
1022
1023/// Returns an iterator over V5 transactions extracted from the given blocks.
1024pub fn v5_transactions<'b>(
1025    blocks: impl DoubleEndedIterator<Item = (&'b u32, &'b &'static [u8])> + 'b,
1026) -> impl DoubleEndedIterator<Item = Transaction> + 'b {
1027    transactions_from_blocks(blocks).filter_map(|(_, tx)| match tx.tx_version() {
1028        TxVersion::V5 | TxVersion::V6 => Some((*tx).clone()),
1029        _ => None,
1030    })
1031}
1032
1033/// Generate an iterator over ([`block::Height`], [`Arc<Transaction>`]).
1034pub fn transactions_from_blocks<'a>(
1035    blocks: impl DoubleEndedIterator<Item = (&'a u32, &'a &'static [u8])> + 'a,
1036) -> impl DoubleEndedIterator<Item = (block::Height, Arc<Transaction>)> + 'a {
1037    blocks.flat_map(|(&block_height, &block_bytes)| {
1038        let block = block_bytes
1039            .zcash_deserialize_into::<block::Block>()
1040            .expect("block is structurally valid");
1041
1042        block
1043            .transactions
1044            .into_iter()
1045            .map(move |transaction| (block::Height(block_height), transaction))
1046    })
1047}