1use 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
23pub const PREVOUTS_CHAIN_HEIGHT: usize = 4;
42
43pub 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]
66pub struct LedgerState {
68 pub height: Height,
75
76 pub network: Network,
78
79 network_upgrade_override: Option<NetworkUpgrade>,
83
84 previous_block_hash_override: Option<block::Hash>,
86
87 transaction_version_override: Option<u32>,
89
90 transaction_has_valid_network_upgrade: bool,
94
95 pub(crate) has_coinbase: bool,
103}
104
105#[derive(Debug, Clone)]
107pub struct LedgerStateOverride {
108 pub network_override: Option<Network>,
110
111 pub height_override: Option<Height>,
113
114 pub previous_block_hash_override: Option<block::Hash>,
117
118 pub network_upgrade_override: Option<NetworkUpgrade>,
121
122 pub transaction_version_override: Option<u32>,
124
125 pub transaction_has_valid_network_upgrade: bool,
129
130 pub always_has_coinbase: bool,
133}
134
135impl LedgerState {
136 pub fn default_strategy() -> BoxedStrategy<Self> {
138 Self::arbitrary_with(LedgerStateOverride::default())
139 }
140
141 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 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 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 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 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 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 pub fn transaction_version_override(&self) -> Option<u32> {
255 self.transaction_version_override
256 }
257
258 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 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 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 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 (0..MAX_ARBITRARY_ITEMS).prop_flat_map(move |transaction_count| {
362 Transaction::vec_strategy(ledger_state.clone(), transaction_count)
363 })
364 };
365
366 (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#[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 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 for _ in 0..count {
422 vec.push((Just(current.height), Block::arbitrary_with(current.clone())));
423 current.height.0 += 1;
424 }
425
426 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 let mut ironwood_tree = orchard::tree::NoteCommitmentTree::default();
438 let mut history_tree: Option<HistoryTree> = None;
445
446 for (height, block) in vec.iter_mut() {
447 if let Some(previous_block_hash) = previous_block_hash {
449 Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
450 }
451
452 let mut new_transactions = Vec::new();
453 for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
454 if let Some(transaction) = fix_generated_transaction(
455 (*transaction).clone(),
456 tx_index_in_block,
457 *height,
458 &mut chain_value_pools,
459 &mut utxos,
460 check_transparent_coinbase_spend,
461 ) {
462 if generate_valid_commitments && *height != Height(0) {
471 for sapling_note_commitment in transaction.sapling_note_commitments() {
472 sapling_tree.append(sapling_note_commitment).unwrap();
473 }
474 for orchard_note_commitment in transaction.orchard_note_commitments() {
475 use halo2::pasta::group::ff::PrimeField;
476 let cm =
477 pallas::Base::from_repr(orchard_note_commitment.to_bytes())
478 .expect("valid orchard note commitment");
479 orchard_tree.append(cm).unwrap();
480 }
481 for ironwood_note_commitment in transaction.ironwood_note_commitments()
482 {
483 use halo2::pasta::group::ff::PrimeField;
484 let cm =
485 pallas::Base::from_repr(ironwood_note_commitment.to_bytes())
486 .expect("valid ironwood note commitment");
487 ironwood_tree.append(cm).unwrap();
488 }
489 }
490 new_transactions.push(Arc::new(transaction));
491 }
492 }
493
494 block.transactions = new_transactions;
496
497 if generate_valid_commitments {
499 let current_height = block.coinbase_height().unwrap();
500 let heartwood_height = NetworkUpgrade::Heartwood
501 .activation_height(¤t.network)
502 .unwrap();
503 let nu5_height = NetworkUpgrade::Nu5.activation_height(¤t.network);
504
505 match current_height.cmp(&heartwood_height) {
506 std::cmp::Ordering::Less => {
507 let block_header = Arc::make_mut(&mut block.header);
512 block_header.commitment_bytes = [0u8; 32].into();
513 block_header.commitment_bytes[0] = 1;
514 }
515 std::cmp::Ordering::Equal => {
516 let block_header = Arc::make_mut(&mut block.header);
518 block_header.commitment_bytes = [0u8; 32].into();
519 }
520 std::cmp::Ordering::Greater => {
521 let history_tree_root = match &history_tree {
523 Some(tree) => tree.hash().unwrap_or_else(|| [0u8; 32].into()),
524 None => [0u8; 32].into(),
525 };
526 if nu5_height.is_some() && current_height >= nu5_height.unwrap() {
527 let auth_data_root = block.auth_data_root();
529 let hash_block_commitments =
530 ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
531 &history_tree_root,
532 &auth_data_root,
533 );
534 let block_header = Arc::make_mut(&mut block.header);
535 block_header.commitment_bytes =
536 hash_block_commitments.bytes_in_serialized_order().into();
537 } else {
538 let block_header = Arc::make_mut(&mut block.header);
539 block_header.commitment_bytes =
540 history_tree_root.bytes_in_serialized_order().into();
541 }
542 }
543 }
544 if let Some(history_tree) = history_tree.as_mut() {
546 history_tree
547 .push(
548 ¤t.network,
549 Arc::new(block.clone()),
550 BlockCommitmentTreeRoots {
551 sapling: &sapling_tree.root(),
552 orchard: &orchard_tree.root(),
553 ironwood: &ironwood_tree.root(),
554 },
555 )
556 .unwrap();
557 } else {
558 history_tree = Some(
559 HistoryTree::from_block(
560 ¤t.network,
561 Arc::new(block.clone()),
562 BlockCommitmentTreeRoots {
563 sapling: &sapling_tree.root(),
564 orchard: &orchard_tree.root(),
565 ironwood: &ironwood_tree.root(),
566 },
567 )
568 .unwrap(),
569 );
570 }
571 }
572
573 previous_block_hash = Some(block.hash());
576 }
577 SummaryDebug(
578 vec.into_iter()
579 .map(|(_height, block)| Arc::new(block))
580 .collect(),
581 )
582 })
583 .boxed()
584 }
585}
586
587pub fn fix_generated_transaction<F, E>(
593 mut transaction: Transaction,
594 tx_index_in_block: usize,
595 height: Height,
596 chain_value_pools: &mut ValueBalance<NonNegative>,
597 utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
598 check_transparent_coinbase_spend: F,
599) -> Option<Transaction>
600where
601 F: Fn(
602 transparent::OutPoint,
603 transparent::CoinbaseSpendRestriction,
604 &transparent::Utxo,
605 ) -> Result<(), E>
606 + Copy
607 + 'static,
608{
609 let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height);
616 let mut new_inputs = Vec::new();
617 let mut spent_outputs = HashMap::new();
618
619 let original_inputs = transaction.inputs().to_vec();
621 for mut input in original_inputs.into_iter() {
622 if input.outpoint().is_some() {
623 if let Some(selected_outpoint) = find_valid_utxo_for_spend(
626 &mut transaction,
627 &mut spend_restriction,
628 height,
629 utxos,
630 check_transparent_coinbase_spend,
631 ) {
632 input.set_outpoint(selected_outpoint);
633 new_inputs.push(input);
634
635 let spent_utxo = utxos.remove(&selected_outpoint)?;
636 spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
637 }
638 } else {
640 new_inputs.push(input.clone());
642 }
643 }
644
645 transaction = transaction.with_transparent_inputs(new_inputs);
647
648 let (_remaining_transaction_value, new_chain_value_pools) = transaction
649 .fix_chain_value_pools(*chain_value_pools, &spent_outputs)
650 .expect("value fixes produce valid chain value pools and remaining transaction values");
651
652 if transaction.has_transparent_or_shielded_inputs() {
654 if height > Height(0) {
657 *chain_value_pools = new_chain_value_pools;
658
659 utxos.extend(new_transaction_ordered_outputs(
660 &transaction,
661 transaction.hash(),
662 tx_index_in_block,
663 height,
664 ));
665 }
666
667 Some(transaction)
668 } else {
669 None
670 }
671}
672
673pub fn find_valid_utxo_for_spend<F, E>(
679 transaction: &mut Transaction,
680 spend_restriction: &mut CoinbaseSpendRestriction,
681 spend_height: Height,
682 utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
683 check_transparent_coinbase_spend: F,
684) -> Option<transparent::OutPoint>
685where
686 F: Fn(
687 transparent::OutPoint,
688 transparent::CoinbaseSpendRestriction,
689 &transparent::Utxo,
690 ) -> Result<(), E>
691 + Copy
692 + 'static,
693{
694 let has_shielded_outputs = transaction.has_shielded_outputs();
695 let delete_transparent_outputs =
696 CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
697 for (candidate_outpoint, candidate_utxo) in utxos.iter().take(100) {
699 if check_transparent_coinbase_spend(
703 *candidate_outpoint,
704 *spend_restriction,
705 candidate_utxo.as_ref(),
706 )
707 .is_ok()
708 {
709 return Some(*candidate_outpoint);
710 } else if has_shielded_outputs
711 && check_transparent_coinbase_spend(
712 *candidate_outpoint,
713 delete_transparent_outputs,
714 candidate_utxo.as_ref(),
715 )
716 .is_ok()
717 {
718 *transaction = transaction.clone().with_transparent_outputs(vec![]);
719 *spend_restriction = delete_transparent_outputs;
720
721 return Some(*candidate_outpoint);
722 }
723 }
724
725 None
726}
727
728impl Arbitrary for Commitment {
729 type Parameters = ();
730
731 fn arbitrary_with(_args: ()) -> Self::Strategy {
732 (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
733 .prop_map(|(commitment_bytes, network, block_height)| {
734 if block_height == Heartwood.activation_height(&network).unwrap() {
735 Commitment::ChainHistoryActivationReserved
736 } else {
737 Commitment::from_bytes(commitment_bytes, &network, block_height)
738 .expect("unexpected failure in from_bytes parsing")
739 }
740 })
741 .boxed()
742 }
743
744 type Strategy = BoxedStrategy<Self>;
745}
746
747impl Arbitrary for Header {
748 type Parameters = LedgerState;
749
750 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
751 (
752 (4u32..(i32::MAX as u32)),
754 any::<Hash>(),
755 any::<merkle::Root>(),
756 any::<HexDebug<[u8; 32]>>(),
757 serialization::arbitrary::datetime_u32(),
758 any::<CompactDifficulty>(),
759 any::<HexDebug<[u8; 32]>>(),
760 any::<equihash::Solution>(),
761 )
762 .prop_map(
763 move |(
764 version,
765 mut previous_block_hash,
766 merkle_root,
767 commitment_bytes,
768 time,
769 difficulty_threshold,
770 nonce,
771 solution,
772 )| {
773 if let Some(previous_block_hash_override) =
774 ledger_state.previous_block_hash_override
775 {
776 previous_block_hash = previous_block_hash_override;
777 } else if ledger_state.height == Height(0) {
778 previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
779 }
780
781 Header {
782 version,
783 previous_block_hash,
784 merkle_root,
785 commitment_bytes,
786 time,
787 difficulty_threshold,
788 nonce,
789 solution,
790 }
791 },
792 )
793 .boxed()
794 }
795
796 type Strategy = BoxedStrategy<Self>;
797}