Update `has_inputs_and_outputs` to check V5 transactions (#2229)

* Fix documentation comment

Was missing a slash to become documentation.

* Add documentation link to type reference

Just to help navigation a bit.

* Implement `Transaction::orchard_actions()` getter

Returns an iterator to iterator over the actions in the Orchard shielded
data (if there is one, otherwise it returns an empty iterator).

* Add V5 support for `has_inputs_and_outputs`

Checks if the transaction has Orchard actions. If it does, it is
considered to have inputs and outputs.

* Refactor transaction test vectors

Make it easier to reuse the fake V5 transaction converter in other test
vectors.

* Move helper function to `zebra-chain` crate

Place it together with some other helper functions, including the one
that actually creates the fake V5 transaction.

* Test transaction with no inputs

`check::has_inputs_and_outputs` should return an error indicating that
the transaction has no inputs.

* Test transaction with no outputs

`check::has_inputs_and_outputs` should return an error indicating that
the transaction has no outputs.

* Note that transaction is fake in `expect` message

Should make the message easier to find, and also gives emphasis to the
fact that the transaction is a fake conversion to V5.

Co-authored-by: teor <teor@riseup.net>

Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Janito Vaqueiro Ferreira Filho 2021-06-01 22:32:52 -03:00 committed by GitHub
parent 1685611592
commit db0cdb74ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 115 additions and 54 deletions

View File

@ -112,7 +112,7 @@ pub enum Transaction {
outputs: Vec<transparent::Output>,
/// The sapling shielded data for this transaction, if any.
sapling_shielded_data: Option<sapling::ShieldedData<sapling::SharedAnchor>>,
// The orchard data for this transaction, if any.
/// The orchard data for this transaction, if any.
orchard_shielded_data: Option<orchard::ShieldedData>,
},
}
@ -395,7 +395,28 @@ impl Transaction {
// orchard
/// Access the orchard::Nullifiers in this transaction, regardless of version.
/// Iterate over the [`orchard::Action`]s in this transaction, if there are any.
pub fn orchard_actions(&self) -> Box<dyn Iterator<Item = &orchard::Action> + '_> {
match self {
// Actions
Transaction::V5 {
orchard_shielded_data: Some(orchard_shielded_data),
..
} => Box::new(orchard_shielded_data.actions()),
// No Actions
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. }
| Transaction::V5 {
orchard_shielded_data: None,
..
} => Box::new(std::iter::empty()),
}
}
/// Access the [`orchard::Nullifier`]s in this transaction, regardless of version.
pub fn orchard_nullifiers(&self) -> Box<dyn Iterator<Item = &orchard::Nullifier> + '_> {
// This function returns a boxed iterator because the different
// transaction variants can have different iterator types

View File

@ -11,7 +11,9 @@ use crate::{
redpallas::{Binding, Signature},
Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof,
},
sapling, sprout, transparent, LedgerState,
sapling,
serialization::ZcashDeserializeInto,
sprout, transparent, LedgerState,
};
use itertools::Itertools;
@ -531,3 +533,26 @@ fn sapling_spend_v4_to_fake_v5(
spend_auth_sig: v4_spend.spend_auth_sig,
}
}
/// Generate an iterator over fake V5 transactions.
///
/// These transactions are converted from non-V5 transactions that exist in the provided network
/// blocks.
pub fn fake_v5_transactions_for_network<'b>(
network: Network,
blocks: impl DoubleEndedIterator<Item = (&'b u32, &'b &'static [u8])> + 'b,
) -> impl DoubleEndedIterator<Item = Transaction> + 'b {
blocks.flat_map(move |(height, original_bytes)| {
let original_block = original_bytes
.zcash_deserialize_into::<block::Block>()
.expect("block is structurally valid");
original_block
.transactions
.into_iter()
.map(move |transaction| {
transaction_to_fake_v5(&*transaction, network, block::Height(*height))
})
.map(Transaction::from)
})
}

View File

