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::{self, BytesInDisplayOrder},
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.remove(&selected_outpoint)?;
606 spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
607 }
608 } else {
610 new_inputs.push(input.clone());
612 }
613 }
614
615 *transaction.inputs_mut() = new_inputs;
617
618 let (_remaining_transaction_value, new_chain_value_pools) = transaction
619 .fix_chain_value_pools(*chain_value_pools, &spent_outputs)
620 .expect("value fixes produce valid chain value pools and remaining transaction values");
621
622 if transaction.has_transparent_or_shielded_inputs() {
624 if height > Height(0) {
627 *chain_value_pools = new_chain_value_pools;
628
629 utxos.extend(new_transaction_ordered_outputs(
630 &transaction,
631 transaction.hash(),
632 tx_index_in_block,
633 height,
634 ));
635 }
636
637 Some(transaction)
638 } else {
639 None
640 }
641}
642
643pub fn find_valid_utxo_for_spend<F, E>(
649 transaction: &mut Transaction,
650 spend_restriction: &mut CoinbaseSpendRestriction,
651 spend_height: Height,
652 utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
653 check_transparent_coinbase_spend: F,
654) -> Option<transparent::OutPoint>
655where
656 F: Fn(
657 transparent::OutPoint,
658 transparent::CoinbaseSpendRestriction,
659 &transparent::Utxo,
660 ) -> Result<(), E>
661 + Copy
662 + 'static,
663{
664 let has_shielded_outputs = transaction.has_shielded_outputs();
665 let delete_transparent_outputs =
666 CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
667 for (candidate_outpoint, candidate_utxo) in utxos.iter().take(100) {
669 if check_transparent_coinbase_spend(
673 *candidate_outpoint,
674 *spend_restriction,
675 candidate_utxo.as_ref(),
676 )
677 .is_ok()
678 {
679 return Some(*candidate_outpoint);
680 } else if has_shielded_outputs
681 && check_transparent_coinbase_spend(
682 *candidate_outpoint,
683 delete_transparent_outputs,
684 candidate_utxo.as_ref(),
685 )
686 .is_ok()
687 {
688 *transaction.outputs_mut() = Vec::new();
689 *spend_restriction = delete_transparent_outputs;
690
691 return Some(*candidate_outpoint);
692 }
693 }
694
695 None
696}
697
698impl Arbitrary for Commitment {
699 type Parameters = ();
700
701 fn arbitrary_with(_args: ()) -> Self::Strategy {
702 (any::<[u8; 32]>(), any::<Network>(), any::<Height>())
703 .prop_map(|(commitment_bytes, network, block_height)| {
704 if block_height == Heartwood.activation_height(&network).unwrap() {
705 Commitment::ChainHistoryActivationReserved
706 } else {
707 Commitment::from_bytes(commitment_bytes, &network, block_height)
708 .expect("unexpected failure in from_bytes parsing")
709 }
710 })
711 .boxed()
712 }
713
714 type Strategy = BoxedStrategy<Self>;
715}
716
717impl Arbitrary for Header {
718 type Parameters = LedgerState;
719
720 fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
721 (
722 (4u32..(i32::MAX as u32)),
724 any::<Hash>(),
725 any::<merkle::Root>(),
726 any::<HexDebug<[u8; 32]>>(),
727 serialization::arbitrary::datetime_u32(),
728 any::<CompactDifficulty>(),
729 any::<HexDebug<[u8; 32]>>(),
730 any::<equihash::Solution>(),
731 )
732 .prop_map(
733 move |(
734 version,
735 mut previous_block_hash,
736 merkle_root,
737 commitment_bytes,
738 time,
739 difficulty_threshold,
740 nonce,
741 solution,
742 )| {
743 if let Some(previous_block_hash_override) =
744 ledger_state.previous_block_hash_override
745 {
746 previous_block_hash = previous_block_hash_override;
747 } else if ledger_state.height == Height(0) {
748 previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
749 }
750
751 Header {
752 version,
753 previous_block_hash,
754 merkle_root,
755 commitment_bytes,
756 time,
757 difficulty_threshold,
758 nonce,
759 solution,
760 }
761 },
762 )
763 .boxed()
764 }
765
766 type Strategy = BoxedStrategy<Self>;
767}