Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate test chains that pass basic chain consistency tests #2221

Merged
merged 3 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 176 additions & 72 deletions zebra-chain/src/block/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use proptest::{
use std::sync::Arc;

use crate::{
block,
parameters::{Network, NetworkUpgrade, GENESIS_PREVIOUS_BLOCK_HASH},
serialization,
work::{difficulty::CompactDifficulty, equihash},
Expand All @@ -17,21 +18,21 @@ use super::*;
#[non_exhaustive]
/// The configuration data for proptest when generating arbitrary chains
pub struct LedgerState {
/// The tip height of the block or start of the chain.
/// The height of the generated block, or the start height of the generated chain.
///
/// To get the network upgrade, use the `network_upgrade` method.
///
/// If `network_upgrade_override` is not set, the network upgrade is derived
/// from the height and network.
pub tip_height: Height,
/// from the `height` and `network`.
pub height: Height,

/// The network to generate fake blocks for.
pub network: Network,

/// Overrides the network upgrade calculated from `tip_height` and `network`.
/// Overrides the network upgrade calculated from `height` and `network`.
///
/// To get the network upgrade, use the `network_upgrade` method.
pub network_upgrade_override: Option<NetworkUpgrade>,
network_upgrade_override: Option<NetworkUpgrade>,

/// Generate coinbase transactions.
///
Expand All @@ -42,94 +43,181 @@ pub struct LedgerState {
/// transaction.
pub(crate) has_coinbase: bool,

/// Should this block have a genesis (all-zeroes) previous block hash?
///
/// In Zebra's proptests, the previous block hash can be overriden with
/// genesis at any block height.
genesis_previous_block_hash_override: bool,
/// Overrides the previous block hashes in blocks generated by this ledger.
previous_block_hash_override: Option<block::Hash>,
}

/// Overrides for arbitrary [`LedgerState`]s.
#[derive(Debug, Clone, Copy)]
pub struct LedgerStateOverride {
/// Regardless of tip height and network, every block has features from this
/// network upgrade.
pub network_upgrade_override: Option<NetworkUpgrade>,

/// Every block has exactly one coinbase transaction.
/// Transactions are always coinbase transactions.
pub always_has_coinbase: bool,

/// Every chain starts at this block. Single blocks have this height.
pub height_override: Option<Height>,

/// Every chain starts with a block with this previous block hash.
/// Single blocks have this previous block hash.
pub previous_block_hash_override: Option<block::Hash>,
}
Comment on lines +50 to 67
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change makes it easier to generate different kinds of chains.


impl LedgerState {
/// Returns the default strategy for creating arbitrary `LedgerState`s.
pub fn default_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride::default())
}

/// Returns a strategy for creating arbitrary `LedgerState`s, without any
/// overrides.
pub fn no_override_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: None,
always_has_coinbase: false,
height_override: None,
previous_block_hash_override: None,
})
}

/// Returns a strategy for creating `LedgerState`s with features from
/// `network_upgrade_override`.
///
/// These featues ignore the actual tip height and network).
pub fn network_upgrade_strategy(
network_upgrade_override: NetworkUpgrade,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: Some(network_upgrade_override),
always_has_coinbase: false,
height_override: None,
previous_block_hash_override: None,
})
}

/// Returns a strategy for creating `LedgerState`s that always have coinbase
/// transactions.
///
/// Also applies `network_upgrade_override`, if present.
pub fn coinbase_strategy(
network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: network_upgrade_override.into(),
always_has_coinbase: true,
height_override: None,
previous_block_hash_override: None,
})
}

/// Returns a strategy for creating `LedgerState`s that start with a genesis
/// block.
///
/// These strategies also have coinbase transactions, and an optional network
/// upgrade override.
///
/// Use the `Genesis` network upgrade to get a random genesis block, with
/// Zcash genesis features.
pub fn genesis_strategy(
network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: network_upgrade_override.into(),
always_has_coinbase: true,
height_override: Some(Height(0)),
previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH),
})
}

