use proptest::prelude::*;
use crate::{
amount::NonNegative,
block,
fmt::{HexDebug, SummaryDebug},
history_tree::HistoryTree,
parameters::{NetworkUpgrade::*, GENESIS_PREVIOUS_BLOCK_HASH},
serialization::{self, BytesInDisplayOrder},
transaction::arbitrary::MAX_ARBITRARY_ITEMS,
transparent::{
new_transaction_ordered_outputs, CoinbaseSpendRestriction,
MIN_TRANSPARENT_COINBASE_MATURITY,
},
work::{difficulty::CompactDifficulty, equihash},
};
use super::*;
pub const PREVOUTS_CHAIN_HEIGHT: usize = 4;
pub const MAX_PARTIAL_CHAIN_BLOCKS: usize =
MIN_TRANSPARENT_COINBASE_MATURITY as usize + PREVOUTS_CHAIN_HEIGHT;
impl Arbitrary for Height {
type Parameters = ();
fn arbitrary_with(_args: ()) -> Self::Strategy {
(Height::MIN.0..=Height::MAX.0).prop_map(Height).boxed()
}
type Strategy = BoxedStrategy<Self>;
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct LedgerState {
pub height: Height,
pub network: Network,
network_upgrade_override: Option<NetworkUpgrade>,
previous_block_hash_override: Option<block::Hash>,
transaction_version_override: Option<u32>,
transaction_has_valid_network_upgrade: bool,
pub(crate) has_coinbase: bool,
}
#[derive(Debug, Clone)]
pub struct LedgerStateOverride {
pub network_override: Option<Network>,
pub height_override: Option<Height>,
pub previous_block_hash_override: Option<block::Hash>,
pub network_upgrade_override: Option<NetworkUpgrade>,
pub transaction_version_override: Option<u32>,
pub transaction_has_valid_network_upgrade: bool,
pub always_has_coinbase: bool,
}
impl LedgerState {
pub fn default_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride::default())
}
pub fn no_override_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_override: None,
height_override: None,
previous_block_hash_override: None,
network_upgrade_override: None,
transaction_version_override: None,
transaction_has_valid_network_upgrade: false,
always_has_coinbase: false,
})
}
pub fn network_upgrade_strategy(
network_upgrade_override: NetworkUpgrade,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_override: None,
height_override: None,
previous_block_hash_override: None,
network_upgrade_override: Some(network_upgrade_override),
transaction_version_override: transaction_version_override.into(),
transaction_has_valid_network_upgrade,
always_has_coinbase: false,
})
}
pub fn coinbase_strategy(
network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_override: None,
height_override: None,
previous_block_hash_override: None,
network_upgrade_override: network_upgrade_override.into(),
transaction_version_override: transaction_version_override.into(),
transaction_has_valid_network_upgrade,
always_has_coinbase: true,
})
}
pub fn genesis_strategy(
network_override: impl Into<Option<Network>>,
network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_override: network_override.into(),
height_override: Some(Height(0)),
previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH),
network_upgrade_override: network_upgrade_override.into(),
transaction_version_override: transaction_version_override.into(),
transaction_has_valid_network_upgrade,
always_has_coinbase: true,
})
}
pub fn height_strategy(
height: Height,
network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_override: None,
height_override: Some(height),
previous_block_hash_override: None,
network_upgrade_override: network_upgrade_override.into(),
transaction_version_override: transaction_version_override.into(),
transaction_has_valid_network_upgrade,
always_has_coinbase: true,
})
}
pub fn network_upgrade(&self) -> NetworkUpgrade {
if let Some(network_upgrade_override) = self.network_upgrade_override {
network_upgrade_override
} else {
NetworkUpgrade::current(&self.network, self.height)
}
}
pub fn transaction_version_override(&self) -> Option<u32> {
self.transaction_version_override
}
pub fn transaction_has_valid_network_upgrade(&self) -> bool {
self.transaction_has_valid_network_upgrade
}
}
impl Default for LedgerState {
fn default() -> Self {
let default_network = Network::default();
let default_override = LedgerStateOverride::default();
let most_recent_nu = NetworkUpgrade::current(&default_network, Height::MAX);
let most_recent_activation_height =
most_recent_nu.activation_height(&default_network).unwrap();
LedgerState {
height: most_recent_activation_height,
network: default_network,
network_upgrade_override: default_override.network_upgrade_override,
previous_block_hash_override: default_override.previous_block_hash_override,
transaction_version_override: default_override.transaction_version_override,
transaction_has_valid_network_upgrade: default_override
.transaction_has_valid_network_upgrade,
has_coinbase: default_override.always_has_coinbase,
}
}
}
impl Default for LedgerStateOverride {
fn default() -> Self {
let default_network = Network::default();
let nu5_activation_height = Nu5.activation_height(&default_network);
let nu5_override = if nu5_activation_height.is_some() {
None
} else {
Some(Nu5)
};
LedgerStateOverride {
network_override: None,
height_override: None,
previous_block_hash_override: None,
network_upgrade_override: nu5_override,
transaction_version_override: None,
transaction_has_valid_network_upgrade: false,
always_has_coinbase: true,
}
}
}
impl Arbitrary for LedgerState {
type Parameters = LedgerStateOverride;
fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
(
any::<Height>(),
any::<Network>(),
any::<bool>(),
any::<bool>(),
)
.prop_map(
move |(height, network, transaction_has_valid_network_upgrade, has_coinbase)| {
LedgerState {
height: ledger_override.height_override.unwrap_or(height),
network: ledger_override
.network_override
.as_ref()
.unwrap_or(&network)
.clone(),
network_upgrade_override: ledger_override.network_upgrade_override,
previous_block_hash_override: ledger_override.previous_block_hash_override,
transaction_version_override: ledger_override.transaction_version_override,
transaction_has_valid_network_upgrade: ledger_override
.transaction_has_valid_network_upgrade
|| transaction_has_valid_network_upgrade,
has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
}
},
)
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Block {
type Parameters = LedgerState;
fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
let transactions_strategy = {
let ledger_state = ledger_state.clone();
(0..MAX_ARBITRARY_ITEMS).prop_flat_map(move |transaction_count| {
Transaction::vec_strategy(ledger_state.clone(), transaction_count)
})
};
(Header::arbitrary_with(ledger_state), transactions_strategy)
.prop_map(move |(header, transactions)| Self {
header: header.into(),
transactions,
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
#[allow(clippy::result_unit_err)]
pub fn allow_all_transparent_coinbase_spends(
_: transparent::OutPoint,
_: transparent::CoinbaseSpendRestriction,
_: &transparent::Utxo,
) -> Result<(), ()> {
Ok(())
}
impl Block {
pub fn partial_chain_strategy<F, E>(
mut current: LedgerState,
count: usize,
check_transparent_coinbase_spend: F,
generate_valid_commitments: bool,
) -> BoxedStrategy<SummaryDebug<Vec<Arc<Self>>>>
where
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
&transparent::Utxo,
) -> Result<(), E>
+ Copy
+ 'static,
{
let mut vec = Vec::with_capacity(count);
for _ in 0..count {
vec.push((Just(current.height), Block::arbitrary_with(current.clone())));
current.height.0 += 1;
}
vec.prop_map(move |mut vec| {
let mut previous_block_hash = None;
let mut utxos = HashMap::new();
let mut chain_value_pools = ValueBalance::zero();
let mut sapling_tree = sapling::tree::NoteCommitmentTree::default();
let mut orchard_tree = orchard::tree::NoteCommitmentTree::default();
let mut history_tree: Option<HistoryTree> = None;
for (height, block) in vec.iter_mut() {
if let Some(previous_block_hash) = previous_block_hash {
Arc::make_mut(&mut block.header).previous_block_hash = previous_block_hash;
}
let mut new_transactions = Vec::new();
for (tx_index_in_block, transaction) in block.transactions.drain(..).enumerate() {
if let Some(transaction) = fix_generated_transaction(
(*transaction).clone(),
tx_index_in_block,
*height,
&mut chain_value_pools,
&mut utxos,
check_transparent_coinbase_spend,
) {
if generate_valid_commitments && *height != Height(0) {
for sapling_note_commitment in transaction.sapling_note_commitments() {
sapling_tree.append(*sapling_note_commitment).unwrap();
}
for orchard_note_commitment in transaction.orchard_note_commitments() {
orchard_tree.append(*orchard_note_commitment).unwrap();
}
}
new_transactions.push(Arc::new(transaction));
}
}
block.transactions = new_transactions;
if generate_valid_commitments {
let current_height = block.coinbase_height().unwrap();
let heartwood_height = NetworkUpgrade::Heartwood
.activation_height(¤t.network)
.unwrap();
let nu5_height = NetworkUpgrade::Nu5.activation_height(¤t.network);
match current_height.cmp(&heartwood_height) {
std::cmp::Ordering::Less => {
let block_header = Arc::make_mut(&mut block.header);
block_header.commitment_bytes = [0u8; 32].into();
block_header.commitment_bytes[0] = 1;
}
std::cmp::Ordering::Equal => {
let block_header = Arc::make_mut(&mut block.header);
block_header.commitment_bytes = [0u8; 32].into();
}
std::cmp::Ordering::Greater => {
let history_tree_root = match &history_tree {
Some(tree) => tree.hash().unwrap_or_else(|| [0u8; 32].into()),
None => [0u8; 32].into(),
};
if nu5_height.is_some() && current_height >= nu5_height.unwrap() {
let auth_data_root = block.auth_data_root();
let hash_block_commitments =
ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
&history_tree_root,
&auth_data_root,
);
let block_header = Arc::make_mut(&mut block.header);
block_header.commitment_bytes =
hash_block_commitments.bytes_in_serialized_order().into();
} else {
let block_header = Arc::make_mut(&mut block.header);
block_header.commitment_bytes =
history_tree_root.bytes_in_serialized_order().into();
}
}
}
if let Some(history_tree) = history_tree.as_mut() {
history_tree
.push(
¤t.network,
Arc::new(block.clone()),
&sapling_tree.root(),
&orchard_tree.root(),
)
.unwrap();
} else {
history_tree = Some(
HistoryTree::from_block(
¤t.network,
Arc::new(block.clone()),
&sapling_tree.root(),
&orchard_tree.root(),
)
.unwrap(),
);
}
}
previous_block_hash = Some(block.hash());
}
SummaryDebug(
vec.into_iter()
.map(|(_height, block)| Arc::new(block))
.collect(),
)
})
.boxed()
}
}
pub fn fix_generated_transaction<F, E>(
mut transaction: Transaction,
tx_index_in_block: usize,
height: Height,
chain_value_pools: &mut ValueBalance<NonNegative>,
utxos: &mut HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
check_transparent_coinbase_spend: F,
) -> Option<Transaction>
where
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
&transparent::Utxo,
) -> Result<(), E>
+ Copy
+ 'static,
{
let mut spend_restriction = transaction.coinbase_spend_restriction(&Network::Mainnet, height);
let mut new_inputs = Vec::new();
let mut spent_outputs = HashMap::new();
let original_inputs = transaction.inputs().to_vec();
for mut input in original_inputs.into_iter() {
if input.outpoint().is_some() {
if let Some(selected_outpoint) = find_valid_utxo_for_spend(
&mut transaction,
&mut spend_restriction,
height,
utxos,
check_transparent_coinbase_spend,
) {
input.set_outpoint(selected_outpoint);
new_inputs.push(input);
let spent_utxo = utxos.remove(&selected_outpoint)?;
spent_outputs.insert(selected_outpoint, spent_utxo.utxo.output);
}
} else {
new_inputs.push(input.clone());
}
}
*transaction.inputs_mut() = new_inputs;
let (_remaining_transaction_value, new_chain_value_pools) = transaction
.fix_chain_value_pools(*chain_value_pools, &spent_outputs)
.expect("value fixes produce valid chain value pools and remaining transaction values");
if transaction.has_transparent_or_shielded_inputs() {
if height > Height(0) {
*chain_value_pools = new_chain_value_pools;
utxos.extend(new_transaction_ordered_outputs(
&transaction,
transaction.hash(),
tx_index_in_block,
height,
));
}
Some(transaction)
} else {
None
}
}
pub fn find_valid_utxo_for_spend<F, E>(
transaction: &mut Transaction,
spend_restriction: &mut CoinbaseSpendRestriction,
spend_height: Height,
utxos: &HashMap<transparent::OutPoint, transparent::OrderedUtxo>,
check_transparent_coinbase_spend: F,
) -> Option<transparent::OutPoint>
where
F: Fn(
transparent::OutPoint,
transparent::CoinbaseSpendRestriction,
&transparent::Utxo,
) -> Result<(), E>
+ Copy
+ 'static,
{
let has_shielded_outputs = transaction.has_shielded_outputs();
let delete_transparent_outputs =
CoinbaseSpendRestriction::CheckCoinbaseMaturity { spend_height };
for (candidate_outpoint, candidate_utxo) in utxos.iter().take(100) {
if check_transparent_coinbase_spend(
*candidate_outpoint,
*spend_restriction,
candidate_utxo.as_ref(),
)
.is_ok()
{
return Some(*candidate_outpoint);
} else if has_shielded_outputs
&& check_transparent_coinbase_spend(
*candidate_outpoint,
delete_transparent_outputs,
candidate_utxo.as_ref(),
)
.is_ok()
{
*transaction.outputs_mut() = Vec::new();
*spend_restriction = delete_transparent_outputs;
return Some(*candidate_outpoint);
}
}
None
}
impl Arbitrary for Commitment {
type Parameters = ();
fn arbitrary_with(_args: ()) -> Self::Strategy {
(any::<[u8; 32]>(), any::<Network>(), any::<Height>())
.prop_map(|(commitment_bytes, network, block_height)| {
if block_height == Heartwood.activation_height(&network).unwrap() {
Commitment::ChainHistoryActivationReserved
} else {
Commitment::from_bytes(commitment_bytes, &network, block_height)
.expect("unexpected failure in from_bytes parsing")
}
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl Arbitrary for Header {
type Parameters = LedgerState;
fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
(
(4u32..(i32::MAX as u32)),
any::<Hash>(),
any::<merkle::Root>(),
any::<HexDebug<[u8; 32]>>(),
serialization::arbitrary::datetime_u32(),
any::<CompactDifficulty>(),
any::<HexDebug<[u8; 32]>>(),
any::<equihash::Solution>(),
)
.prop_map(
move |(
version,
mut previous_block_hash,
merkle_root,
commitment_bytes,
time,
difficulty_threshold,
nonce,
solution,
)| {
if let Some(previous_block_hash_override) =
ledger_state.previous_block_hash_override
{
previous_block_hash = previous_block_hash_override;
} else if ledger_state.height == Height(0) {
previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
}
Header {
version,
previous_block_hash,
merkle_root,
commitment_bytes,
time,
difficulty_threshold,
nonce,
solution,
}
},
)
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}