Add property test strategies for V5 transactions (#2347)

Add proptest strategies that:
- set the initial block height
- set the transaction version
- make all V5 transaction network upgrade fields valid
This commit is contained in:
teor 2021-06-19 03:40:08 +10:00 committed by GitHub
parent 4d22a0bae9
commit 2396950641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 232 additions and 56 deletions

View File

@ -39,6 +39,17 @@ pub struct LedgerState {
/// To get the network upgrade, use the `network_upgrade` method. /// To get the network upgrade, use the `network_upgrade` method.
network_upgrade_override: Option<NetworkUpgrade>, network_upgrade_override: Option<NetworkUpgrade>,
/// Overrides the previous block hashes in blocks generated by this ledger.
previous_block_hash_override: Option<block::Hash>,
/// Regardless of tip height and network, every transaction is this version.
transaction_version_override: Option<u32>,
/// Every V5 and later transaction has a valid `network_upgrade` field.
///
/// If `false`, some transactions have invalid network upgrades.
transaction_has_valid_network_upgrade: bool,
/// Generate coinbase transactions. /// Generate coinbase transactions.
/// ///
/// In a block or transaction vector, make the first transaction a coinbase /// In a block or transaction vector, make the first transaction a coinbase
@ -47,28 +58,33 @@ pub struct LedgerState {
/// For an individual transaction, make the transaction a coinbase /// For an individual transaction, make the transaction a coinbase
/// transaction. /// transaction.
pub(crate) has_coinbase: bool, pub(crate) has_coinbase: bool,
/// Overrides the previous block hashes in blocks generated by this ledger.
previous_block_hash_override: Option<block::Hash>,
} }
/// Overrides for arbitrary [`LedgerState`]s. /// Overrides for arbitrary [`LedgerState`]s.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct LedgerStateOverride { 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. /// Every chain starts at this block. Single blocks have this height.
pub height_override: Option<Height>, pub height_override: Option<Height>,
/// Every chain starts with a block with this previous block hash. /// Every chain starts with a block with this previous block hash.
/// Single blocks have this previous block hash. /// Single blocks have this previous block hash.
pub previous_block_hash_override: Option<block::Hash>, pub previous_block_hash_override: Option<block::Hash>,
/// Regardless of tip height and network, every block has features from this
/// network upgrade.
pub network_upgrade_override: Option<NetworkUpgrade>,
/// Regardless of tip height and network, every transaction is this version.
pub transaction_version_override: Option<u32>,
/// Every V5 and later transaction has a valid `network_upgrade` field.
///
/// If `false`, some transactions have invalid network upgrades.
pub transaction_has_valid_network_upgrade: bool,
/// Every block has exactly one coinbase transaction.
/// Transactions are always coinbase transactions.
pub always_has_coinbase: bool,
} }
impl LedgerState { impl LedgerState {
@ -81,25 +97,31 @@ impl LedgerState {
/// overrides. /// overrides.
pub fn no_override_strategy() -> BoxedStrategy<Self> { pub fn no_override_strategy() -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride { Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: None,
always_has_coinbase: false,
height_override: None, height_override: None,
previous_block_hash_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,
}) })
} }
/// Returns a strategy for creating `LedgerState`s with features from /// Returns a strategy for creating `LedgerState`s with features from
/// `network_upgrade_override`. /// `network_upgrade_override`.
/// ///
/// These featues ignore the actual tip height and network). /// These featues ignore the actual tip height and network.
pub fn network_upgrade_strategy( pub fn network_upgrade_strategy(
network_upgrade_override: NetworkUpgrade, network_upgrade_override: NetworkUpgrade,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> { ) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride { Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: Some(network_upgrade_override),
always_has_coinbase: false,
height_override: None, height_override: None,
previous_block_hash_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,
}) })
} }
@ -109,12 +131,16 @@ impl LedgerState {
/// Also applies `network_upgrade_override`, if present. /// Also applies `network_upgrade_override`, if present.
pub fn coinbase_strategy( pub fn coinbase_strategy(
network_upgrade_override: impl Into<Option<NetworkUpgrade>>, network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> { ) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride { Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: network_upgrade_override.into(),
always_has_coinbase: true,
height_override: None, height_override: None,
previous_block_hash_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,
}) })
} }
@ -128,12 +154,36 @@ impl LedgerState {
/// Zcash genesis features. /// Zcash genesis features.
pub fn genesis_strategy( pub fn genesis_strategy(
network_upgrade_override: impl Into<Option<NetworkUpgrade>>, network_upgrade_override: impl Into<Option<NetworkUpgrade>>,
transaction_version_override: impl Into<Option<u32>>,
transaction_has_valid_network_upgrade: bool,
) -> BoxedStrategy<Self> { ) -> BoxedStrategy<Self> {
Self::arbitrary_with(LedgerStateOverride { Self::arbitrary_with(LedgerStateOverride {
network_upgrade_override: network_upgrade_override.into(),
always_has_coinbase: true,
height_override: Some(Height(0)), height_override: Some(Height(0)),
previous_block_hash_override: Some(GENESIS_PREVIOUS_BLOCK_HASH), 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,
})
}
/// Returns a strategy for creating `LedgerState`s that start at `height`.
///
/// These strategies also have coinbase transactions, and an optional network
/// upgrade override.
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 {
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,
}) })
} }
@ -148,6 +198,18 @@ impl LedgerState {
NetworkUpgrade::current(self.network, self.height) NetworkUpgrade::current(self.network, self.height)
} }
} }
/// Returns the transaction version override.
pub fn transaction_version_override(&self) -> Option<u32> {
self.transaction_version_override
}
/// Returns `true` if all transactions have valid network upgrade fields.
///
/// If `false`, some transactions have invalid network upgrades.
pub fn transaction_has_valid_network_upgrade(&self) -> bool {
self.transaction_has_valid_network_upgrade
}
} }
impl Default for LedgerState { impl Default for LedgerState {
@ -160,12 +222,15 @@ impl Default for LedgerState {
let most_recent_activation_height = let most_recent_activation_height =
most_recent_nu.activation_height(default_network).unwrap(); most_recent_nu.activation_height(default_network).unwrap();
Self { LedgerState {
height: most_recent_activation_height, height: most_recent_activation_height,
network: default_network, network: default_network,
network_upgrade_override: default_override.network_upgrade_override, 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, 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,
} }
} }
} }
@ -183,10 +248,12 @@ impl Default for LedgerStateOverride {
}; };
LedgerStateOverride { LedgerStateOverride {
network_upgrade_override: nu5_override,
always_has_coinbase: true,
height_override: None, height_override: None,
previous_block_hash_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,
} }
} }
} }
@ -194,12 +261,11 @@ impl Default for LedgerStateOverride {
impl Arbitrary for LedgerState { impl Arbitrary for LedgerState {
type Parameters = LedgerStateOverride; type Parameters = LedgerStateOverride;
/// Generate an arbitrary `LedgerState`. /// Generate an arbitrary [`LedgerState`].
/// ///
/// The default strategy arbitrarily skips some coinbase transactions, and /// The default strategy arbitrarily skips some coinbase transactions, and
/// has an arbitrary start height. To override, use: /// has an arbitrary start height. To override, use a specific [`LegderState`]
/// - [`LedgerState::coinbase_strategy`], or /// strategy method.
/// - [`LedgerState::genesis_strategy`].
fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy { fn arbitrary_with(ledger_override: Self::Parameters) -> Self::Strategy {
( (
any::<Height>(), any::<Height>(),
@ -207,20 +273,21 @@ impl Arbitrary for LedgerState {
any::<bool>(), any::<bool>(),
any::<bool>(), any::<bool>(),
) )
.prop_map(move |(height, network, nu5_override, has_coinbase)| { .prop_map(
// TODO: dynamically select any future network upgrade (#1974) move |(height, network, transaction_has_valid_network_upgrade, has_coinbase)| {
let nu5_override = if nu5_override { Some(Nu5) } else { None }; LedgerState {
height: ledger_override.height_override.unwrap_or(height),
LedgerState { network,
height: ledger_override.height_override.unwrap_or(height), network_upgrade_override: ledger_override.network_upgrade_override,
network, previous_block_hash_override: ledger_override.previous_block_hash_override,
network_upgrade_override: ledger_override transaction_version_override: ledger_override.transaction_version_override,
.network_upgrade_override transaction_has_valid_network_upgrade: ledger_override
.or(nu5_override), .transaction_has_valid_network_upgrade
has_coinbase: ledger_override.always_has_coinbase || has_coinbase, || transaction_has_valid_network_upgrade,
previous_block_hash_override: ledger_override.previous_block_hash_override, has_coinbase: ledger_override.always_has_coinbase || has_coinbase,
} }
}) },
)
.boxed() .boxed()
} }

View File

@ -1,12 +1,13 @@
use std::env; use std::{env, io::ErrorKind};
use std::io::ErrorKind;
use proptest::{arbitrary::any, prelude::*, test_runner::Config}; use proptest::{arbitrary::any, prelude::*, test_runner::Config};
use zebra_test::prelude::*; use zebra_test::prelude::*;
use crate::serialization::{SerializationError, ZcashDeserializeInto, ZcashSerialize};
use crate::{ use crate::{
parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH},
serialization::{SerializationError, ZcashDeserializeInto, ZcashSerialize},
transaction::arbitrary::MAX_ARBITRARY_ITEMS,
LedgerState, LedgerState,
}; };
@ -128,7 +129,8 @@ proptest! {
fn blocks_have_coinbase() -> Result<()> { fn blocks_have_coinbase() -> Result<()> {
zebra_test::init(); zebra_test::init();
let strategy = LedgerState::coinbase_strategy(None).prop_flat_map(Block::arbitrary_with); let strategy =
LedgerState::coinbase_strategy(None, None, false).prop_flat_map(Block::arbitrary_with);
proptest!(|(block in strategy)| { proptest!(|(block in strategy)| {
let has_coinbase = block.coinbase_height().is_some(); let has_coinbase = block.coinbase_height().is_some();
@ -144,7 +146,8 @@ fn blocks_have_coinbase() -> Result<()> {
fn block_genesis_strategy() -> Result<()> { fn block_genesis_strategy() -> Result<()> {
zebra_test::init(); zebra_test::init();
let strategy = LedgerState::genesis_strategy(None).prop_flat_map(Block::arbitrary_with); let strategy =
LedgerState::genesis_strategy(None, None, false).prop_flat_map(Block::arbitrary_with);
proptest!(|(block in strategy)| { proptest!(|(block in strategy)| {
prop_assert_eq!(block.coinbase_height(), Some(Height(0))); prop_assert_eq!(block.coinbase_height(), Some(Height(0)));
@ -160,8 +163,8 @@ fn block_genesis_strategy() -> Result<()> {
fn partial_chain_strategy() -> Result<()> { fn partial_chain_strategy() -> Result<()> {
zebra_test::init(); zebra_test::init();
let strategy = LedgerState::genesis_strategy(None) let strategy = LedgerState::genesis_strategy(None, None, false)
.prop_flat_map(|init| Block::partial_chain_strategy(init, 3)); .prop_flat_map(|init| Block::partial_chain_strategy(init, MAX_ARBITRARY_ITEMS));
proptest!(|(chain in strategy)| { proptest!(|(chain in strategy)| {
let mut height = Height(0); let mut height = Height(0);
@ -176,3 +179,32 @@ fn partial_chain_strategy() -> Result<()> {
Ok(()) Ok(())
} }
/// Make sure our block height strategy generates a chain with the correct coinbase
/// heights and hashes.
#[test]
fn arbitrary_height_partial_chain_strategy() -> Result<()> {
zebra_test::init();
let strategy = any::<Height>()
.prop_flat_map(|height| LedgerState::height_strategy(height, None, None, false))
.prop_flat_map(|init| Block::partial_chain_strategy(init, MAX_ARBITRARY_ITEMS));
proptest!(|(chain in strategy)| {
let mut height = None;
let mut previous_block_hash = None;
for block in chain {
if height.is_none() {
prop_assert!(block.coinbase_height().is_some());
height = block.coinbase_height();
} else {
height = Some(Height(height.unwrap().0 + 1));
prop_assert_eq!(block.coinbase_height(), height);
prop_assert_eq!(Some(block.header.previous_block_hash), previous_block_hash);
}
previous_block_hash = Some(block.hash());
}
});
Ok(())
}

View File

@ -16,7 +16,7 @@ use crate::{
redpallas::{Binding, Signature}, redpallas::{Binding, Signature},
Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof, Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof,
}, },
sapling, sapling::{self, AnchorVariant, PerSpendAnchor, SharedAnchor},
serialization::{ZcashDeserialize, ZcashDeserializeInto}, serialization::{ZcashDeserialize, ZcashDeserializeInto},
sprout, transparent, LedgerState, sprout, transparent, LedgerState,
}; };
@ -24,7 +24,6 @@ use crate::{
use itertools::Itertools; use itertools::Itertools;
use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction}; use super::{FieldNotPresent, JoinSplitData, LockTime, Memo, Transaction};
use sapling::{AnchorVariant, PerSpendAnchor, SharedAnchor};
/// The maximum number of arbitrary transactions, inputs, or outputs. /// The maximum number of arbitrary transactions, inputs, or outputs.
/// ///
@ -130,7 +129,7 @@ impl Transaction {
option::of(any::<orchard::ShieldedData>()), option::of(any::<orchard::ShieldedData>()),
) )
.prop_map( .prop_map(
|( move |(
network_upgrade, network_upgrade,
lock_time, lock_time,
expiry_height, expiry_height,
@ -140,7 +139,11 @@ impl Transaction {
orchard_shielded_data, orchard_shielded_data,
)| { )| {
Transaction::V5 { Transaction::V5 {
network_upgrade, network_upgrade: if ledger_state.transaction_has_valid_network_upgrade() {
ledger_state.network_upgrade()
} else {
network_upgrade
},
lock_time, lock_time,
expiry_height, expiry_height,
inputs, inputs,
@ -393,6 +396,16 @@ impl Arbitrary for Transaction {
type Parameters = LedgerState; type Parameters = LedgerState;
fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy { fn arbitrary_with(ledger_state: Self::Parameters) -> Self::Strategy {
match ledger_state.transaction_version_override() {
Some(1) => return Self::v1_strategy(ledger_state),
Some(2) => return Self::v2_strategy(ledger_state),
Some(3) => return Self::v3_strategy(ledger_state),
Some(4) => return Self::v4_strategy(ledger_state),
Some(5) => return Self::v5_strategy(ledger_state),
Some(_) => unreachable!("invalid transaction version in override"),
None => {}
}
match ledger_state.network_upgrade() { match ledger_state.network_upgrade() {
NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => { NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter => {
Self::v1_strategy(ledger_state) Self::v1_strategy(ledger_state)

View File

@ -1,9 +1,19 @@
//! Randomised property tests for transactions.
use proptest::prelude::*; use proptest::prelude::*;
use std::io::Cursor; use std::io::Cursor;
use zebra_test::prelude::*;
use super::super::*; use super::super::*;
use crate::serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize}; use crate::{
block::Block,
serialization::{ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
transaction::arbitrary::MAX_ARBITRARY_ITEMS,
LedgerState,
};
proptest! { proptest! {
#[test] #[test]
@ -44,3 +54,57 @@ proptest! {
prop_assert_eq![locktime, other_locktime]; prop_assert_eq![locktime, other_locktime];
} }
} }
/// Make sure a transaction version override generates transactions with the specified
/// transaction versions.
#[test]
fn arbitrary_transaction_version_strategy() -> Result<()> {
zebra_test::init();
// Update with new transaction versions as needed
let strategy = (1..5u32)
.prop_flat_map(|transaction_version| {
LedgerState::coinbase_strategy(None, transaction_version, false)
})
.prop_flat_map(|ledger_state| Transaction::vec_strategy(ledger_state, MAX_ARBITRARY_ITEMS));
proptest!(|(transactions in strategy)| {
let mut version = None;
for t in transactions {
if version.is_none() {
version = Some(t.version());
} else {
prop_assert_eq!(Some(t.version()), version);
}
}
});
Ok(())
}
/// Make sure a transaction valid network upgrade strategy generates transactions
/// with valid network upgrades.
#[test]
fn transaction_valid_network_upgrade_strategy() -> Result<()> {
zebra_test::init();
// Update with new transaction versions as needed
let strategy = LedgerState::coinbase_strategy(None, 5, true).prop_flat_map(|ledger_state| {
(
Just(ledger_state.network),
Block::arbitrary_with(ledger_state),
)
});
proptest!(|((network, block) in strategy)| {
// TODO: replace with check_transaction_network_upgrade from #2343
let block_network_upgrade = NetworkUpgrade::current(network, block.coinbase_height().unwrap());
for transaction in block.transactions {
if let Transaction::V5 { network_upgrade, .. } = transaction.as_ref() {
prop_assert_eq!(network_upgrade, &block_network_upgrade);
}
}
});
Ok(())
}

View File

@ -27,7 +27,7 @@ fn coinbase_has_height() -> Result<()> {
fn input_coinbase_vecs_only_have_coinbase_input() -> Result<()> { fn input_coinbase_vecs_only_have_coinbase_input() -> Result<()> {
zebra_test::init(); zebra_test::init();
let strategy = LedgerState::coinbase_strategy(None) let strategy = LedgerState::coinbase_strategy(None, None, false)
.prop_flat_map(|ledger_state| Input::vec_strategy(ledger_state, MAX_ARBITRARY_ITEMS)); .prop_flat_map(|ledger_state| Input::vec_strategy(ledger_state, MAX_ARBITRARY_ITEMS));
proptest!(|(inputs in strategy.prop_map(SummaryDebug))| { proptest!(|(inputs in strategy.prop_map(SummaryDebug))| {

View File

@ -55,7 +55,7 @@ impl Strategy for PreparedChain {
let mut chain = self.chain.lock().unwrap(); let mut chain = self.chain.lock().unwrap();
if chain.is_none() { if chain.is_none() {
// TODO: use the latest network upgrade (#1974) // TODO: use the latest network upgrade (#1974)
let ledger_strategy = LedgerState::genesis_strategy(Nu5); let ledger_strategy = LedgerState::genesis_strategy(Nu5, None, false);
let (network, blocks) = ledger_strategy let (network, blocks) = ledger_strategy
.prop_flat_map(|ledger| { .prop_flat_map(|ledger| {