/// Returns the network upgrade for this ledger state.
///
/// If `network_upgrade_override` is set, it replaces the upgrade calculated
/// using `tip_height` and `network`.
/// using `height` and `network`.
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.tip_height)
NetworkUpgrade::current(self.network, self.height)
}
}
}

/// Should this block have a genesis (all-zeroes) previous block hash?
///
/// In Zebra's proptests, the previous block hash can be overriden with
/// genesis at any block height.
pub fn use_genesis_previous_block_hash(&self) -> bool {
self.tip_height == Height(0) || self.genesis_previous_block_hash_override
}
impl Default for LedgerState {
fn default() -> Self {
// TODO: stop having a default network
let default_network = Network::default();
let default_override = LedgerStateOverride::default();

/// Returns a strategy for creating `LedgerState`s that always have coinbase
/// transactions.
pub fn coinbase_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(true)
let most_recent_nu = NetworkUpgrade::current(default_network, Height::MAX);
let most_recent_activation_height =
most_recent_nu.activation_height(default_network).unwrap();

Self {
height: most_recent_activation_height,
network: default_network,
network_upgrade_override: default_override.network_upgrade_override,
has_coinbase: default_override.always_has_coinbase,
previous_block_hash_override: default_override.previous_block_hash_override,
}
}
}

