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 serialization,
12 transaction::arbitrary::MAX_ARBITRARY_ITEMS,
13 transparent::{
14 new_transaction_ordered_outputs, CoinbaseSpendRestriction,
15 MIN_TRANSPARENT_COINBASE_MATURITY,
16 },
17 work::{difficulty::CompactDifficulty, equihash},
18};
19
20use super::*;
21
22pub const PREVOUTS_CHAIN_HEIGHT: usize = 4;
41
42pub const MAX_PARTIAL_CHAIN_BLOCKS: usize =
51 MIN_TRANSPARENT_COINBASE_MATURITY as usize + PREVOUTS_CHAIN_HEIGHT;
52
53impl Arbitrary for Height {
54 type Parameters = ();
55
56 fn arbitrary_with(_args: ()) -> Self::Strategy {
57 (Height::MIN.0..=Height::MAX.0).prop_map(Height).boxed()
58 }
59
60 type Strategy = BoxedStrategy<Self>;
61}
62
63#[derive(Debug, Clone)]
64#[non_exhaustive]
65pub struct LedgerState {
67 pub height: Height,
74
75 pub network: Network,
77
78 network_upgrade_override: Option<NetworkUpgrade>,
82
83 previous_block_hash_override: Option<block::Hash>,
85
86 transaction_version_override: Option<u32>,
88
89 transaction_has_valid_network_upgrade: bool,
93
94 pub(crate) has_coinbase: bool,
102}
103
104#[derive(Debug, Clone, Copy)]
106pub struct LedgerStateOverride {
107 pub height_override: Option<Height>,
109
110 pub previous_block_hash_override: Option<block::Hash>,
113
114 pub network_upgrade_override: Option<NetworkUpgrade>,
117
118 pub transaction_version_override: Option<u32>,
120
121 pub transaction_has_valid_network_upgrade: bool,
125
126 pub always_has_coinbase: bool,
129}
130
131impl LedgerState {
132 pub fn default_strategy() -> BoxedStrategy<Self> {
134 Self::arbitrary_with(LedgerStateOverride::default())
135 }
136
137 pub fn no_override_strategy() -> BoxedStrategy<Self> {
140 Self::arbitrary_with(LedgerStateOverride {
141 height_override: None,
142 previous_block_hash_override: None,
143 network_upgrade_override: None,
144 transaction_version_override: None,
145 transaction_has_valid_network_upgrade: false,
146 always_has_coinbase: false,
147 })
148 }
149
150 pub fn network_upgrade_strategy(
155 network_upgrade_override: NetworkUpgrade,
156 transaction_version_override: impl Into<Option<u32>>,
157 transaction_has_valid_network_upgrade: bool,
158 ) -> BoxedStrategy<Self> {
159 Self::arbitrary_with(LedgerStateOverride {
160 height_override: None,
161 previous_block_hash_override: None,
162 network_upgrade_override: Some(network_upgrade_override),
163 transaction_version_override: transaction_version_override.into(),
164 transaction_has_valid_network_upgrade,
165 always_has_coinbase: false,
166 })
167 }
168
169 pub fn coinbase_strategy(
174 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
175 transaction_version_override: impl Into<Option<u32>>,
176 transaction_has_valid_network_upgrade: bool,
177 ) -> BoxedStrategy<Self> {
178 Self::arbitrary_with(LedgerStateOverride {
179 height_override: None,
180 previous_block_hash_override: None,
181 network_upgrade_override: network_upgrade_override.into(),
182 transaction_version_override: transaction_version_override.into(),
183 transaction_has_valid_network_upgrade,
184 always_has_coinbase: true,
185 })
186 }
187
188 pub fn genesis_strategy(
197 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
198 transaction_version_override: impl Into<Option<u32>>,
199 transaction_has_valid_network_upgrade: bool,
200 ) -> BoxedStrategy<Self> {
201 Self::arbitrary_with(LedgerStateOverride {
202 height_override: Some(Height(0)),
203 previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH),
204 network_upgrade_override: network_upgrade_override.into(),
205 transaction_version_override: transaction_version_override.into(),
206 transaction_has_valid_network_upgrade,
207 always_has_coinbase: true,
208 })
209 }
210
211 pub fn height_strategy(
216 height: Height,
217 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
218 transaction_version_override: impl Into<Option<u32>>,
219 transaction_has_valid_network_upgrade: bool,
220 ) -> BoxedStrategy<Self> {
221 Self::arbitrary_with(LedgerStateOverride {
222 height_override: Some(height),
223 previous_block_hash_override: None,
224 network_upgrade_override: network_upgrade_override.into(),
225 transaction_version_override: transaction_version_override.into(),
226 transaction_has_valid_network_upgrade,
227 always_has_coinbase: true,
228 })
229 }
230
231 pub fn network_upgrade(&self) -> NetworkUpgrade {
236 if let Some(network_upgrade_override) = self.network_upgrade_override {
237 network_upgrade_override
238 } else {
239 NetworkUpgrade::current(&self.network, self.height)
240 }
241 }
242
243 pub fn transaction_version_override(&self) -> Option<u32> {
245 self.transaction_version_override
246 }
247
248 pub fn transaction_has_valid_network_upgrade(&self) -> bool {
252 self.transaction_has_valid_network_upgrade
253 }
254}
255
256impl Default for LedgerState {
257 fn default() -> Self {
258 let default_network = Network::default();
260 let default_override = LedgerStateOverride::default();
261
262 let most_recent_nu = NetworkUpgrade::current(&default_network, Height::MAX);
263 let most_recent_activation_height =
264 most_recent_nu.activation_height(&default_network).unwrap();
265
266 LedgerState {
267 height: most_recent_activation_height,
268 network: default_network,
269 network_upgrade_override: default_override.network_upgrade_override,
270 previous_block_hash_override: default_override.previous_block_hash_override,
271 transaction_version_override: default_override.transaction_version_override,
272 transaction_has_valid_network_upgrade: default_override
273 .transaction_has_valid_network_upgrade,
274 has_coinbase: default_override.always_has_coinbase,
275 }
276 }
277}
278
279impl Default for LedgerStateOverride {
280 fn default() -> Self {
281 let default_network = Network::default();
282
283 let nu5_activation_height = Nu5.activation_height(&default_network);
285 let nu5_override = if nu5_activation_height.is_some() {
286 None
287 } else {
288 Some(Nu5)
289 };
290
291 LedgerStateOverride {
292 height_override: None,
293 previous_block_hash_override: None,
294 network_upgrade_override: nu5_override,
295 transaction_version_override: None,
296 transaction_has_valid_network_upgrade: false,
297 always_has_coinbase: true,
298 }
299 }
300}
301
302impl Arbitrary for LedgerState {
303 type Parameters = LedgerStateOverride;
304
305 fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
311 (
312 any::<Height>(),
313 any::<Network>(),
314 any::<bool>(),
315 any::<bool>(),
316 )
317 .prop_map(
318 move |(height, network, transaction_has_valid_network_upgrade, has_coinbase)| {
319 LedgerState {
320 height: ledger_override.height_override.unwrap_or(height),
321 network,
322 network_upgrade_override: ledger_override.network_upgrade_override,
323 previous_block_hash_override: ledger_override.previous_block_hash_override,
324 transaction_version_override: ledger_override.transaction_version_override,
325 transaction_has_valid_network_upgrade: ledger_override
326 .transaction_has_valid_network_upgrade
327 || transaction_has_valid_network_upgrade,
328 has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
329 }
330 },
331 )
332 .boxed()
333 }
334
335 type Strategy = BoxedStrategy<Self>;
336}
337
338impl Arbitrary for Block {
339 type Parameters = LedgerState;
340
341 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
342 let transactions_strategy = {
343 let ledger_state = ledger_state.clone();
344 (0..MAX_ARBITRARY_ITEMS).prop_flat_map(move |transaction_count| {
347 Transaction::vec_strategy(ledger_state.clone(), transaction_count)
348 })
349 };
350
351 (Header::arbitrary_with(ledger_state), transactions_strategy)
356 .prop_map(move |(header, transactions)| Self {
357 header: header.into(),
358 transactions,
359 })
360 .boxed()
361 }
362
363 type Strategy = BoxedStrategy<Self>;
364}
365
366#[allow(clippy::result_unit_err)]
368pub fn allow_all_transparent_coinbase_spends(
369 _: transparent::OutPoint,
370 _: transparent::CoinbaseSpendRestriction,
371 _: &transparent::Utxo,
372) -> Result<(), ()> {
373 Ok(())
374}
375
376impl Block {
377 pub fn partial_chain_strategy<F, E>(
389 mut current: LedgerState,
390 count: usize,
391 check_transparent_coinbase_spend: F,
392 generate_valid_commitments: bool,
393 ) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>>
394 where
395 F: Fn(
396 transparent::OutPoint,
397 transparent::CoinbaseSpendRestriction,
398 &transparent::Utxo,
399 ) -> Result<(), E>
400 + Copy
401 + 'static,
402 {
403 let mut vec = Vec::with_capacity(count);
404
405 for _ in 0..count {
407 vec.push((Just(current.height), Block::arbitrary_with(current.clone())));
408 current.height.0 += 1;
409 }
410
411 vec.prop_map(move |mut vec| {
413 let mut previous_block_hash = None;
414 let mut utxos = HashMap::new();
415 let mut chain_value_pools = ValueBalance::zero();
416 let mut sapling_tree = sapling::tree::NoteCommitmentTree::default();
417 let mut orchard_tree = orchard::tree::NoteCommitmentTree::default();
418 let mut history_tree: Option<HistoryTree> = None;
425
426 for (height, block) in vec.iter_mut() {
427 if let Some(previous_block_hash) = previous_block_hash {
429 Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
430 }
431
432 let mut new_transactions = Vec::new();
433 for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
434 if let Some(transaction) = fix_generated_transaction(
435 (*transaction).clone(),
436 tx_index_in_block,
437 *height,
438 &mut chain_value_pools,
439 &mut utxos,
440 check_transparent_coinbase_spend,
441 ) {
442 if generate_valid_commitments && *height != Height(0) {
451 for sapling_note_commitment in transaction.sapling_note_commitments() {
452 sapling_tree.append(*sapling_note_commitment).unwrap();
453 }
454 for orchard_note_commitment in transaction.orchard_note_commitments() {
455 orchard_tree.append(*orchard_note_commitment).unwrap();
456 }
457 }
458 new_transactions.push(Arc::new(transaction));
459 }
460 }
461
462 block.transactions = new_transactions;
464
465 if generate_valid_commitments {
467 let current_height = block.coinbase_height().unwrap();
468 let heartwood_height = NetworkUpgrade::Heartwood
469 .activation_height(¤t.network)
470 .unwrap();
471 let nu5_height = NetworkUpgrade::Nu5.activation_height(¤t.network);
472
473 match current_height.cmp(&heartwood_height) {
474 std::cmp::Ordering::Less => {
475 let block_header = Arc::make_mut(&mut block.header);
480 block_header.commitment_bytes = [0u8; 32].into();
481 block_header.commitment_bytes[0] = 1;
482 }
483 std::cmp::Ordering::Equal => {
484 let block_header = Arc::make_mut(&mut block.header);
486 block_header.commitment_bytes = [0u8; 32].into();
487 }
488 std::cmp::Ordering::Greater => {
489 let history_tree_root = match &history_tree {
491 Some(tree) => tree.hash().unwrap_or_else(|| [0u8; 32].into()),
492 None => [0u8; 32].into(),
493 };
494 if nu5_height.is_some() && current_height >= nu5_height.unwrap() {
495 let auth_data_root = block.auth_data_root();
497 let hash_block_commitments =
498 ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
499 &history_tree_root,
500 &auth_data_root,
501 );
502 let block_header = Arc::make_mut(&mut block.header);
503 block_header.commitment_bytes =
504 hash_block_commitments.bytes_in_serialized_order().into();
505 } else {
506 let block_header = Arc::make_mut(&mut block.header);
507 block_header.commitment_bytes =
508 history_tree_root.bytes_in_serialized_order().into();
509 }
510 }
511 }
512 if let Some(history_tree) = history_tree.as_mut() {
514 history_tree
515 .push(
516 ¤t.network,
517 Arc::new(block.clone()),
518 &sapling_tree.root(),
519 &orchard_tree.root(),
520 )
521 .unwrap();
522 } else {
523 history_tree = Some(
524 HistoryTree::from_block(
525 ¤t.network,
526 Arc::new(block.clone()),
527 &sapling_tree.root(),
528 &orchard_tree.root(),
529 )
530 .unwrap(),
531 );
532 }
533 }
534
535 previous_block_hash = Some(block.hash());
538 }
539 SummaryDebug(
540 vec.into_iter()
541 .map(|(_height, block)| Arc::new(block))
542 .collect(),
543 )
544 })
545 .boxed()
546 }
547}
548
549pub fn fix_generated_transaction<F, E>(
555 mut transaction: Transaction,
556 tx_index_in_block: usize,
557 height: Height,
558 chain_value_pools: &mut ValueBalance<NonNegative>,
559 utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
560 check_transparent_coinbase_spend: F,
561) -> Option<Transaction>
562where
563 F: Fn(
564 transparent::OutPoint,
565 transparent::CoinbaseSpendRestriction,
566 &transparent::Utxo,
567 ) -> Result<(), E>
568 + Copy
569 + 'static,
570{
571 let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height);
572 let mut new_inputs = Vec::new();
573 let mut spent_outputs = HashMap::new();
574
575 let original_inputs = transaction.inputs().to_vec();
577 for mut input in original_inputs.into_iter() {
578 if input.outpoint().is_some() {
579 if let Some(selected_outpoint) = find_valid_utxo_for_spend(
582 &mut transaction,
583 &mut spend_restriction,
584 height,
585 utxos,
586 check_transparent_coinbase_spend,
587 ) {
588 input.set_outpoint(selected_outpoint);
589 new_inputs.push(input);
590
591 let spent_utxo = utxos
592 .remove(&selected_outpoint)
593 .expect("selected outpoint must have a UTXO");
594 spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
595 }
596 } else {
598 new_inputs.push(input.clone());
600 }
601 }
602
603 *transaction.inputs_mut() = new_inputs;
605
606 let (_remaining_transaction_value, new_chain_value_pools) = transaction
607 .fix_chain_value_pools(*chain_value_pools, &spent_outputs)
608 .expect("value fixes produce valid chain value pools and remaining transaction values");
609
610 if transaction.has_transparent_or_shielded_inputs() {
612 if height > Height(0) {
615 *chain_value_pools = new_chain_value_pools;
616
617 utxos.extend(new_transaction_ordered_outputs(
618 &transaction,
619 transaction.hash(),
620 tx_index_in_block,
621 height,
622 ));
623 }
624
625 Some(transaction)
626 } else {
627 None
628 }
629}
630
631pub fn find_valid_utxo_for_spend<F, E>(
637 transaction: &mut Transaction,
638 spend_restriction: &mut CoinbaseSpendRestriction,
639 spend_height: Height,
640 utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
641 check_transparent_coinbase_spend: F,
642) -> Option<transparent::OutPoint>
643where
644 F: Fn(
645 transparent::OutPoint,
646 transparent::CoinbaseSpendRestriction,
647 &transparent::Utxo,
648 ) -> Result<(), E>
649 + Copy
650 + 'static,
651{
652 let has_shielded_outputs = transaction.has_shielded_outputs();
653 let delete_transparent_outputs =
654 CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
655 let mut attempts: usize = 0;
656
657 while let Some((candidate_outpoint, candidate_utxo)) = utxos.iter().next() {
659 attempts += 1;
660
661 if attempts > 100 {
664 return None;
665 }
666
667 if check_transparent_coinbase_spend(
669 *candidate_outpoint,
670 *spend_restriction,
671 candidate_utxo.as_ref(),
672 )
673 .is_ok()
674 {
675 return Some(*candidate_outpoint);
676 } else if has_shielded_outputs
677 && check_transparent_coinbase_spend(
678 *candidate_outpoint,
679 delete_transparent_outputs,
680 candidate_utxo.as_ref(),
681 )
682 .is_ok()
683 {
684 *transaction.outputs_mut() = Vec::new();
685 *spend_restriction = delete_transparent_outputs;
686
687 return Some(*candidate_outpoint);
688 }
689 }
690
691 None
692}
693
694impl Arbitrary for Commitment {
695 type Parameters = ();
696
697 fn arbitrary_with(_args: ()) -> Self::Strategy {
698 (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
699 .prop_map(|(commitment_bytes, network, block_height)| {
700 if block_height == Heartwood.activation_height(&network).unwrap() {
701 Commitment::ChainHistoryActivationReserved
702 } else {
703 Commitment::from_bytes(commitment_bytes, &network, block_height)
704 .expect("unexpected failure in from_bytes parsing")
705 }
706 })
707 .boxed()
708 }
709
710 type Strategy = BoxedStrategy<Self>;
711}
712
713impl Arbitrary for Header {
714 type Parameters = LedgerState;
715
716 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
717 (
718 (4u32..(i32::MAX as u32)),
720 any::<Hash>(),
721 any::<merkle::Root>(),
722 any::<HexDebug<[u8; 32]>>(),
723 serialization::arbitrary::datetime_u32(),
724 any::<CompactDifficulty>(),
725 any::<HexDebug<[u8; 32]>>(),
726 any::<equihash::Solution>(),
727 )
728 .prop_map(
729 move |(
730 version,
731 mut previous_block_hash,
732 merkle_root,
733 commitment_bytes,
734 time,
735 difficulty_threshold,
736 nonce,
737 solution,
738 )| {
739 if let Some(previous_block_hash_override) =
740 ledger_state.previous_block_hash_override
741 {
742 previous_block_hash = previous_block_hash_override;
743 } else if ledger_state.height == Height(0) {
744 previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
745 }
746
747 Header {
748 version,
749 previous_block_hash,
750 merkle_root,
751 commitment_bytes,
752 time,
753 difficulty_threshold,
754 nonce,
755 solution,
756 }
757 },
758 )
759 .boxed()
760 }
761
762 type Strategy = BoxedStrategy<Self>;
763}