Zebra/zebra-state/src/service/check/tests/prop.rs

428 lines
16 KiB
Rust

//! Randomised property tests for state contextual validation nullifier: (), in_finalized_state: () nullifier: (), in_finalized_state: () checks.
use std::{convert::TryInto, sync::Arc};
use itertools::Itertools;
use proptest::prelude::*;
use zebra_chain::{
block::{Block, Height},
fmt::TypeNameToDebug,
parameters::Network::*,
primitives::Groth16Proof,
serialization::ZcashDeserializeInto,
sprout::{self, JoinSplit},
transaction::{JoinSplitData, LockTime, Transaction},
};
use crate::{
config::Config, service::StateService, tests::Prepare, FinalizedBlock,
ValidateContextError::DuplicateSproutNullifier,
};
// These tests use the `Arbitrary` trait to easily generate complex types,
// then modify those types to cause an error (or to ensure success).
//
// We could use mainnet or testnet blocks in these tests,
// but the differences shouldn't matter,
// because we're only interested in spend validation,
// (and passing various other state checks).
proptest! {
/// Make sure an arbitrary sprout nullifier is accepted by state contextual validation.
///
/// This test makes sure there are no spurious rejections that might hide bugs in the other tests.
/// (And that the test infrastructure generally works.)
#[test]
fn accept_distinct_arbitrary_sprout_nullifiers(
mut joinsplit in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit_data in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
use_finalized_state in any::<bool>(),
) {
zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(&mut joinsplit.nullifiers);
// make sure there are no other nullifiers
joinsplit_data.first = joinsplit.0;
joinsplit_data.rest = Vec::new();
let transaction = transaction_v4_with_joinsplit_data(joinsplit_data.0);
// convert the coinbase transaction to a version that the non-finalized state will accept
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.push(transaction.into());
let (mut state, _genesis) = new_state_with_mainnet_genesis();
let previous_mem = state.mem.clone();
// randomly choose to commit the block to the finalized or non-finalized state
if use_finalized_state {
let block1 = FinalizedBlock::from(Arc::new(block1));
let commit_result = state.disk.commit_finalized_direct(block1.clone(), "test");
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
prop_assert!(commit_result.is_ok());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
} else {
let block1 = Arc::new(block1).prepare();
let commit_result =
state.validate_and_commit(block1.clone());
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
prop_assert!(!state.mem.eq_internal_state(&previous_mem));
}
}
/// Make sure duplicate sprout nullifiers are rejected by state contextual validation,
/// if they come from the same JoinSplit.
#[test]
fn reject_duplicate_sprout_nullifiers_in_joinsplit(
mut joinsplit in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit_data in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
) {
zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// create a double-spend within the same joinsplit
// this might not actually be valid under the nullifier generation consensus rules
let duplicate_nullifier = joinsplit.nullifiers[0];
joinsplit.nullifiers[1] = duplicate_nullifier;
joinsplit_data.first = joinsplit.0;
joinsplit_data.rest = Vec::new();
let transaction = transaction_v4_with_joinsplit_data(joinsplit_data.0);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.push(transaction.into());
let (mut state, genesis) = new_state_with_mainnet_genesis();
let previous_mem = state.mem.clone();
let block1 = Arc::new(block1).prepare();
let commit_result =
state.validate_and_commit(block1);
// if the random proptest data produces other errors,
// we might need to just check `is_err()` here
prop_assert_eq!(
commit_result,
Err(
DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}.into()
)
);
// block was rejected
prop_assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
}
/// Make sure duplicate sprout nullifiers are rejected by state contextual validation,
/// if they come from different JoinSplits in the same JoinSplitData/Transaction.
#[test]
fn reject_duplicate_sprout_nullifiers_in_transaction(
mut joinsplit1 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit2 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit_data in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
) {
zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(&mut joinsplit1.nullifiers.iter_mut().chain(joinsplit2.nullifiers.iter_mut()));
// create a double-spend across two joinsplits
let duplicate_nullifier = joinsplit1.nullifiers[0];
joinsplit2.nullifiers[0] = duplicate_nullifier;
// make sure there are no other nullifiers
joinsplit_data.first = joinsplit1.0;
joinsplit_data.rest = vec![joinsplit2.0];
let transaction = transaction_v4_with_joinsplit_data(joinsplit_data.0);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.push(transaction.into());
let (mut state, genesis) = new_state_with_mainnet_genesis();
let previous_mem = state.mem.clone();
let block1 = Arc::new(block1).prepare();
let commit_result =
state.validate_and_commit(block1);
prop_assert_eq!(
commit_result,
Err(
DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}.into()
)
);
prop_assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
}
/// Make sure duplicate sprout nullifiers are rejected by state contextual validation,
/// if they come from different transactions in the same block.
#[test]
fn reject_duplicate_sprout_nullifiers_in_block(
mut joinsplit1 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit2 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit_data1 in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
mut joinsplit_data2 in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
) {
zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(&mut joinsplit1.nullifiers.iter_mut().chain(joinsplit2.nullifiers.iter_mut()));
// create a double-spend across two transactions
let duplicate_nullifier = joinsplit1.nullifiers[0];
joinsplit2.nullifiers[0] = duplicate_nullifier;
// make sure there are no other nullifiers
joinsplit_data1.first = joinsplit1.0;
joinsplit_data1.rest = Vec::new();
joinsplit_data2.first = joinsplit2.0;
joinsplit_data2.rest = Vec::new();
let transaction1 = transaction_v4_with_joinsplit_data(joinsplit_data1.0);
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.push(transaction1.into());
block1
.transactions
.push(transaction2.into());
let (mut state, genesis) = new_state_with_mainnet_genesis();
let previous_mem = state.mem.clone();
let block1 = Arc::new(block1).prepare();
let commit_result =
state.validate_and_commit(block1);
prop_assert_eq!(
commit_result,
Err(
DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}.into()
)
);
prop_assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
}
/// Make sure duplicate sprout nullifiers are rejected by state contextual validation,
/// if they come from different blocks in the same chain.
#[test]
fn reject_duplicate_sprout_nullifiers_in_chain(
mut joinsplit1 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit2 in TypeNameToDebug::<JoinSplit::<Groth16Proof>>::arbitrary(),
mut joinsplit_data1 in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
mut joinsplit_data2 in TypeNameToDebug::<JoinSplitData::<Groth16Proof>>::arbitrary(),
duplicate_in_finalized_state in any::<bool>(),
) {
zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let mut block2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(&mut joinsplit1.nullifiers.iter_mut().chain(joinsplit2.nullifiers.iter_mut()));
// create a double-spend across two blocks
let duplicate_nullifier = joinsplit1.nullifiers[0];
joinsplit2.nullifiers[0] = duplicate_nullifier;
// make sure there are no other nullifiers
joinsplit_data1.first = joinsplit1.0;
joinsplit_data1.rest = Vec::new();
joinsplit_data2.first = joinsplit2.0;
joinsplit_data2.rest = Vec::new();
let transaction1 = transaction_v4_with_joinsplit_data(joinsplit_data1.0);
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block2.transactions[0] = transaction_v4_from_coinbase(&block2.transactions[0]).into();
block1
.transactions
.push(transaction1.into());
block2
.transactions
.push(transaction2.into());
let (mut state, _genesis) = new_state_with_mainnet_genesis();
let mut previous_mem = state.mem.clone();
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
let block1 = FinalizedBlock::from(Arc::new(block1));
let commit_result = state.disk.commit_finalized_direct(block1.clone(), "test");
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
prop_assert!(commit_result.is_ok());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
block1_hash = block1.hash;
} else {
let block1 = Arc::new(block1).prepare();
let commit_result =
state.validate_and_commit(block1.clone());
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), state.best_tip());
prop_assert!(!state.mem.eq_internal_state(&previous_mem));
block1_hash = block1.hash;
previous_mem = state.mem.clone();
}
let block2 = Arc::new(block2).prepare();
let commit_result =
state.validate_and_commit(block2);
prop_assert_eq!(
commit_result,
Err(
DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
}.into()
)
);
prop_assert_eq!(Some((Height(1), block1_hash)), state.best_tip());
prop_assert!(state.mem.eq_internal_state(&previous_mem));
}
}
/// Return a new `StateService` containing the mainnet genesis block.
/// Also returns the finalized genesis block itself.
fn new_state_with_mainnet_genesis() -> (StateService, FinalizedBlock) {
let genesis = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block should deserialize");
let mut state = StateService::new(Config::ephemeral(), Mainnet);
assert_eq!(None, state.best_tip());
let genesis = FinalizedBlock::from(genesis);
state
.disk
.commit_finalized_direct(genesis.clone(), "test")
.expect("unexpected invalid genesis block test vector");
assert_eq!(Some((Height(0), genesis.hash)), state.best_tip());
(state, genesis)
}
/// Make sure the supplied nullifiers are distinct, modifying them if necessary.
fn make_distinct_nullifiers<'joinsplit>(
nullifiers: impl IntoIterator<Item = &'joinsplit mut sprout::Nullifier>,
) {
let nullifiers: Vec<_> = nullifiers.into_iter().collect();
if nullifiers.iter().unique().count() < nullifiers.len() {
let mut tweak: u8 = 0x00;
for nullifier in nullifiers {
nullifier.0[0] = tweak;
tweak = tweak
.checked_add(0x01)
.expect("unexpectedly large nullifier list");
}
}
}
/// Return a `Transaction::V4` containing `joinsplit_data`.
///
/// Other fields have empty or default values.
fn transaction_v4_with_joinsplit_data(
joinsplit_data: impl Into<Option<JoinSplitData<Groth16Proof>>>,
) -> Transaction {
let mut joinsplit_data = joinsplit_data.into();
// set value balance to 0 to pass the chain value pool checks
if let Some(ref mut joinsplit_data) = joinsplit_data {
let zero_amount = 0.try_into().expect("unexpected invalid zero amount");
joinsplit_data.first.vpub_old = zero_amount;
joinsplit_data.first.vpub_new = zero_amount;
for mut joinsplit in &mut joinsplit_data.rest {
joinsplit.vpub_old = zero_amount;
joinsplit.vpub_new = zero_amount;
}
}
Transaction::V4 {
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time(),
expiry_height: Height(0),
joinsplit_data,
sapling_shielded_data: None,
}
}
/// Return a `Transaction::V4` with the coinbase data from `coinbase`.
///
/// Used to convert a coinbase transaction to a version that the non-finalized state will accept.
fn transaction_v4_from_coinbase(coinbase: &Transaction) -> Transaction {
assert!(
!coinbase.has_sapling_shielded_data(),
"conversion assumes sapling shielded data is None"
);
Transaction::V4 {
inputs: coinbase.inputs().to_vec(),
outputs: coinbase.outputs().to_vec(),
lock_time: coinbase.lock_time(),
// `Height(0)` means that the expiry height is ignored
expiry_height: coinbase.expiry_height().unwrap_or(Height(0)),
// invalid for coinbase transactions
joinsplit_data: None,
sapling_shielded_data: None,
}
}