impl Default for LedgerState {
impl Default for LedgerStateOverride {
fn default() -> Self {
let network = Network::Mainnet;
let most_recent_nu = NetworkUpgrade::current(network, Height::MAX);
let most_recent_activation_height = most_recent_nu.activation_height(network).unwrap();
let default_network = Network::default();

// TODO: dynamically select any future network upgrade (#1974)
let nu5_activation_height = NetworkUpgrade::Nu5.activation_height(network);
let nu5_activation_height = NetworkUpgrade::Nu5.activation_height(default_network);
let nu5_override = if nu5_activation_height.is_some() {
None
} else {
Some(NetworkUpgrade::Nu5)
};

Self {
tip_height: most_recent_activation_height,
network,
LedgerStateOverride {
network_upgrade_override: nu5_override,
has_coinbase: true,
// start each chain with a genesis previous block hash, regardless of height
genesis_previous_block_hash_override: true,
always_has_coinbase: true,
height_override: None,
previous_block_hash_override: None,
}
}
}

impl Arbitrary for LedgerState {
type Parameters = bool;
type Parameters = LedgerStateOverride;

/// Generate an arbitrary `LedgerState`.
///
/// The default strategy arbitrarily skips some coinbase transactions. To
/// override, use `LedgerState::coinbase_strategy`.
fn arbitrary_with(require_coinbase: Self::Parameters) -> Self::Strategy {
/// The default strategy arbitrarily skips some coinbase transactions, and
/// has an arbitrary start height. To override, use:
/// - [`LedgerState::coinbase_strategy`], or
/// - [`LedgerState::genesis_strategy`].
fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
(
any::<Height>(),
any::<Network>(),
any::<bool>(),
any::<bool>(),
)
.prop_map(move |(tip_height, network, nu5_override, has_coinbase)| {
.prop_map(move |(height, network, nu5_override, has_coinbase)| {
// TODO: dynamically select any future network upgrade (#1974)
let network_upgrade_override = if nu5_override {
let nu5_override = if nu5_override {
Some(NetworkUpgrade::Nu5)
} else {
None
};

LedgerState {
tip_height,
height: ledger_override.height_override.unwrap_or(height),
network,
network_upgrade_override,
has_coinbase: require_coinbase || has_coinbase,
genesis_previous_block_hash_override: true,
network_upgrade_override: ledger_override
.network_upgrade_override
.or(nu5_override),
has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
previous_block_hash_override: ledger_override.previous_block_hash_override,
}
})
.boxed()
Expand All @@ -144,15 +232,10 @@ impl Arbitrary for Block {
fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
let transactions_strategy = Transaction::vec_strategy(ledger_state, 2);

(any::<Header>(), transactions_strategy)
.prop_map(move |(mut header, transactions)| {
if ledger_state.genesis_previous_block_hash_override {
header.previous_block_hash = GENESIS_PREVIOUS_BLOCK_HASH;
}
Self {
header,
transactions,
}
(Header::arbitrary_with(ledger_state), transactions_strategy)
.prop_map(move |(header, transactions)| Self {
header,
transactions,
})
.boxed()
}
Expand All @@ -164,18 +247,29 @@ impl Block {
/// Returns a strategy for creating Vecs of blocks with increasing height of
/// the given length.
pub fn partial_chain_strategy(
init: LedgerState,
mut current: LedgerState,
count: usize,
) -> BoxedStrategy<Vec<Arc<Self>>> {
let mut current = init;
let mut vec = Vec::with_capacity(count);

// generate block strategies with the correct heights
for _ in 0..count {
vec.push(Block::arbitrary_with(current).prop_map(Arc::new));
current.tip_height.0 += 1;
current.genesis_previous_block_hash_override = false;
vec.push(Block::arbitrary_with(current));
current.height.0 += 1;
}

vec.boxed()
// after the vec strategy generates blocks, update the previous block hashes
vec.prop_map(|mut vec| {
let mut previous_block_hash = None;
for block in vec.iter_mut() {
if let Some(previous_block_hash) = previous_block_hash {
block.header.previous_block_hash = previous_block_hash;
}
previous_block_hash = Some(block.hash());
}
vec.into_iter().map(Arc::new).collect()
})
.boxed()
Comment on lines +261 to +272
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes up the previous block hashes

}
}

Expand Down Expand Up @@ -203,9 +297,9 @@ impl Arbitrary for Commitment {
}

impl Arbitrary for Header {
type Parameters = ();
type Parameters = LedgerState;

fn arbitrary_with(_args: ()) -> Self::Strategy {
fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
(
// version is interpreted as i32 in the spec, so we are limited to i32::MAX here
(4u32..(i32::MAX as u32)),
Expand All @@ -218,24 +312,34 @@ impl Arbitrary for Header {
any::<equihash::Solution>(),
)
.prop_map(
|(
move |(
version,
previous_block_hash,
merkle_root,
commitment_bytes,
time,
difficulty_threshold,
nonce,
solution,
)| Header {
version,
previous_block_hash,
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;
}
Comment on lines +325 to +331
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code move makes header generation consistent with block generation


Header {
version,
previous_block_hash,
merkle_root,
commitment_bytes,
time,
difficulty_threshold,
nonce,
solution,
}
},
)
.boxed()
Expand Down
4 changes: 2 additions & 2 deletions zebra-chain/src/block/tests/preallocate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ proptest! {
/// Confirm that each counted header takes at least COUNTED_HEADER_LEN bytes when serialized.
/// This verifies that our calculated [`TrustedPreallocate::max_allocation`] is indeed an upper bound.
#[test]
fn counted_header_min_length(header in Header::arbitrary_with(()), transaction_count in (0..MAX_BLOCK_BYTES)) {
fn counted_header_min_length(header in any::<Header>(), transaction_count in (0..MAX_BLOCK_BYTES)) {
let header = CountedHeader {
header,
transaction_count: transaction_count.try_into().expect("Must run test on platform with at least 32 bit address space"),
Expand All @@ -68,7 +68,7 @@ proptest! {
/// 1. The smallest disallowed vector of `CountedHeaders`s is too large to send via the Zcash Wire Protocol
/// 2. The largest allowed vector is small enough to fit in a legal Zcash Wire Protocol message
#[test]
fn counted_header_max_allocation(header in Header::arbitrary_with(())) {
fn counted_header_max_allocation(header in any::<Header>()) {
let header = CountedHeader {
header,
transaction_count: 0,
Expand Down
Loading