zebra/zebra-state/src/service/check/tests/nullifier.rs

1156 lines
47 KiB
Rust

//! Randomised property tests for nullifier contextual validation
use std::{env, sync::Arc};
use itertools::Itertools;
use proptest::prelude::*;
use zebra_chain::{
block::{Block, Height},
fmt::TypeNameToDebug,
orchard,
parameters::NetworkUpgrade::Nu5,
primitives::Groth16Proof,
sapling::{self, FieldNotPresent, PerSpendAnchor, TransferData::*},
serialization::ZcashDeserializeInto,
sprout::JoinSplit,
transaction::{JoinSplitData, LockTime, Transaction},
};
use crate::{
arbitrary::Prepare,
service::{
check::nullifier::tx_no_duplicates_in_chain, read, write::validate_and_commit_non_finalized,
},
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
CheckpointVerifiedBlock,
ValidateContextError::{
DuplicateOrchardNullifier, DuplicateSaplingNullifier, 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).
const DEFAULT_NULLIFIER_PROPTEST_CASES: u32 = 2;
proptest! {
#![proptest_config(
proptest::test_runner::Config::with_cases(env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_NULLIFIER_PROPTEST_CASES))
)]
// sprout
/// 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_in_one_block(
mut joinsplit in TypeNameToDebug::<JoinSplit<Groth16Proof>>::arbitrary(),
joinsplit_data in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
use_finalized_state in any::<bool>(),
) {
let _init_guard = 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);
let expected_nullifiers = joinsplit.nullifiers;
let transaction = transaction_v4_with_joinsplit_data(joinsplit_data.0, [joinsplit.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 finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
// randomly choose to commit the block to the finalized or non-finalized state
if use_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), None, "test");
// the block was committed
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
// the non-finalized state didn't change
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
// the finalized state has the nullifiers
prop_assert!(finalized_state
.contains_sprout_nullifier(&expected_nullifiers[0]));
prop_assert!(finalized_state
.contains_sprout_nullifier(&expected_nullifiers[1]));
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
// the block was committed
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
// the block data is in the non-finalized state
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
// the non-finalized state has the nullifiers
prop_assert_eq!(non_finalized_state.chain_count(), 1);
prop_assert!(non_finalized_state
.best_contains_sprout_nullifier(&expected_nullifiers[0]));
prop_assert!(non_finalized_state
.best_contains_sprout_nullifier(&expected_nullifiers[1]));
}
}
/// 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(),
joinsplit_data in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
) {
let _init_guard = 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;
let transaction = transaction_v4_with_joinsplit_data(joinsplit_data.0, [joinsplit.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(transaction.into());
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
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)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.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(),
joinsplit_data in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(
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;
let transaction =
transaction_v4_with_joinsplit_data(joinsplit_data.0, [joinsplit1.0, joinsplit2.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(transaction.into());
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.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(),
joinsplit_data1 in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
joinsplit_data2 in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
make_distinct_nullifiers(
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;
let transaction1 = transaction_v4_with_joinsplit_data(joinsplit_data1.0, [joinsplit1.0]);
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0, [joinsplit2.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.extend([transaction1.into(), transaction2.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.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(),
joinsplit_data1 in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
joinsplit_data2 in TypeNameToDebug::<JoinSplitData<Groth16Proof>>::arbitrary(),
duplicate_in_finalized_state in any::<bool>(),
) {
let _init_guard = 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(
joinsplit1
.nullifiers
.iter_mut()
.chain(joinsplit2.nullifiers.iter_mut()),
);
let expected_nullifiers = joinsplit1.nullifiers;
// create a double-spend across two blocks
let duplicate_nullifier = joinsplit1.nullifiers[0];
joinsplit2.nullifiers[0] = duplicate_nullifier;
let transaction1 = Arc::new(transaction_v4_with_joinsplit_data(joinsplit_data1.0, [joinsplit1.0]));
let transaction2 = transaction_v4_with_joinsplit_data(joinsplit_data2.0, [joinsplit2.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.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
finalized_state.populate_with_anchors(&block2);
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), None, "test");
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(finalized_state
.contains_sprout_nullifier(&expected_nullifiers[0]));
prop_assert!(finalized_state
.contains_sprout_nullifier(&expected_nullifiers[1]));
block1_hash = block1.hash;
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(non_finalized_state
.best_contains_sprout_nullifier(&expected_nullifiers[0]));
prop_assert!(non_finalized_state
.best_contains_sprout_nullifier(&expected_nullifiers[1]));
block1_hash = block1.hash;
previous_mem = non_finalized_state.clone();
}
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2
);
prop_assert_eq!(
commit_result,
Err(DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateSproutNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
// sapling
/// Make sure an arbitrary sapling 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_sapling_nullifiers_in_one_block(
spend in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
use_finalized_state in any::<bool>(),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let expected_nullifier = spend.nullifier;
let transaction =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data.0, [spend.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 finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
// randomly choose to commit the block to the finalized or non-finalized state
if use_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(),None, "test");
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(finalized_state.contains_sapling_nullifier(&expected_nullifier));
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(non_finalized_state
.best_contains_sapling_nullifier(&expected_nullifier));
}
}
/// Make sure duplicate sapling nullifiers are rejected by state contextual validation,
/// if they come from different Spends in the same sapling::ShieldedData/Transaction.
#[test]
fn reject_duplicate_sapling_nullifiers_in_transaction(
spend1 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
mut spend2 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
) {
let _init_guard = 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 across two spends
let duplicate_nullifier = spend1.nullifier;
spend2.nullifier = duplicate_nullifier;
let transaction = transaction_v4_with_sapling_shielded_data(
sapling_shielded_data.0,
[spend1.0, spend2.0],
);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(transaction.into());
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateSaplingNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
/// Make sure duplicate sapling nullifiers are rejected by state contextual validation,
/// if they come from different transactions in the same block.
#[test]
fn reject_duplicate_sapling_nullifiers_in_block(
spend1 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
mut spend2 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data1 in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data2 in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
) {
let _init_guard = 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 across two transactions
let duplicate_nullifier = spend1.nullifier;
spend2.nullifier = duplicate_nullifier;
let transaction1 =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data1.0, [spend1.0]);
let transaction2 =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data2.0, [spend2.0]);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.extend([transaction1.into(), transaction2.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateSaplingNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
/// Make sure duplicate sapling nullifiers are rejected by state contextual validation,
/// if they come from different blocks in the same chain.
#[test]
fn reject_duplicate_sapling_nullifiers_in_chain(
spend1 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
mut spend2 in TypeNameToDebug::<sapling::Spend<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data1 in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
sapling_shielded_data2 in TypeNameToDebug::<sapling::ShieldedData<PerSpendAnchor>>::arbitrary(),
duplicate_in_finalized_state in any::<bool>(),
) {
let _init_guard = 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");
// create a double-spend across two blocks
let duplicate_nullifier = spend1.nullifier;
spend2.nullifier = duplicate_nullifier;
let transaction1 =
Arc::new(transaction_v4_with_sapling_shielded_data(sapling_shielded_data1.0, [spend1.0]));
let transaction2 =
transaction_v4_with_sapling_shielded_data(sapling_shielded_data2.0, [spend2.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.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
finalized_state.populate_with_anchors(&block2);
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(),None, "test");
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(finalized_state.contains_sapling_nullifier(&duplicate_nullifier));
block1_hash = block1.hash;
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(non_finalized_state
.best_contains_sapling_nullifier(&duplicate_nullifier));
block1_hash = block1.hash;
previous_mem = non_finalized_state.clone();
}
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2
);
prop_assert_eq!(
commit_result,
Err(DuplicateSaplingNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateSaplingNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
// orchard
/// Make sure an arbitrary orchard 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_orchard_nullifiers_in_one_block(
authorized_action in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
orchard_shielded_data in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
use_finalized_state in any::<bool>(),
) {
let _init_guard = zebra_test::init();
let mut block1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let expected_nullifier = authorized_action.action.nullifier;
let transaction = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data.0,
[authorized_action.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 finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
// randomly choose to commit the block to the finalized or non-finalized state
if use_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), None, "test");
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(finalized_state.contains_orchard_nullifier(&expected_nullifier));
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(non_finalized_state
.best_contains_orchard_nullifier(&expected_nullifier));
}
}
/// Make sure duplicate orchard nullifiers are rejected by state contextual validation,
/// if they come from different AuthorizedActions in the same orchard::ShieldedData/Transaction.
#[test]
fn reject_duplicate_orchard_nullifiers_in_transaction(
authorized_action1 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
mut authorized_action2 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
orchard_shielded_data in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
) {
let _init_guard = 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 across two authorized_actions
let duplicate_nullifier = authorized_action1.action.nullifier;
authorized_action2.action.nullifier = duplicate_nullifier;
let transaction = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data.0,
[authorized_action1.0, authorized_action2.0],
);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1.transactions.push(transaction.into());
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateOrchardNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
/// Make sure duplicate orchard nullifiers are rejected by state contextual validation,
/// if they come from different transactions in the same block.
#[test]
fn reject_duplicate_orchard_nullifiers_in_block(
authorized_action1 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
mut authorized_action2 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
orchard_shielded_data1 in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
orchard_shielded_data2 in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
) {
let _init_guard = 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 across two transactions
let duplicate_nullifier = authorized_action1.action.nullifier;
authorized_action2.action.nullifier = duplicate_nullifier;
let transaction1 = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data1.0,
[authorized_action1.0],
);
let transaction2 = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data2.0,
[authorized_action2.0],
);
block1.transactions[0] = transaction_v4_from_coinbase(&block1.transactions[0]).into();
block1
.transactions
.extend([transaction1.into(), transaction2.into()]);
let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
let previous_mem = non_finalized_state.clone();
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1
);
prop_assert_eq!(
commit_result,
Err(DuplicateOrchardNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: false,
}
.into())
);
prop_assert_eq!(Some((Height(0), genesis.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
/// Make sure duplicate orchard nullifiers are rejected by state contextual validation,
/// if they come from different blocks in the same chain.
#[test]
fn reject_duplicate_orchard_nullifiers_in_chain(
authorized_action1 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
mut authorized_action2 in TypeNameToDebug::<orchard::AuthorizedAction>::arbitrary(),
orchard_shielded_data1 in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
orchard_shielded_data2 in TypeNameToDebug::<orchard::ShieldedData>::arbitrary(),
duplicate_in_finalized_state in any::<bool>(),
) {
let _init_guard = 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");
// create a double-spend across two blocks
let duplicate_nullifier = authorized_action1.action.nullifier;
authorized_action2.action.nullifier = duplicate_nullifier;
let transaction1 = Arc::new(transaction_v5_with_orchard_shielded_data(
orchard_shielded_data1.0,
[authorized_action1.0],
));
let transaction2 = transaction_v5_with_orchard_shielded_data(
orchard_shielded_data2.0,
[authorized_action2.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.clone());
block2.transactions.push(transaction2.into());
let (mut finalized_state, mut non_finalized_state, _genesis) = new_state_with_mainnet_genesis();
// Allows anchor checks to pass
finalized_state.populate_with_anchors(&block1);
finalized_state.populate_with_anchors(&block2);
let mut previous_mem = non_finalized_state.clone();
// makes sure there are no spurious rejections that might hide bugs in `tx_no_duplicates_in_chain`
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert!(check_tx_no_duplicates_in_chain.is_ok());
let block1_hash;
// randomly choose to commit the next block to the finalized or non-finalized state
if duplicate_in_finalized_state {
let block1 = CheckpointVerifiedBlock::from(Arc::new(block1));
let commit_result = finalized_state.commit_finalized_direct(block1.clone().into(), None, "test");
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(commit_result.is_ok());
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(finalized_state.contains_orchard_nullifier(&duplicate_nullifier));
block1_hash = block1.hash;
} else {
let block1 = Arc::new(block1).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block1.clone()
);
prop_assert_eq!(commit_result, Ok(()));
prop_assert_eq!(Some((Height(1), block1.hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(!non_finalized_state.eq_internal_state(&previous_mem));
prop_assert!(non_finalized_state
.best_contains_orchard_nullifier(&duplicate_nullifier));
block1_hash = block1.hash;
previous_mem = non_finalized_state.clone();
}
let block2 = Arc::new(block2).prepare();
let commit_result = validate_and_commit_non_finalized(
&finalized_state.db,
&mut non_finalized_state,
block2
);
prop_assert_eq!(
commit_result,
Err(DuplicateOrchardNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
}
.into())
);
let check_tx_no_duplicates_in_chain =
tx_no_duplicates_in_chain(&finalized_state.db, non_finalized_state.best_chain(), &transaction1);
prop_assert_eq!(
check_tx_no_duplicates_in_chain,
Err(DuplicateOrchardNullifier {
nullifier: duplicate_nullifier,
in_finalized_state: duplicate_in_finalized_state,
})
);
prop_assert_eq!(Some((Height(1), block1_hash)), read::best_tip(&non_finalized_state, &finalized_state.db));
prop_assert!(non_finalized_state.eq_internal_state(&previous_mem));
}
}
/// Make sure the supplied nullifiers are distinct, modifying them if necessary.
fn make_distinct_nullifiers<'until_modified, NullifierT>(
nullifiers: impl IntoIterator<Item = &'until_modified mut NullifierT>,
) where
NullifierT: Into<[u8; 32]> + Clone + Eq + std::hash::Hash + 'until_modified,
[u8; 32]: Into<NullifierT>,
{
let nullifiers: Vec<_> = nullifiers.into_iter().collect();
if nullifiers.iter().unique().count() < nullifiers.len() {
let mut tweak: u8 = 0x00;
for nullifier in nullifiers {
let mut nullifier_bytes: [u8; 32] = nullifier.clone().into();
nullifier_bytes[0] = tweak;
*nullifier = nullifier_bytes.into();
tweak = tweak
.checked_add(0x01)
.expect("unexpectedly large nullifier list");
}
}
}
/// Return a `Transaction::V4` containing `joinsplit_data`,
/// with its `JoinSplit`s replaced by `joinsplits`.
///
/// Other fields have empty or default values.
///
/// # Panics
///
/// If there are no `JoinSplit`s in `joinsplits`.
fn transaction_v4_with_joinsplit_data(
joinsplit_data: impl Into<Option<JoinSplitData<Groth16Proof>>>,
joinsplits: impl IntoIterator<Item = JoinSplit<Groth16Proof>>,
) -> Transaction {
let mut joinsplit_data = joinsplit_data.into();
let joinsplits: Vec<_> = joinsplits.into_iter().collect();
if let Some(ref mut joinsplit_data) = joinsplit_data {
// make sure there are no other nullifiers, by replacing all the joinsplits
let (first, rest) = joinsplits
.split_first()
.expect("unexpected empty joinsplits");
joinsplit_data.first = first.clone();
joinsplit_data.rest = rest.to_vec();
// set value balance to 0 to pass the chain value pool checks
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 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_timestamp(),
expiry_height: Height(0),
joinsplit_data,
sapling_shielded_data: None,
}
}
/// Return a `Transaction::V4` containing `sapling_shielded_data`.
/// with its `Spend`s replaced by `spends`.
///
/// Other fields have empty or default values.
///
/// Note: since sapling nullifiers in V5 transactions are identical to V4 transactions,
/// we just use V4 transactions in the tests.
///
/// # Panics
///
/// If there are no `Spend`s in `spends`, and no `Output`s in `sapling_shielded_data`.
fn transaction_v4_with_sapling_shielded_data(
sapling_shielded_data: impl Into<Option<sapling::ShieldedData<PerSpendAnchor>>>,
spends: impl IntoIterator<Item = sapling::Spend<PerSpendAnchor>>,
) -> Transaction {
let mut sapling_shielded_data = sapling_shielded_data.into();
let spends: Vec<_> = spends.into_iter().collect();
if let Some(ref mut sapling_shielded_data) = sapling_shielded_data {
// make sure there are no other nullifiers, by replacing all the spends
sapling_shielded_data.transfers = match (
sapling_shielded_data.transfers.clone(),
spends.try_into().ok(),
) {
// old and new spends: replace spends
(
SpendsAndMaybeOutputs {
shared_anchor,
maybe_outputs,
..
},
Some(spends),
) => SpendsAndMaybeOutputs {
shared_anchor,
spends,
maybe_outputs,
},
// old spends, but no new spends: delete spends, panic if no outputs
(SpendsAndMaybeOutputs { maybe_outputs, .. }, None) => JustOutputs {
outputs: maybe_outputs.try_into().expect(
"unexpected invalid TransferData: must have at least one spend or one output",
),
},
// no old spends, but new spends: add spends
(JustOutputs { outputs, .. }, Some(spends)) => SpendsAndMaybeOutputs {
shared_anchor: FieldNotPresent,
spends,
maybe_outputs: outputs.into(),
},
// no old and no new spends: do nothing
(just_outputs @ JustOutputs { .. }, None) => just_outputs,
};
// set value balance to 0 to pass the chain value pool checks
let zero_amount = 0.try_into().expect("unexpected invalid zero amount");
sapling_shielded_data.value_balance = zero_amount;
}
Transaction::V4 {
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time_timestamp(),
expiry_height: Height(0),
joinsplit_data: None,
sapling_shielded_data,
}
}
/// Return a `Transaction::V5` containing `orchard_shielded_data`.
/// with its `AuthorizedAction`s replaced by `authorized_actions`.
///
/// Other fields have empty or default values.
///
/// # Panics
///
/// If there are no `AuthorizedAction`s in `authorized_actions`.
fn transaction_v5_with_orchard_shielded_data(
orchard_shielded_data: impl Into<Option<orchard::ShieldedData>>,
authorized_actions: impl IntoIterator<Item = orchard::AuthorizedAction>,
) -> Transaction {
let mut orchard_shielded_data = orchard_shielded_data.into();
let authorized_actions: Vec<_> = authorized_actions.into_iter().collect();
if let Some(ref mut orchard_shielded_data) = orchard_shielded_data {
// make sure there are no other nullifiers, by replacing all the authorized_actions
orchard_shielded_data.actions = authorized_actions.try_into().expect(
"unexpected invalid orchard::ShieldedData: must have at least one AuthorizedAction",
);
// set value balance to 0 to pass the chain value pool checks
let zero_amount = 0.try_into().expect("unexpected invalid zero amount");
orchard_shielded_data.value_balance = zero_amount;
}
Transaction::V5 {
network_upgrade: Nu5,
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time_timestamp(),
expiry_height: Height(0),
sapling_shielded_data: None,
orchard_shielded_data,
}
}