//! Arbitrary data generation and test setup for Zebra's state. use std::sync::Arc; use futures::{stream::FuturesUnordered, StreamExt}; use proptest::{ num::usize::BinarySearch, prelude::*, strategy::{NewTree, ValueTree}, test_runner::TestRunner, }; use tower::{buffer::Buffer, util::BoxService, Service, ServiceExt}; use zebra_chain::{ block::Block, fmt::SummaryDebug, history_tree::HistoryTree, parameters::{Network, NetworkUpgrade}, LedgerState, }; use crate::{ arbitrary::Prepare, service::{check, ReadStateService, StateService}, BoxError, ChainTipChange, Config, LatestChainTip, PreparedBlock, Request, Response, }; pub use zebra_chain::block::arbitrary::MAX_PARTIAL_CHAIN_BLOCKS; #[derive(Debug)] pub struct PreparedChainTree { chain: Arc>>, count: BinarySearch, network: Network, history_tree: HistoryTree, } impl ValueTree for PreparedChainTree { type Value = ( Arc>>, ::Value, Network, HistoryTree, ); fn current(&self) -> Self::Value { ( self.chain.clone(), self.count.current(), self.network, self.history_tree.clone(), ) } fn simplify(&mut self) -> bool { self.count.simplify() } fn complicate(&mut self) -> bool { self.count.complicate() } } #[derive(Debug, Default)] pub struct PreparedChain { // the proptests are threaded (not async), so we want to use a threaded mutex here chain: std::sync::Mutex>>, HistoryTree)>>, // the strategy for generating LedgerStates. If None, it calls [`LedgerState::genesis_strategy`]. ledger_strategy: Option>, generate_valid_commitments: bool, } impl PreparedChain { /// Create a PreparedChain strategy with Heartwood-onward blocks. // dead_code is allowed because the function is called only by tests, // but the code is also compiled when proptest-impl is activated. #[allow(dead_code)] pub(crate) fn new_heartwood() -> Self { // The history tree only works with Heartwood onward. // Since the network will be chosen later, we pick the larger // between the mainnet and testnet Heartwood activation heights. let main_height = NetworkUpgrade::Heartwood .activation_height(Network::Mainnet) .expect("must have height"); let test_height = NetworkUpgrade::Heartwood .activation_height(Network::Testnet) .expect("must have height"); let height = std::cmp::max(main_height, test_height); PreparedChain { ledger_strategy: Some(LedgerState::height_strategy( height, NetworkUpgrade::Nu5, None, false, )), ..Default::default() } } /// Transform the strategy to use valid commitments in the block. /// /// This is slower so it should be used only when needed. #[allow(dead_code)] pub(crate) fn with_valid_commitments(mut self) -> Self { self.generate_valid_commitments = true; self } } impl Strategy for PreparedChain { type Tree = PreparedChainTree; type Value = ::Value; fn new_tree(&self, runner: &mut TestRunner) -> NewTree { let mut chain = self.chain.lock().unwrap(); if chain.is_none() { // TODO: use the latest network upgrade (#1974) let default_ledger_strategy = LedgerState::genesis_strategy(NetworkUpgrade::Nu5, None, false); let ledger_strategy = self .ledger_strategy .as_ref() .unwrap_or(&default_ledger_strategy); let (network, blocks) = ledger_strategy .prop_flat_map(|ledger| { ( Just(ledger.network), Block::partial_chain_strategy( ledger, MAX_PARTIAL_CHAIN_BLOCKS, check::utxo::transparent_coinbase_spend, self.generate_valid_commitments, ), ) }) .prop_map(|(network, vec)| { ( network, vec.iter() .map(|blk| blk.clone().prepare()) .collect::>(), ) }) .new_tree(runner)? .current(); // Generate a history tree from the first block let history_tree = HistoryTree::from_block( network, blocks[0].block.clone(), // Dummy roots since this is only used for tests &Default::default(), &Default::default(), ) .expect("history tree should be created"); *chain = Some((network, Arc::new(SummaryDebug(blocks)), history_tree)); } let chain = chain.clone().expect("should be generated"); let count = (1..chain.1.len()).new_tree(runner)?; Ok(PreparedChainTree { chain: chain.1, count, network: chain.0, history_tree: chain.2, }) } } /// Initialize a state service with blocks, and return: /// - a read-write [`StateService`] /// - a read-only [`ReadStateService`] /// - a [`LatestChainTip`] /// - a [`ChainTipChange`] tracker pub async fn populated_state( blocks: impl IntoIterator>, network: Network, ) -> ( Buffer, Request>, ReadStateService, LatestChainTip, ChainTipChange, ) { let requests = blocks .into_iter() .map(|block| Request::CommitFinalizedBlock(block.into())); let (state, read_state, latest_chain_tip, chain_tip_change) = StateService::new(Config::ephemeral(), network); let mut state = Buffer::new(BoxService::new(state), 1); let mut responses = FuturesUnordered::new(); for request in requests { let rsp = state.ready().await.unwrap().call(request); responses.push(rsp); } while let Some(rsp) = responses.next().await { rsp.expect("blocks should commit just fine"); } (state, read_state, latest_chain_tip, chain_tip_change) }