Skip to main content

zebra_chain/block/
arbitrary.rs

1//! Randomised property testing for [`Block`]s.
2
3use proptest::prelude::*;
4
5use crate::{
6    amount::NonNegative,
7    block,
8    fmt::{HexDebug, SummaryDebug},
9    history_tree::HistoryTree,
10    parameters::{NetworkUpgrade::*, GENESIS_PREVIOUS_BLOCK_HASH},
11    primitives::zcash_history::BlockCommitmentTreeRoots,
12    serialization::{self, BytesInDisplayOrder},
13    transaction::arbitrary::MAX_ARBITRARY_ITEMS,
14    transparent::{
15        new_transaction_ordered_outputs, CoinbaseSpendRestriction,
16        MIN_TRANSPARENT_COINBASE_MATURITY,
17    },
18    work::{difficulty::CompactDifficulty, equihash},
19};
20
21use super::*;
22
23/// The chain length for most zebra-chain proptests.
24///
25/// Most generated chains will contain transparent spends at or before this height.
26///
27/// This height was chosen a tradeoff between chains with no spends,
28/// and chains which spend outputs created by previous spends.
29///
30/// The raw probability of having no spends during a test run is:
31/// ```text
32/// shielded_input = shielded_pool_count / pool_count
33/// expected_transactions = expected_inputs = MAX_ARBITRARY_ITEMS/2
34/// shielded_input^(expected_transactions * expected_inputs * (PREVOUTS_CHAIN_HEIGHT - 1))
35/// ```
36///
37/// This probability is approximately 3%. However, proptest generation and
38/// minimisation strategies can create additional chains with no transparent spends.
39///
40/// To increase the proportion of test runs with proptest spends, increase `PREVOUTS_CHAIN_HEIGHT`.
41pub const PREVOUTS_CHAIN_HEIGHT: usize = 4;
42
43/// The chain length for most zebra-state proptests.
44///
45/// Most generated chains will contain transparent spends at or before this height.
46///
47/// This height was chosen as a tradeoff between chains with no transparent spends,
48/// and chains which spend outputs created by previous spends.
49///
50/// See [`block::arbitrary::PREVOUTS_CHAIN_HEIGHT`] for details.
51pub const MAX_PARTIAL_CHAIN_BLOCKS: usize =
52    MIN_TRANSPARENT_COINBASE_MATURITY as usize + PREVOUTS_CHAIN_HEIGHT;
53
54impl Arbitrary for Height {
55    type Parameters = ();
56
57    fn arbitrary_with(_args: ()) -> Self::Strategy {
58        (Height::MIN.0..=Height::MAX.0).prop_map(Height).boxed()
59    }
60
61    type Strategy = BoxedStrategy<Self>;
62}
63
64#[derive(Debug, Clone)]
65#[non_exhaustive]
66/// The configuration data for proptest when generating arbitrary chains
67pub struct LedgerState {
68    /// The height of the generated block, or the start height of the generated chain.
69    ///
70    /// To get the network upgrade, use the `network_upgrade` method.
71    ///
72    /// If `network_upgrade_override` is not set, the network upgrade is derived
73    /// from the `height` and `network`.
74    pub height: Height,
75
76    /// The network to generate fake blocks for.
77    pub network: Network,
78
79    /// Overrides the network upgrade calculated from `height` and `network`.
80    ///
81    /// To get the network upgrade, use the `network_upgrade` method.
82    network_upgrade_override: Option<NetworkUpgrade>,
83
84    /// Overrides the previous block hashes in blocks generated by this ledger.
85    previous_block_hash_override: Option<block::Hash>,
86
87    /// Regardless of tip height and network, every transaction is this version.
88    transaction_version_override: Option<u32>,
89
90    /// Every V5 and later transaction has a valid `network_upgrade` field.
91    ///
92    /// If `false`, zero or more transactions may have invalid network upgrades.
93    transaction_has_valid_network_upgrade: bool,
94
95    /// Generate coinbase transactions.
96    ///
97    /// In a block or transaction vector, make the first transaction a coinbase
98    /// transaction.
99    ///
100    /// For an individual transaction, make the transaction a coinbase
101    /// transaction.
102    pub(crate) has_coinbase: bool,
103}
104
105/// Overrides for arbitrary [`LedgerState`]s.
106#[derive(Debug, Clone)]
107pub struct LedgerStateOverride {
108    /// Use the given network instead of Mainnet
109    pub network_override: Option<Network>,
110
111    /// Every chain starts at this block. Single blocks have this height.
112    pub height_override: Option<Height>,
113
114    /// Every chain starts with a block with this previous block hash.
115    /// Single blocks have this previous block hash.
116    pub previous_block_hash_override: Option<block::Hash>,
117
118    /// Regardless of tip height and network, every block has features from this
119    /// network upgrade.
120    pub network_upgrade_override: Option<NetworkUpgrade>,
121
122    /// Regardless of tip height and network, every transaction is this version.
123    pub transaction_version_override: Option<u32>,
124
125    /// Every V5 and later transaction has a valid `network_upgrade` field.
126    ///
127    /// If `false`, zero or more transactions may have invalid network upgrades.
128    pub transaction_has_valid_network_upgrade: bool,
129
130    /// Every block has exactly one coinbase transaction.
131    /// Transactions are always coinbase transactions.
132    pub always_has_coinbase: bool,
133}
134
135impl LedgerState {
136    /// Returns the default strategy for creating arbitrary `LedgerState`s.
137    pub fn default_strategy() -> BoxedStrategy<Self> {
138        Self::arbitrary_with(LedgerStateOverride::default())
139    }
140
141    /// Returns a strategy for creating arbitrary `LedgerState`s, without any
142    /// overrides.
143    pub fn no_override_strategy() -> BoxedStrategy<Self> {
144        Self::arbitrary_with(LedgerStateOverride {
145            network_override: None,
146            height_override: None,
147            previous_block_hash_override: None,
148            network_upgrade_override: None,
149            transaction_version_override: None,
150            transaction_has_valid_network_upgrade: false,
151            always_has_coinbase: false,
152        })
153    }
154
155    /// Returns a strategy for creating `LedgerState`s with features from
156    /// `network_upgrade_override`.
157    ///
158    /// These features ignore the actual tip height and network.
159    pub fn network_upgrade_strategy(
160        network_upgrade_override: NetworkUpgrade,
161        transaction_version_override: impl Into<Option<u32>>,
162        transaction_has_valid_network_upgrade: bool,
163    ) -> BoxedStrategy<Self> {
164        Self::arbitrary_with(LedgerStateOverride {
165            network_override: None,
166            height_override: None,
167            previous_block_hash_override: None,
168            network_upgrade_override: Some(network_upgrade_override),
169            transaction_version_override: transaction_version_override.into(),
170            transaction_has_valid_network_upgrade,
171            always_has_coinbase: false,
172        })
173    }
174
175    /// Returns a strategy for creating `LedgerState`s that always have coinbase
176    /// transactions.
177    ///
178    /// Also applies `network_upgrade_override`, if present.
179    pub fn coinbase_strategy(
180        network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
181        transaction_version_override: impl Into<Option<u32>>,
182        transaction_has_valid_network_upgrade: bool,
183    ) -> BoxedStrategy<Self> {
184        Self::arbitrary_with(LedgerStateOverride {
185            network_override: None,
186            height_override: None,
187            previous_block_hash_override: None,
188            network_upgrade_override: network_upgrade_override.into(),
189            transaction_version_override: transaction_version_override.into(),
190            transaction_has_valid_network_upgrade,
191            always_has_coinbase: true,
192        })
193    }
194
195    /// Returns a strategy for creating `LedgerState`s that start with a genesis
196    /// block.
197    ///
198    /// These strategies also have coinbase transactions, and an optional network
199    /// upgrade override.
200    ///
201    /// Use the `Genesis` network upgrade to get a random genesis block, with
202    /// Zcash genesis features.
203    pub fn genesis_strategy(
204        network_override: impl Into<Option<Network>>,
205        network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
206        transaction_version_override: impl Into<Option<u32>>,
207        transaction_has_valid_network_upgrade: bool,
208    ) -> BoxedStrategy<Self> {
209        Self::arbitrary_with(LedgerStateOverride {
210            network_override: network_override.into(),
211            height_override: Some(Height(0)),
212            previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH),
213            network_upgrade_override: network_upgrade_override.into(),
214            transaction_version_override: transaction_version_override.into(),
215            transaction_has_valid_network_upgrade,
216            always_has_coinbase: true,
217        })
218    }
219
220    /// Returns a strategy for creating `LedgerState`s that start at `height`.
221    ///
222    /// These strategies also have coinbase transactions, and an optional network
223    /// upgrade override.
224    pub fn height_strategy(
225        height: Height,
226        network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
227        transaction_version_override: impl Into<Option<u32>>,
228        transaction_has_valid_network_upgrade: bool,
229    ) -> BoxedStrategy<Self> {
230        Self::arbitrary_with(LedgerStateOverride {
231            network_override: None,
232            height_override: Some(height),
233            previous_block_hash_override: None,
234            network_upgrade_override: network_upgrade_override.into(),
235            transaction_version_override: transaction_version_override.into(),
236            transaction_has_valid_network_upgrade,
237            always_has_coinbase: true,
238        })
239    }
240
241    /// Returns the network upgrade for this ledger state.
242    ///
243    /// If `network_upgrade_override` is set, it replaces the upgrade calculated
244    /// using `height` and `network`.
245    pub fn network_upgrade(&self) -> NetworkUpgrade {
246        if let Some(network_upgrade_override) = self.network_upgrade_override {
247            network_upgrade_override
248        } else {
249            NetworkUpgrade::current(&self.network, self.height)
250        }
251    }
252
253    /// Returns the transaction version override.
254    pub fn transaction_version_override(&self) -> Option<u32> {
255        self.transaction_version_override
256    }
257
258    /// Returns `true` if all transactions have valid network upgrade fields.
259    ///
260    /// If `false`, some transactions have invalid network upgrades.
261    pub fn transaction_has_valid_network_upgrade(&self) -> bool {
262        self.transaction_has_valid_network_upgrade
263    }
264}
265
266impl Default for LedgerState {
267    fn default() -> Self {
268        // TODO: stop having a default network
269        let default_network = Network::default();
270        let default_override = LedgerStateOverride::default();
271
272        let most_recent_nu = NetworkUpgrade::current(&default_network, Height::MAX);
273        let most_recent_activation_height =
274            most_recent_nu.activation_height(&default_network).unwrap();
275
276        LedgerState {
277            height: most_recent_activation_height,
278            network: default_network,
279            network_upgrade_override: default_override.network_upgrade_override,
280            previous_block_hash_override: default_override.previous_block_hash_override,
281            transaction_version_override: default_override.transaction_version_override,
282            transaction_has_valid_network_upgrade: default_override
283                .transaction_has_valid_network_upgrade,
284            has_coinbase: default_override.always_has_coinbase,
285        }
286    }
287}
288
289impl Default for LedgerStateOverride {
290    fn default() -> Self {
291        let default_network = Network::default();
292
293        // TODO: dynamically select any future network upgrade (#1974)
294        let nu5_activation_height = Nu5.activation_height(&default_network);
295        let nu5_override = if nu5_activation_height.is_some() {
296            None
297        } else {
298            Some(Nu5)
299        };
300
301        LedgerStateOverride {
302            network_override: None,
303            height_override: None,
304            previous_block_hash_override: None,
305            network_upgrade_override: nu5_override,
306            transaction_version_override: None,
307            transaction_has_valid_network_upgrade: false,
308            always_has_coinbase: true,
309        }
310    }
311}
312
313impl Arbitrary for LedgerState {
314    type Parameters = LedgerStateOverride;
315
316    /// Generate an arbitrary [`LedgerState`].
317    ///
318    /// The default strategy arbitrarily skips some coinbase transactions, and
319    /// has an arbitrary start height. To override, use a specific [`LedgerState`]
320    /// strategy method.
321    fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
322        (
323            any::<Height>(),
324            any::<Network>(),
325            any::<bool>(),
326            any::<bool>(),
327        )
328            .prop_map(
329                move |(height, network, transaction_has_valid_network_upgrade, has_coinbase)| {
330                    LedgerState {
331                        height: ledger_override.height_override.unwrap_or(height),
332                        network: ledger_override
333                            .network_override
334                            .as_ref()
335                            .unwrap_or(&network)
336                            .clone(),
337                        network_upgrade_override: ledger_override.network_upgrade_override,
338                        previous_block_hash_override: ledger_override.previous_block_hash_override,
339                        transaction_version_override: ledger_override.transaction_version_override,
340                        transaction_has_valid_network_upgrade: ledger_override
341                            .transaction_has_valid_network_upgrade
342                            || transaction_has_valid_network_upgrade,
343                        has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
344                    }
345                },
346            )
347            .boxed()
348    }
349
350    type Strategy = BoxedStrategy<Self>;
351}
352
353impl Arbitrary for Block {
354    type Parameters = LedgerState;
355
356    fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
357        let transactions_strategy = {
358            let ledger_state = ledger_state.clone();
359            // Generate a random number transactions. A coinbase tx is always generated, so if
360            // `transaction_count` is zero, the block will contain only the coinbase tx.
361            (0..MAX_ARBITRARY_ITEMS).prop_flat_map(move |transaction_count| {
362                Transaction::vec_strategy(ledger_state.clone(), transaction_count)
363            })
364        };
365
366        // TODO: if needed, fixup:
367        // - history and authorizing data commitments
368        // - the transaction merkle root
369
370        (Header::arbitrary_with(ledger_state), transactions_strategy)
371            .prop_map(move |(header, transactions)| Self {
372                header: header.into(),
373                transactions,
374            })
375            .boxed()
376    }
377
378    type Strategy = BoxedStrategy<Self>;
379}
380
381/// Skip checking transparent coinbase spends in [`Block::partial_chain_strategy`].
382#[allow(clippy::result_unit_err)]
383pub fn allow_all_transparent_coinbase_spends(
384    _: transparent::OutPoint,
385    _: transparent::CoinbaseSpendRestriction,
386    _: &transparent::Utxo,
387) -> Result<(), ()> {
388    Ok(())
389}
390
391impl Block {
392    /// Returns a strategy for creating vectors of blocks with increasing height.
393    ///
394    /// Each vector is `count` blocks long.
395    ///
396    /// `check_transparent_coinbase_spend` is used to check if
397    /// transparent coinbase UTXOs are valid, before using them in blocks.
398    /// Use [`allow_all_transparent_coinbase_spends`] to disable this check.
399    ///
400    /// `generate_valid_commitments` specifies if the generated blocks
401    /// should have valid commitments. This makes it much slower so it's better
402    /// to enable only when needed.
403    pub fn partial_chain_strategy<F, E>(
404        mut current: LedgerState,
405        count: usize,
406        check_transparent_coinbase_spend: F,
407        generate_valid_commitments: bool,
408    ) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>>
409    where
410        F: Fn(
411                transparent::OutPoint,
412                transparent::CoinbaseSpendRestriction,
413                &transparent::Utxo,
414            ) -> Result<(), E>
415            + Copy
416            + 'static,
417    {
418        let mut vec = Vec::with_capacity(count);
419
420        // generate block strategies with the correct heights
421        for _ in 0..count {
422            vec.push((Just(current.height), Block::arbitrary_with(current.clone())));
423            current.height.0 += 1;
424        }
425
426        // after the vec strategy generates blocks, fixup invalid parts of the blocks
427        vec.prop_map(move |mut vec| {
428            let mut previous_block_hash = None;
429            let mut utxos = HashMap::new();
430            let mut chain_value_pools = ValueBalance::zero();
431            let mut sapling_tree = sapling::tree::NoteCommitmentTree::default();
432            let mut orchard_tree = orchard::tree::NoteCommitmentTree::default();
433            // Ironwood reuses the Orchard note commitment tree type. Generated blocks have no
434            // Ironwood data, so this stays empty, but it must be threaded through the V3 history
435            // node (NU6.3+) using its real empty-tree root so commitments match validation.
436            let mut ironwood_tree = orchard::tree::NoteCommitmentTree::default();
437            // The history tree usually takes care of "creating itself". But this
438            // only works when blocks are pushed into it starting from genesis
439            // (or at least pre-Heartwood, where the tree is not required).
440            // However, this strategy can generate blocks from an arbitrary height,
441            // so we must wait for the first block to create the history tree from it.
442            // This is why `Option` is used here.
443            let mut history_tree: Option<HistoryTree> = None;
444
445            for (height, block) in vec.iter_mut() {
446                // fixup the previous block hash
447                if let Some(previous_block_hash) = previous_block_hash {
448                    Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
449                }
450
451                let mut new_transactions = Vec::new();
452                for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
453                    if let Some(transaction) = fix_generated_transaction(
454                        (*transaction).clone(),
455                        tx_index_in_block,
456                        *height,
457                        &mut chain_value_pools,
458                        &mut utxos,
459                        check_transparent_coinbase_spend,
460                    ) {
461                        // The FinalizedState does not update the note commitment trees with the genesis block,
462                        // because it doesn't need to (the trees are not used at that point) and updating them
463                        // would be awkward since the genesis block is handled separately there.
464                        // This forces us to skip the genesis block here too in order to able to use
465                        // this to test the finalized state.
466                        //
467                        // TODO: run note commitment tree updates in parallel rayon threads,
468                        //       using `NoteCommitmentTrees::update_trees_parallel()`
469                        if generate_valid_commitments && *height != Height(0) {
470                            for sapling_note_commitment in transaction.sapling_note_commitments() {
471                                sapling_tree.append(*sapling_note_commitment).unwrap();
472                            }
473                            for orchard_note_commitment in transaction.orchard_note_commitments() {
474                                orchard_tree.append(*orchard_note_commitment).unwrap();
475                            }
476                            for ironwood_note_commitment in transaction.ironwood_note_commitments()
477                            {
478                                ironwood_tree.append(*ironwood_note_commitment).unwrap();
479                            }
480                        }
481                        new_transactions.push(Arc::new(transaction));
482                    }
483                }
484
485                // delete invalid transactions
486                block.transactions = new_transactions;
487
488                // fix commitment (must be done after finishing changing the block)
489                if generate_valid_commitments {
490                    let current_height = block.coinbase_height().unwrap();
491                    let heartwood_height = NetworkUpgrade::Heartwood
492                        .activation_height(&current.network)
493                        .unwrap();
494                    let nu5_height = NetworkUpgrade::Nu5.activation_height(&current.network);
495
496                    match current_height.cmp(&heartwood_height) {
497                        std::cmp::Ordering::Less => {
498                            // In pre-Heartwood blocks this is the Sapling note commitment tree root.
499                            // We don't validate it since we checkpoint on Canopy, but it
500                            // needs to be well-formed, i.e. smaller than 𝑞_J, so we
501                            // arbitrarily set it to 1.
502                            let block_header = Arc::make_mut(&mut block.header);
503                            block_header.commitment_bytes = [0u8; 32].into();
504                            block_header.commitment_bytes[0] = 1;
505                        }
506                        std::cmp::Ordering::Equal => {
507                            // The Heartwood activation block has a hardcoded all-zeroes commitment.
508                            let block_header = Arc::make_mut(&mut block.header);
509                            block_header.commitment_bytes = [0u8; 32].into();
510                        }
511                        std::cmp::Ordering::Greater => {
512                            // Set the correct commitment bytes according to the network upgrade.
513                            let history_tree_root = match &history_tree {
514                                Some(tree) => tree.hash().unwrap_or_else(|| [0u8; 32].into()),
515                                None => [0u8; 32].into(),
516                            };
517                            if nu5_height.is_some() && current_height >= nu5_height.unwrap() {
518                                // From zebra-state/src/service/check.rs
519                                let auth_data_root = block.auth_data_root();
520                                let hash_block_commitments =
521                                    ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
522                                        &history_tree_root,
523                                        &auth_data_root,
524                                    );
525                                let block_header = Arc::make_mut(&mut block.header);
526                                block_header.commitment_bytes =
527                                    hash_block_commitments.bytes_in_serialized_order().into();
528                            } else {
529                                let block_header = Arc::make_mut(&mut block.header);
530                                block_header.commitment_bytes =
531                                    history_tree_root.bytes_in_serialized_order().into();
532                            }
533                        }
534                    }
535                    // update history tree for the next block
536                    if let Some(history_tree) = history_tree.as_mut() {
537                        history_tree
538                            .push(
539                                &current.network,
540                                Arc::new(block.clone()),
541                                BlockCommitmentTreeRoots {
542                                    sapling: &sapling_tree.root(),
543                                    orchard: &orchard_tree.root(),
544                                    ironwood: &ironwood_tree.root(),
545                                },
546                            )
547                            .unwrap();
548                    } else {
549                        history_tree = Some(
550                            HistoryTree::from_block(
551                                &current.network,
552                                Arc::new(block.clone()),
553                                BlockCommitmentTreeRoots {
554                                    sapling: &sapling_tree.root(),
555                                    orchard: &orchard_tree.root(),
556                                    ironwood: &ironwood_tree.root(),
557                                },
558                            )
559                            .unwrap(),
560                        );
561                    }
562                }
563
564                // now that we've made all the changes, calculate our block hash,
565                // so the next block can use it
566                previous_block_hash = Some(block.hash());
567            }
568            SummaryDebug(
569                vec.into_iter()
570                    .map(|(_height, block)| Arc::new(block))
571                    .collect(),
572            )
573        })
574        .boxed()
575    }
576}
577
578/// Fix `transaction` so it obeys more consensus rules.
579///
580/// Spends [`transparent::OutPoint`]s from `utxos`, and adds newly created outputs.
581///
582/// If the transaction can't be fixed, returns `None`.
583pub fn fix_generated_transaction<F, E>(
584    mut transaction: Transaction,
585    tx_index_in_block: usize,
586    height: Height,
587    chain_value_pools: &mut ValueBalance<NonNegative>,
588    utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
589    check_transparent_coinbase_spend: F,
590) -> Option<Transaction>
591where
592    F: Fn(
593            transparent::OutPoint,
594            transparent::CoinbaseSpendRestriction,
595            &transparent::Utxo,
596        ) -> Result<(), E>
597        + Copy
598        + 'static,
599{
600    // Coinbase transactions must not contain Sapling spends (GHSA-rgwx-8r98-p34c).
601    // The arbitrary `Transaction` strategy generates these independently, so clear
602    // any generated Sapling shielded data on coinbase transactions before the chain
603    // builder commits them. The deserialization rejection path is still exercised
604    // by the `transaction_roundtrip` proptest and the GHSA-rgwx-8r98-p34c reproduction
605    // vector in `zebra-chain`.
606    if transaction.is_coinbase() {
607        match &mut transaction {
608            Transaction::V4 {
609                sapling_shielded_data,
610                ..
611            } => *sapling_shielded_data = None,
612            Transaction::V5 {
613                sapling_shielded_data,
614                ..
615            } => *sapling_shielded_data = None,
616            Transaction::V6 {
617                sapling_shielded_data,
618                ..
619            } => *sapling_shielded_data = None,
620            Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {}
621        }
622    }
623
624    let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height);
625    let mut new_inputs = Vec::new();
626    let mut spent_outputs = HashMap::new();
627
628    // fixup the transparent spends
629    let original_inputs = transaction.inputs().to_vec();
630    for mut input in original_inputs.into_iter() {
631        if input.outpoint().is_some() {
632            // the transparent chain value pool is the sum of unspent UTXOs,
633            // so we don't need to check it separately, because we only spend unspent UTXOs
634            if let Some(selected_outpoint) = find_valid_utxo_for_spend(
635                &mut transaction,
636                &mut spend_restriction,
637                height,
638                utxos,
639                check_transparent_coinbase_spend,
640            ) {
641                input.set_outpoint(selected_outpoint);
642                new_inputs.push(input);
643
644                let spent_utxo = utxos.remove(&selected_outpoint)?;
645                spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
646            }
647            // otherwise, drop the invalid input, because it has no valid UTXOs to spend
648        } else {
649            // preserve coinbase inputs
650            new_inputs.push(input.clone());
651        }
652    }
653
654    // delete invalid inputs
655    *transaction.inputs_mut() = new_inputs;
656
657    let (_remaining_transaction_value, new_chain_value_pools) = transaction
658        .fix_chain_value_pools(*chain_value_pools, &spent_outputs)
659        .expect("value fixes produce valid chain value pools and remaining transaction values");
660
661    // TODO: if needed, check output count here as well
662    if transaction.has_transparent_or_shielded_inputs() {
663        // consensus rule: skip genesis created UTXOs
664        // Zebra implementation: also skip shielded chain value pool changes
665        if height > Height(0) {
666            *chain_value_pools = new_chain_value_pools;
667
668            utxos.extend(new_transaction_ordered_outputs(
669                &transaction,
670                transaction.hash(),
671                tx_index_in_block,
672                height,
673            ));
674        }
675
676        Some(transaction)
677    } else {
678        None
679    }
680}
681
682/// Find a valid [`transparent::OutPoint`] in `utxos` to spend in `transaction`.
683///
684/// Modifies `transaction` and updates `spend_restriction` if needed.
685///
686/// If there is no valid output, or many search attempts have failed, returns `None`.
687pub fn find_valid_utxo_for_spend<F, E>(
688    transaction: &mut Transaction,
689    spend_restriction: &mut CoinbaseSpendRestriction,
690    spend_height: Height,
691    utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
692    check_transparent_coinbase_spend: F,
693) -> Option<transparent::OutPoint>
694where
695    F: Fn(
696            transparent::OutPoint,
697            transparent::CoinbaseSpendRestriction,
698            &transparent::Utxo,
699        ) -> Result<(), E>
700        + Copy
701        + 'static,
702{
703    let has_shielded_outputs = transaction.has_shielded_outputs();
704    let delete_transparent_outputs =
705        CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
706    // choose an arbitrary spendable UTXO, in hash set order, with a bounded scan
707    for (candidate_outpoint, candidate_utxo) in utxos.iter().take(100) {
708        // Avoid O(n^2) algorithmic complexity by limiting the number of checks
709
710        // try the utxo as-is, then try it with deleted transparent outputs
711        if check_transparent_coinbase_spend(
712            *candidate_outpoint,
713            *spend_restriction,
714            candidate_utxo.as_ref(),
715        )
716        .is_ok()
717        {
718            return Some(*candidate_outpoint);
719        } else if has_shielded_outputs
720            && check_transparent_coinbase_spend(
721                *candidate_outpoint,
722                delete_transparent_outputs,
723                candidate_utxo.as_ref(),
724            )
725            .is_ok()
726        {
727            *transaction.outputs_mut() = Vec::new();
728            *spend_restriction = delete_transparent_outputs;
729
730            return Some(*candidate_outpoint);
731        }
732    }
733
734    None
735}
736
737impl Arbitrary for Commitment {
738    type Parameters = ();
739
740    fn arbitrary_with(_args: ()) -> Self::Strategy {
741        (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
742            .prop_map(|(commitment_bytes, network, block_height)| {
743                if block_height == Heartwood.activation_height(&network).unwrap() {
744                    Commitment::ChainHistoryActivationReserved
745                } else {
746                    Commitment::from_bytes(commitment_bytes, &network, block_height)
747                        .expect("unexpected failure in from_bytes parsing")
748                }
749            })
750            .boxed()
751    }
752
753    type Strategy = BoxedStrategy<Self>;
754}
755
756impl Arbitrary for Header {
757    type Parameters = LedgerState;
758
759    fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
760        (
761            // version is interpreted as i32 in the spec, so we are limited to i32::MAX here
762            (4u32..(i32::MAX as u32)),
763            any::<Hash>(),
764            any::<merkle::Root>(),
765            any::<HexDebug<[u8; 32]>>(),
766            serialization::arbitrary::datetime_u32(),
767            any::<CompactDifficulty>(),
768            any::<HexDebug<[u8; 32]>>(),
769            any::<equihash::Solution>(),
770        )
771            .prop_map(
772                move |(
773                    version,
774                    mut previous_block_hash,
775                    merkle_root,
776                    commitment_bytes,
777                    time,
778                    difficulty_threshold,
779                    nonce,
780                    solution,
781                )| {
782                    if let Some(previous_block_hash_override) =
783                        ledger_state.previous_block_hash_override
784                    {
785                        previous_block_hash = previous_block_hash_override;
786                    } else if ledger_state.height == Height(0) {
787                        previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
788                    }
789
790                    Header {
791                        version,
792                        previous_block_hash,
793                        merkle_root,
794                        commitment_bytes,
795                        time,
796                        difficulty_threshold,
797                        nonce,
798                        solution,
799                    }
800                },
801            )
802            .boxed()
803    }
804
805    type Strategy = BoxedStrategy<Self>;
806}