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)]
106pub struct LedgerStateOverride {
107 pub network_override: Option<Network>,
109
110 pub height_override: Option<Height>,
112
113 pub previous_block_hash_override: Option<block::Hash>,
116
117 pub network_upgrade_override: Option<NetworkUpgrade>,
120
121 pub transaction_version_override: Option<u32>,
123
124 pub transaction_has_valid_network_upgrade: bool,
128
129 pub always_has_coinbase: bool,
132}
133
134impl LedgerState {
135 pub fn default_strategy() -> BoxedStrategy<Self> {
137 Self::arbitrary_with(LedgerStateOverride::default())
138 }
139
140 pub fn no_override_strategy() -> BoxedStrategy<Self> {
143 Self::arbitrary_with(LedgerStateOverride {
144 network_override: None,
145 height_override: None,
146 previous_block_hash_override: None,
147 network_upgrade_override: None,
148 transaction_version_override: None,
149 transaction_has_valid_network_upgrade: false,
150 always_has_coinbase: false,
151 })
152 }
153
154 pub fn network_upgrade_strategy(
159 network_upgrade_override: NetworkUpgrade,
160 transaction_version_override: impl Into<Option<u32>>,
161 transaction_has_valid_network_upgrade: bool,
162 ) -> BoxedStrategy<Self> {
163 Self::arbitrary_with(LedgerStateOverride {
164 network_override: None,
165 height_override: None,
166 previous_block_hash_override: None,
167 network_upgrade_override: Some(network_upgrade_override),
168 transaction_version_override: transaction_version_override.into(),
169 transaction_has_valid_network_upgrade,
170 always_has_coinbase: false,
171 })
172 }
173
174 pub fn coinbase_strategy(
179 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
180 transaction_version_override: impl Into<Option<u32>>,
181 transaction_has_valid_network_upgrade: bool,
182 ) -> BoxedStrategy<Self> {
183 Self::arbitrary_with(LedgerStateOverride {
184 network_override: None,
185 height_override: None,
186 previous_block_hash_override: None,
187 network_upgrade_override: network_upgrade_override.into(),
188 transaction_version_override: transaction_version_override.into(),
189 transaction_has_valid_network_upgrade,
190 always_has_coinbase: true,
191 })
192 }
193
194 pub fn genesis_strategy(
203 network_override: impl Into<Option<Network>>,
204 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
205 transaction_version_override: impl Into<Option<u32>>,
206 transaction_has_valid_network_upgrade: bool,
207 ) -> BoxedStrategy<Self> {
208 Self::arbitrary_with(LedgerStateOverride {
209 network_override: network_override.into(),
210 height_override: Some(Height(0)),
211 previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH),
212 network_upgrade_override: network_upgrade_override.into(),
213 transaction_version_override: transaction_version_override.into(),
214 transaction_has_valid_network_upgrade,
215 always_has_coinbase: true,
216 })
217 }
218
219 pub fn height_strategy(
224 height: Height,
225 network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
226 transaction_version_override: impl Into<Option<u32>>,
227 transaction_has_valid_network_upgrade: bool,
228 ) -> BoxedStrategy<Self> {
229 Self::arbitrary_with(LedgerStateOverride {
230 network_override: None,
231 height_override: Some(height),
232 previous_block_hash_override: None,
233 network_upgrade_override: network_upgrade_override.into(),
234 transaction_version_override: transaction_version_override.into(),
235 transaction_has_valid_network_upgrade,
236 always_has_coinbase: true,
237 })
238 }
239
240 pub fn network_upgrade(&self) -> NetworkUpgrade {
245 if let Some(network_upgrade_override) = self.network_upgrade_override {
246 network_upgrade_override
247 } else {
248 NetworkUpgrade::current(&self.network, self.height)
249 }
250 }
251
252 pub fn transaction_version_override(&self) -> Option<u32> {
254 self.transaction_version_override
255 }
256
257 pub fn transaction_has_valid_network_upgrade(&self) -> bool {
261 self.transaction_has_valid_network_upgrade
262 }
263}
264
265impl Default for LedgerState {
266 fn default() -> Self {
267 let default_network = Network::default();
269 let default_override = LedgerStateOverride::default();
270
271 let most_recent_nu = NetworkUpgrade::current(&default_network, Height::MAX);
272 let most_recent_activation_height =
273 most_recent_nu.activation_height(&default_network).unwrap();
274
275 LedgerState {
276 height: most_recent_activation_height,
277 network: default_network,
278 network_upgrade_override: default_override.network_upgrade_override,
279 previous_block_hash_override: default_override.previous_block_hash_override,
280 transaction_version_override: default_override.transaction_version_override,
281 transaction_has_valid_network_upgrade: default_override
282 .transaction_has_valid_network_upgrade,
283 has_coinbase: default_override.always_has_coinbase,
284 }
285 }
286}
287
288impl Default for LedgerStateOverride {
289 fn default() -> Self {
290 let default_network = Network::default();
291
292 let nu5_activation_height = Nu5.activation_height(&default_network);
294 let nu5_override = if nu5_activation_height.is_some() {
295 None
296 } else {
297 Some(Nu5)
298 };
299
300 LedgerStateOverride {
301 network_override: None,
302 height_override: None,
303 previous_block_hash_override: None,
304 network_upgrade_override: nu5_override,
305 transaction_version_override: None,
306 transaction_has_valid_network_upgrade: false,
307 always_has_coinbase: true,
308 }
309 }
310}
311
312impl Arbitrary for LedgerState {
313 type Parameters = LedgerStateOverride;
314
315 fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
321 (
322 any::<Height>(),
323 any::<Network>(),
324 any::<bool>(),
325 any::<bool>(),
326 )
327 .prop_map(
328 move |(height, network, transaction_has_valid_network_upgrade, has_coinbase)| {
329 LedgerState {
330 height: ledger_override.height_override.unwrap_or(height),
331 network: ledger_override
332 .network_override
333 .as_ref()
334 .unwrap_or(&network)
335 .clone(),
336 network_upgrade_override: ledger_override.network_upgrade_override,
337 previous_block_hash_override: ledger_override.previous_block_hash_override,
338 transaction_version_override: ledger_override.transaction_version_override,
339 transaction_has_valid_network_upgrade: ledger_override
340 .transaction_has_valid_network_upgrade
341 || transaction_has_valid_network_upgrade,
342 has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
343 }
344 },
345 )
346 .boxed()
347 }
348
349 type Strategy = BoxedStrategy<Self>;
350}
351
352impl Arbitrary for Block {
353 type Parameters = LedgerState;
354
355 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
356 let transactions_strategy = {
357 let ledger_state = ledger_state.clone();
358 (0..MAX_ARBITRARY_ITEMS).prop_flat_map(move |transaction_count| {
361 Transaction::vec_strategy(ledger_state.clone(), transaction_count)
362 })
363 };
364
365 (Header::arbitrary_with(ledger_state), transactions_strategy)
370 .prop_map(move |(header, transactions)| Self {
371 header: header.into(),
372 transactions,
373 })
374 .boxed()
375 }
376
377 type Strategy = BoxedStrategy<Self>;
378}
379
380#[allow(clippy::result_unit_err)]
382pub fn allow_all_transparent_coinbase_spends(
383 _: transparent::OutPoint,
384 _: transparent::CoinbaseSpendRestriction,
385 _: &transparent::Utxo,
386) -> Result<(), ()> {
387 Ok(())
388}
389
390impl Block {
391 pub fn partial_chain_strategy<F, E>(
403 mut current: LedgerState,
404 count: usize,
405 check_transparent_coinbase_spend: F,
406 generate_valid_commitments: bool,
407 ) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>>
408 where
409 F: Fn(
410 transparent::OutPoint,
411 transparent::CoinbaseSpendRestriction,
412 &transparent::Utxo,
413 ) -> Result<(), E>
414 + Copy
415 + 'static,
416 {
417 let mut vec = Vec::with_capacity(count);
418
419 for _ in 0..count {
421 vec.push((Just(current.height), Block::arbitrary_with(current.clone())));
422 current.height.0 += 1;
423 }
424
425 vec.prop_map(move |mut vec| {
427 let mut previous_block_hash = None;
428 let mut utxos = HashMap::new();
429 let mut chain_value_pools = ValueBalance::zero();
430 let mut sapling_tree = sapling::tree::NoteCommitmentTree::default();
431 let mut orchard_tree = orchard::tree::NoteCommitmentTree::default();
432 let mut history_tree: Option<HistoryTree> = None;
439
440 for (height, block) in vec.iter_mut() {
441 if let Some(previous_block_hash) = previous_block_hash {
443 Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
444 }
445
446 let mut new_transactions = Vec::new();
447 for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
448 if let Some(transaction) = fix_generated_transaction(
449 (*transaction).clone(),
450 tx_index_in_block,
451 *height,
452 &mut chain_value_pools,
453 &mut utxos,
454 check_transparent_coinbase_spend,
455 ) {
456 if generate_valid_commitments && *height != Height(0) {
465 for sapling_note_commitment in transaction.sapling_note_commitments() {
466 sapling_tree.append(*sapling_note_commitment).unwrap();
467 }
468 for orchard_note_commitment in transaction.orchard_note_commitments() {
469 orchard_tree.append(*orchard_note_commitment).unwrap();
470 }
471 }
472 new_transactions.push(Arc::new(transaction));
473 }
474 }
475
476 block.transactions = new_transactions;
478
479 if generate_valid_commitments {
481 let current_height = block.coinbase_height().unwrap();
482 let heartwood_height = NetworkUpgrade::Heartwood
483 .activation_height(¤t.network)
484 .unwrap();
485 let nu5_height = NetworkUpgrade::Nu5.activation_height(¤t.network);
486
487 match current_height.cmp(&heartwood_height) {
488 std::cmp::Ordering::Less => {
489 let block_header = Arc::make_mut(&mut block.header);
494 block_header.commitment_bytes = [0u8; 32].into();
495 block_header.commitment_bytes[0] = 1;
496 }
497 std::cmp::Ordering::Equal => {
498 let block_header = Arc::make_mut(&mut block.header);
500 block_header.commitment_bytes = [0u8; 32].into();
501 }
502 std::cmp::Ordering::Greater => {
503 let history_tree_root = match &history_tree {
505 Some(tree) => tree.hash().unwrap_or_else(|| [0u8; 32].into()),
506 None => [0u8; 32].into(),
507 };
508 if nu5_height.is_some() && current_height >= nu5_height.unwrap() {
509 let auth_data_root = block.auth_data_root();
511 let hash_block_commitments =
512 ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
513 &history_tree_root,
514 &auth_data_root,
515 );
516 let block_header = Arc::make_mut(&mut block.header);
517 block_header.commitment_bytes =
518 hash_block_commitments.bytes_in_serialized_order().into();
519 } else {
520 let block_header = Arc::make_mut(&mut block.header);
521 block_header.commitment_bytes =
522 history_tree_root.bytes_in_serialized_order().into();
523 }
524 }
525 }
526 if let Some(history_tree) = history_tree.as_mut() {
528 history_tree
529 .push(
530 ¤t.network,
531 Arc::new(block.clone()),
532 &sapling_tree.root(),
533 &orchard_tree.root(),
534 )
535 .unwrap();
536 } else {
537 history_tree = Some(
538 HistoryTree::from_block(
539 ¤t.network,
540 Arc::new(block.clone()),
541 &sapling_tree.root(),
542 &orchard_tree.root(),
543 )
544 .unwrap(),
545 );
546 }
547 }
548
549 previous_block_hash = Some(block.hash());
552 }
553 SummaryDebug(
554 vec.into_iter()
555 .map(|(_height, block)| Arc::new(block))
556 .collect(),
557 )
558 })
559 .boxed()
560 }
561}
562
563pub fn fix_generated_transaction<F, E>(
569 mut transaction: Transaction,
570 tx_index_in_block: usize,
571 height: Height,
572 chain_value_pools: &mut ValueBalance<NonNegative>,
573 utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
574 check_transparent_coinbase_spend: F,
575) -> Option<Transaction>
576where
577 F: Fn(
578 transparent::OutPoint,
579 transparent::CoinbaseSpendRestriction,
580 &transparent::Utxo,
581 ) -> Result<(), E>
582 + Copy
583 + 'static,
584{
585 let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height);
586 let mut new_inputs = Vec::new();
587 let mut spent_outputs = HashMap::new();
588
589 let original_inputs = transaction.inputs().to_vec();
591 for mut input in original_inputs.into_iter() {
592 if input.outpoint().is_some() {
593 if let Some(selected_outpoint) = find_valid_utxo_for_spend(
596 &mut transaction,
597 &mut spend_restriction,
598 height,
599 utxos,
600 check_transparent_coinbase_spend,
601 ) {
602 input.set_outpoint(selected_outpoint);
603 new_inputs.push(input);
604
605 let spent_utxo = utxos
606 .remove(&selected_outpoint)
607 .expect("selected outpoint must have a UTXO");
608 spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
609 }
610 } else {
612 new_inputs.push(input.clone());
614 }
615 }
616
617 *transaction.inputs_mut() = new_inputs;
619
620 let (_remaining_transaction_value, new_chain_value_pools) = transaction
621 .fix_chain_value_pools(*chain_value_pools, &spent_outputs)
622 .expect("value fixes produce valid chain value pools and remaining transaction values");
623
624 if transaction.has_transparent_or_shielded_inputs() {
626 if height > Height(0) {
629 *chain_value_pools = new_chain_value_pools;
630
631 utxos.extend(new_transaction_ordered_outputs(
632 &transaction,
633 transaction.hash(),
634 tx_index_in_block,
635 height,
636 ));
637 }
638
639 Some(transaction)
640 } else {
641 None
642 }
643}
644
645pub fn find_valid_utxo_for_spend<F, E>(
651 transaction: &mut Transaction,
652 spend_restriction: &mut CoinbaseSpendRestriction,
653 spend_height: Height,
654 utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
655 check_transparent_coinbase_spend: F,
656) -> Option<transparent::OutPoint>
657where
658 F: Fn(
659 transparent::OutPoint,
660 transparent::CoinbaseSpendRestriction,
661 &transparent::Utxo,
662 ) -> Result<(), E>
663 + Copy
664 + 'static,
665{
666 let has_shielded_outputs = transaction.has_shielded_outputs();
667 let delete_transparent_outputs =
668 CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
669 for (candidate_outpoint, candidate_utxo) in utxos.iter().take(100) {
671 if check_transparent_coinbase_spend(
675 *candidate_outpoint,
676 *spend_restriction,
677 candidate_utxo.as_ref(),
678 )
679 .is_ok()
680 {
681 return Some(*candidate_outpoint);
682 } else if has_shielded_outputs
683 && check_transparent_coinbase_spend(
684 *candidate_outpoint,
685 delete_transparent_outputs,
686 candidate_utxo.as_ref(),
687 )
688 .is_ok()
689 {
690 *transaction.outputs_mut() = Vec::new();
691 *spend_restriction = delete_transparent_outputs;
692
693 return Some(*candidate_outpoint);
694 }
695 }
696
697 None
698}
699
700impl Arbitrary for Commitment {
701 type Parameters = ();
702
703 fn arbitrary_with(_args: ()) -> Self::Strategy {
704 (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
705 .prop_map(|(commitment_bytes, network, block_height)| {
706 if block_height == Heartwood.activation_height(&network).unwrap() {
707 Commitment::ChainHistoryActivationReserved
708 } else {
709 Commitment::from_bytes(commitment_bytes, &network, block_height)
710 .expect("unexpected failure in from_bytes parsing")
711 }
712 })
713 .boxed()
714 }
715
716 type Strategy = BoxedStrategy<Self>;
717}
718
719impl Arbitrary for Header {
720 type Parameters = LedgerState;
721
722 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
723 (
724 (4u32..(i32::MAX as u32)),
726 any::<Hash>(),
727 any::<merkle::Root>(),
728 any::<HexDebug<[u8; 32]>>(),
729 serialization::arbitrary::datetime_u32(),
730 any::<CompactDifficulty>(),
731 any::<HexDebug<[u8; 32]>>(),
732 any::<equihash::Solution>(),
733 )
734 .prop_map(
735 move |(
736 version,
737 mut previous_block_hash,
738 merkle_root,
739 commitment_bytes,
740 time,
741 difficulty_threshold,
742 nonce,
743 solution,
744 )| {
745 if let Some(previous_block_hash_override) =
746 ledger_state.previous_block_hash_override
747 {
748 previous_block_hash = previous_block_hash_override;
749 } else if ledger_state.height == Height(0) {
750 previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
751 }
752
753 Header {
754 version,
755 previous_block_hash,
756 merkle_root,
757 commitment_bytes,
758 time,
759 difficulty_threshold,
760 nonce,
761 solution,
762 }
763 },
764 )
765 .boxed()
766 }
767
768 type Strategy = BoxedStrategy<Self>;
769}