@ -28,15 +28,11 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError>
let n_joinsplit = tx.joinsplit_count();
let n_spends_sapling = tx.sapling_spends_per_anchor().count();
let n_outputs_sapling = tx.sapling_outputs().count();
let n_actions_orchard = tx.orchard_actions().count();
// TODO: Orchard validation (#1980)
// For `Transaction::V5`:
// * at least one of `tx_in_count`, `nSpendsSapling`, and `nActionsOrchard` MUST be non-zero.
// * at least one of `tx_out_count`, `nOutputsSapling`, and `nActionsOrchard` MUST be non-zero.
if tx_in_count + n_spends_sapling + n_joinsplit == 0 {
if tx_in_count + n_spends_sapling + n_joinsplit + n_actions_orchard == 0 {
Err(TransactionError::NoInputs)
} else if tx_out_count + n_outputs_sapling + n_joinsplit == 0 {
} else if tx_out_count + n_outputs_sapling + n_joinsplit + n_actions_orchard == 0 {
Err(TransactionError::NoOutputs)
} else {
Ok(())

View File

@ -1,59 +1,32 @@
use zebra_chain::{
block::{Block, Height},
parameters::Network,
serialization::ZcashDeserializeInto,
transaction::{arbitrary::transaction_to_fake_v5, Transaction},
transaction::{arbitrary::fake_v5_transactions_for_network, Transaction},
};
use crate::error::TransactionError::*;
use super::check;
use crate::error::TransactionError;
use color_eyre::eyre::Report;
#[test]
fn v5_fake_transactions() -> Result<(), Report> {
zebra_test::init();
v5_fake_transactions_for_network(Network::Mainnet)?;
v5_fake_transactions_for_network(Network::Testnet)?;
let networks = vec![
(Network::Mainnet, zebra_test::vectors::MAINNET_BLOCKS.iter()),
(Network::Testnet, zebra_test::vectors::TESTNET_BLOCKS.iter()),
];
Ok(())
}
fn v5_fake_transactions_for_network(network: Network) -> Result<(), Report> {
zebra_test::init();
// get all the blocks we have available
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};
for (height, original_bytes) in block_iter {
let original_block = original_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
// convert all transactions from the block to V5
let transactions: Vec<Transaction> = original_block
.transactions
.iter()
.map(AsRef::as_ref)
.map(|t| transaction_to_fake_v5(t, network, Height(*height)))
.map(Into::into)
.collect();
// after the conversion some transactions end up with no inputs nor outputs.
for transaction in transactions {
match super::check::has_inputs_and_outputs(&transaction) {
Err(e) => {
if e != NoInputs && e != NoOutputs {
panic!("error must be NoInputs or NoOutputs")
}
}
for (network, blocks) in networks {
for transaction in fake_v5_transactions_for_network(network, blocks) {
match check::has_inputs_and_outputs(&transaction) {
Ok(()) => (),
Err(TransactionError::NoInputs) | Err(TransactionError::NoOutputs) => (),
Err(_) => panic!("error must be NoInputs or NoOutputs"),
};
// make sure there are no joinsplits nor spends in coinbase
super::check::coinbase_tx_no_prevout_joinsplit_spend(&transaction)?;
check::coinbase_tx_no_prevout_joinsplit_spend(&transaction)?;
// validate the sapling shielded data
match transaction {
@ -62,13 +35,13 @@ fn v5_fake_transactions_for_network(network: Network) -> Result<(), Report> {
..
} => {
if let Some(s) = sapling_shielded_data {
super::check::sapling_balances_match(&s)?;
check::sapling_balances_match(&s)?;
for spend in s.spends_per_anchor() {
super::check::spend_cv_rk_not_small_order(&spend)?
check::spend_cv_rk_not_small_order(&spend)?
}
for output in s.outputs() {
super::check::output_cv_epk_not_small_order(&output)?;
check::output_cv_epk_not_small_order(&output)?;
}
}
}
@ -76,5 +49,51 @@ fn v5_fake_transactions_for_network(network: Network) -> Result<(), Report> {
}
}
}
Ok(())
}
#[test]
fn v5_transaction_with_no_inputs_fails_validation() {
let transaction = fake_v5_transactions_for_network(
Network::Mainnet,
zebra_test::vectors::MAINNET_BLOCKS.iter(),
)
.rev()
.find(|transaction| {
transaction.inputs().is_empty()
&& transaction.sapling_spends_per_anchor().next().is_none()
&& transaction.orchard_actions().next().is_none()
&& transaction.joinsplit_count() == 0
&& (!transaction.outputs().is_empty() || transaction.sapling_outputs().next().is_some())
})
.expect("At least one fake v5 transaction with no inputs in the test vectors");
assert_eq!(
check::has_inputs_and_outputs(&transaction),
Err(TransactionError::NoInputs)
);
}
#[test]
fn v5_transaction_with_no_outputs_fails_validation() {
let transaction = fake_v5_transactions_for_network(
Network::Mainnet,
zebra_test::vectors::MAINNET_BLOCKS.iter(),
)
.rev()
.find(|transaction| {
transaction.outputs().is_empty()
&& transaction.sapling_outputs().next().is_none()
&& transaction.orchard_actions().next().is_none()
&& transaction.joinsplit_count() == 0
&& (!transaction.inputs().is_empty()
|| transaction.sapling_spends_per_anchor().next().is_some())
})
.expect("At least one fake v5 transaction with no outputs in the test vectors");
assert_eq!(
check::has_inputs_and_outputs(&transaction),
Err(TransactionError::NoOutputs)
);
}