use ::orchard::bundle::{BundleVersion, Flags};
use arbitrary::v5_transactions;
use chrono::DateTime;
use color_eyre::eyre::Result;
use lazy_static::lazy_static;
use rand::seq::IteratorRandom;
use zcash_protocol::{consensus::BranchId, value::ZatBalance};
use std::sync::Arc;
use crate::{
amount::MAX_MONEY,
block::{Block, Height, MAX_BLOCK_BYTES},
orchard,
parameters::Network,
primitives::{x25519, zcash_primitives::PrecomputedTxData, Groth16Proof},
serialization::{
SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashDeserializeWithContext,
ZcashSerialize,
},
sprout,
transaction::{
arbitrary::shielded::{fake_orchard_bundle, fake_v6_transaction},
sighash::SigHasher,
},
transparent::Script,
value_balance::ValueBalanceError,
};
use zebra_test::{
vectors::{ZIP143_1, ZIP143_2, ZIP243_1, ZIP243_2, ZIP243_3},
zip0143, zip0243, zip0244,
};
use super::super::*;
lazy_static! {
pub static ref EMPTY_V5_TX: Transaction = Transaction::test_v5(
NetworkUpgrade::Nu5,
Vec::new(),
Vec::new(),
LockTime::min_lock_time_timestamp(),
block::Height(0),
);
}
fn mock_pre_v5_output_list(output: transparent::Output, index: usize) -> Vec<transparent::Output> {
std::iter::repeat_n(output, index + 1).collect()
}
#[test]
fn transactionhash_struct_from_str_roundtrip() {
let _init_guard = zebra_test::init();
let hash: Hash = "3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"
.parse()
.unwrap();
assert_eq!(
format!("{hash:?}"),
r#"transaction::Hash("3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf")"#
);
assert_eq!(
hash.to_string(),
"3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"
);
}
#[test]
fn auth_digest_struct_from_str_roundtrip() {
let _init_guard = zebra_test::init();
let digest: AuthDigest = "3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"
.parse()
.unwrap();
assert_eq!(
format!("{digest:?}"),
r#"AuthDigest("3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf")"#
);
assert_eq!(
digest.to_string(),
"3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"
);
}
#[test]
fn wtx_id_struct_from_str_roundtrip() {
let _init_guard = zebra_test::init();
let wtx_id: WtxId = "3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf0000000000000000000000000000000000000000000000000000000000000001"
.parse()
.unwrap();
assert_eq!(
format!("{wtx_id:?}"),
r#"WtxId { id: transaction::Hash("3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf"), auth_digest: AuthDigest("0000000000000000000000000000000000000000000000000000000000000001") }"#
);
assert_eq!(
wtx_id.to_string(),
"3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf0000000000000000000000000000000000000000000000000000000000000001"
);
}
#[test]
fn librustzcash_tx_deserialize_and_round_trip() {
let _init_guard = zebra_test::init();
let tx = Transaction::zcash_deserialize(&zebra_test::vectors::GENERIC_TESTNET_TX[..])
.expect("transaction test vector from librustzcash should deserialize");
let mut data2 = Vec::new();
tx.zcash_serialize(&mut data2).expect("tx should serialize");
assert_eq!(&zebra_test::vectors::GENERIC_TESTNET_TX[..], &data2[..]);
}
#[test]
fn librustzcash_tx_hash() {
let _init_guard = zebra_test::init();
let tx = Transaction::zcash_deserialize(&zebra_test::vectors::GENERIC_TESTNET_TX[..])
.expect("transaction test vector from librustzcash should deserialize");
let hash = tx.hash();
let expected = "64f0bd7fe30ce23753358fe3a2dc835b8fba9c0274c4e2c54a6f73114cb55639"
.parse::<Hash>()
.expect("hash should parse correctly");
assert_eq!(hash, expected);
}
#[test]
fn doesnt_deserialize_transaction_with_invalid_value_balance() {
let _init_guard = zebra_test::init();
let dummy_transaction =
Transaction::test_v4(vec![], vec![], LockTime::Height(Height(1)), Height(10));
let mut input_bytes = Vec::new();
dummy_transaction
.zcash_serialize(&mut input_bytes)
.expect("dummy transaction should serialize");
input_bytes[18] = 1;
let result = Transaction::zcash_deserialize(&input_bytes[..]);
assert!(matches!(
result,
Err(SerializationError::BadTransactionBalance)
));
}
#[test]
fn v4_value_balance_check_locates_the_field_with_joinsplits() {
let _init_guard = zebra_test::init();
let block: Block = zebra_test::vectors::BLOCK_MAINNET_419201_BYTES
.zcash_deserialize_into()
.expect("hard-coded test vector must deserialize");
let tx = block
.transactions
.iter()
.find(|tx| {
tx.tx_version() == TxVersion::V4
&& tx.sprout_joinsplit_descriptions().next().is_some()
&& tx.sapling_bundle().is_none()
})
.expect("block 419,201 has a V4 JoinSplit transaction with no Sapling bundle");
let mut bytes = tx
.zcash_serialize_to_vec()
.expect("a mined transaction must serialize");
Transaction::zcash_deserialize(&bytes[..])
.expect("an unmodified mined transaction must deserialize");
let joinsplit_count = tx.sprout_joinsplit_descriptions().count();
const V4_JOINSPLIT_SIZE: usize = (2 * 8) + (9 * 32) + 192 + (2 * 601);
let sprout_size = 1 + joinsplit_count * V4_JOINSPLIT_SIZE + 32 + 64;
let counts_start = bytes.len() - sprout_size - 2;
assert_eq!(
&bytes[counts_start..counts_start + 2],
&[0x00, 0x00],
"the Sapling spend and output counts must be zero",
);
let value_balance_start = counts_start - 8;
bytes[value_balance_start] = 1;
assert!(
matches!(
Transaction::zcash_deserialize(&bytes[..]),
Err(SerializationError::BadTransactionBalance)
),
"a non-zero valueBalanceSapling with no Sapling bundle must be rejected",
);
}
#[test]
fn v4_transactions_in_one_block_all_round_trip() {
let _init_guard = zebra_test::init();
let block: Block = zebra_test::vectors::BLOCK_MAINNET_419201_BYTES
.zcash_deserialize_into()
.expect("hard-coded test vector must deserialize");
let v4_count = block
.transactions
.iter()
.filter(|tx| tx.tx_version() == TxVersion::V4)
.count();
assert!(
v4_count > 1,
"this test needs a block with several V4 transactions, found {v4_count}",
);
for tx in &block.transactions {
let bytes = tx.zcash_serialize_to_vec().expect("transaction serializes");
let round_tripped =
Transaction::zcash_deserialize(&bytes[..]).expect("transaction round-trips");
assert_eq!(tx.hash(), round_tripped.hash());
}
let block_bytes = block.zcash_serialize_to_vec().expect("block serializes");
let round_tripped: Block = block_bytes
.zcash_deserialize_into()
.expect("block round-trips");
assert_eq!(block.hash(), round_tripped.hash());
}
#[test]
fn serde_serialize_labels_v6_transactions_correctly() {
use crate::transaction::arbitrary::{fake_bundle_for_branch, fake_v6_transaction};
let _init_guard = zebra_test::init();
let ironwood = fake_bundle_for_branch(
zcash_protocol::consensus::BranchId::Nu6_3,
::orchard::ValuePool::Ironwood,
1,
3,
)
.expect("the Ironwood pool is defined at NU6.3");
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, None, Some(ironwood));
assert_eq!(tx.version(), 6);
let rendered = format!("{:?}", serde_json::to_value(&tx).expect("V6 tx serializes"));
assert!(
rendered.contains("V6"),
"a V6 transaction must serialize under its own variant name, got: {rendered}",
);
assert!(
rendered.contains("ironwood_shielded_data"),
"a V6 transaction must serialize its Ironwood bundle, got: {rendered}",
);
let v5 = Transaction::test_v5(
NetworkUpgrade::Nu5,
Vec::new(),
Vec::new(),
LockTime::unlocked(),
block::Height(0),
);
let rendered_v5 = format!("{:?}", serde_json::to_value(&v5).expect("V5 tx serializes"));
assert!(rendered_v5.contains("V5"));
assert!(!rendered_v5.contains("ironwood_shielded_data"));
}
#[test]
fn sighasher_preserves_the_ironwood_bundle() {
use crate::transaction::arbitrary::{fake_bundle_for_branch, fake_v6_transaction};
let _init_guard = zebra_test::init();
let ironwood = fake_bundle_for_branch(
zcash_protocol::consensus::BranchId::Nu6_3,
::orchard::ValuePool::Ironwood,
2,
21,
)
.expect("the Ironwood pool is defined at NU6.3");
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, None, Some(ironwood));
assert!(tx.has_ironwood_shielded_data());
let sighasher = tx
.sighasher(NetworkUpgrade::Nu6_3, std::sync::Arc::new(Vec::new()))
.expect("a v6 transaction at NU6.3 has a valid branch ID");
let bundle = sighasher
.ironwood_bundle()
.expect("the sighasher must expose the Ironwood bundle, or its proof is never verified");
assert_eq!(bundle.actions().len(), 2);
assert!(sighasher.orchard_bundle().is_none());
}
#[test]
fn ironwood_only_transaction_has_shielded_inputs_and_outputs() {
use crate::transaction::arbitrary::{fake_bundle_for_branch, fake_v6_transaction};
let _init_guard = zebra_test::init();
let ironwood = fake_bundle_for_branch(
zcash_protocol::consensus::BranchId::Nu6_3,
::orchard::ValuePool::Ironwood,
2,
7,
)
.expect("the Ironwood pool is defined at NU6.3");
let flags = *ironwood.flags();
assert!(
flags.spends_enabled() && flags.outputs_enabled(),
"this test is only meaningful when the bundle enables both spends and outputs",
);
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, None, Some(ironwood));
assert!(tx.inputs().is_empty());
assert!(tx.outputs().is_empty());
assert!(tx.orchard_bundle().is_none());
assert!(
tx.has_shielded_inputs(),
"Ironwood spends are shielded inputs"
);
assert!(
tx.has_shielded_outputs(),
"Ironwood outputs are shielded outputs"
);
assert!(tx.has_transparent_or_shielded_inputs());
assert!(tx.has_transparent_or_shielded_outputs());
}
#[test]
fn zip143_deserialize_and_round_trip() {
let _init_guard = zebra_test::init();
let tx1 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP143_1[..])
.expect("transaction test vector from ZIP143 should deserialize");
let mut data1 = Vec::new();
tx1.zcash_serialize(&mut data1)
.expect("tx should serialize");
assert_eq!(&zebra_test::vectors::ZIP143_1[..], &data1[..]);
let tx2 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP143_2[..])
.expect("transaction test vector from ZIP143 should deserialize");
let mut data2 = Vec::new();
tx2.zcash_serialize(&mut data2)
.expect("tx should serialize");
assert_eq!(&zebra_test::vectors::ZIP143_2[..], &data2[..]);
}
#[test]
fn zip243_deserialize_and_round_trip() {
let _init_guard = zebra_test::init();
let tx1 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP243_1[..])
.expect("transaction test vector from ZIP243 should deserialize");
let mut data1 = Vec::new();
tx1.zcash_serialize(&mut data1)
.expect("tx should serialize");
assert_eq!(&zebra_test::vectors::ZIP243_1[..], &data1[..]);
let tx2 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP243_2[..])
.expect("transaction test vector from ZIP243 should deserialize");
let mut data2 = Vec::new();
tx2.zcash_serialize(&mut data2)
.expect("tx should serialize");
assert_eq!(&zebra_test::vectors::ZIP243_2[..], &data2[..]);
let tx3 = Transaction::zcash_deserialize(&zebra_test::vectors::ZIP243_3[..])
.expect("transaction test vector from ZIP243 should deserialize");
let mut data3 = Vec::new();
tx3.zcash_serialize(&mut data3)
.expect("tx should serialize");
assert_eq!(&zebra_test::vectors::ZIP243_3[..], &data3[..]);
}
#[test]
fn deserialize_large_transaction() {
let _init_guard = zebra_test::init();
let input =
transparent::Input::zcash_deserialize(&zebra_test::vectors::DUMMY_INPUT1[..]).unwrap();
let output =
transparent::Output::zcash_deserialize(&zebra_test::vectors::DUMMY_OUTPUT1[..]).unwrap();
let mut input_data = Vec::new();
input
.zcash_serialize(&mut input_data)
.expect("input should serialize");
let tx_inputs_num = MAX_BLOCK_BYTES as usize / input_data.len();
let inputs = std::iter::repeat_n(input, tx_inputs_num).collect::<Vec<_>>();
let oversized_tx = Transaction::test_v1(
inputs,
vec![output],
LockTime::Time(DateTime::from_timestamp(61, 0).unwrap()),
);
let mut tx_data = Vec::new();
oversized_tx
.zcash_serialize(&mut tx_data)
.expect("transaction should serialize");
assert!(tx_data.len() > MAX_BLOCK_BYTES as usize);
Transaction::zcash_deserialize(&tx_data[..])
.expect_err("transaction should not deserialize due to its size");
}
#[test]
fn empty_v5_round_trip() {
let _init_guard = zebra_test::init();
let tx: &Transaction = &EMPTY_V5_TX;
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2: &Transaction = &data
.zcash_deserialize_into()
.expect("tx should deserialize");
assert_eq!(tx, tx2);
let data2 = tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(data, data2, "data must be equal if structs are equal");
}
#[test]
fn empty_v4_round_trip() {
let _init_guard = zebra_test::init();
let tx = Transaction::test_v4(
Vec::new(),
Vec::new(),
LockTime::min_lock_time_timestamp(),
block::Height(0),
);
let data = tx.zcash_serialize_to_vec().expect("tx should serialize");
let tx2 = data
.zcash_deserialize_into()
.expect("tx should deserialize");
assert_eq!(tx, tx2);
let data2 = tx2
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(data, data2, "data must be equal if structs are equal");
}
#[test]
fn empty_v5_librustzcash_round_trip() {
let _init_guard = zebra_test::init();
let tx: &Transaction = &EMPTY_V5_TX;
let _nu = tx.network_upgrade().expect("network upgrade");
let bytes = tx
.zcash_serialize_to_vec()
.expect("empty V5 transaction serializes");
let _tx2: Transaction = bytes
.zcash_deserialize_into()
.expect("empty V5 transaction deserializes");
}
#[test]
fn invalid_orchard_nullifier() {
let _init_guard = zebra_test::init();
use std::convert::TryFrom;
let invalid_nullifier_bytes = [
62, 157, 27, 63, 100, 228, 1, 82, 140, 16, 238, 78, 68, 19, 221, 184, 189, 207, 230, 95,
194, 216, 165, 24, 110, 221, 139, 195, 106, 98, 192, 71,
];
assert_eq!(
orchard::Nullifier::try_from(invalid_nullifier_bytes)
.err()
.unwrap()
.to_string(),
SerializationError::Parse("Invalid pallas::Base value for orchard Nullifier").to_string()
);
}
#[test]
fn fake_v5_librustzcash_round_trip() {
let _init_guard = zebra_test::init();
for network in Network::iter() {
fake_v5_librustzcash_round_trip_for_network(network);
}
}
fn fake_v5_librustzcash_round_trip_for_network(network: Network) {
let block_iter = network.block_iter();
let overwinter_activation_height = NetworkUpgrade::Overwinter
.activation_height(&network)
.expect("a valid height")
.0;
let nu5_activation_height = NetworkUpgrade::Nu5
.activation_height(&network)
.unwrap_or(Height::MAX_EXPIRY_HEIGHT)
.0;
let blocks_after_overwinter_and_before_nu5 = block_iter
.skip_while(|(height, _)| **height < overwinter_activation_height)
.take_while(|(height, _)| **height < nu5_activation_height);
for (height, original_bytes) in blocks_after_overwinter_and_before_nu5 {
let original_block = original_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
let mut fake_block = original_block.clone();
fake_block.transactions = fake_block
.transactions
.iter()
.map(AsRef::as_ref)
.map(|t| arbitrary::transaction_to_fake_v5(t, &network, Height(*height)))
.map(Into::into)
.collect();
for (original_tx, fake_tx) in original_block
.transactions
.iter()
.zip(fake_block.transactions.iter())
{
assert_ne!(
&original_tx, &fake_tx,
"v1-v4 transactions must change when converted to fake v5"
);
let fake_bytes = fake_tx
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_ne!(
&original_bytes[..],
fake_bytes,
"v1-v4 transaction data must change when converted to fake v5"
);
}
}
}
#[test]
fn zip244_round_trip() -> Result<()> {
let _init_guard = zebra_test::init();
for test in zip0244::TEST_VECTORS.iter() {
let tx = test.tx.zcash_deserialize_into::<Transaction>()?;
let reencoded = tx.zcash_serialize_to_vec()?;
assert_eq!(test.tx, reencoded);
}
Ok(())
}
#[test]
fn zip244_txid() -> Result<()> {
let _init_guard = zebra_test::init();
for test in zip0244::TEST_VECTORS.iter() {
let tx: Transaction = test.tx.zcash_deserialize_into()?;
let txid = tx.hash();
assert_eq!(txid.0, test.txid);
}
Ok(())
}
#[test]
fn zip244_auth_digest() -> Result<()> {
let _init_guard = zebra_test::init();
for test in zip0244::TEST_VECTORS.iter() {
let transaction = test.tx.zcash_deserialize_into::<Transaction>()?;
let auth_digest = transaction.auth_digest();
assert_eq!(
auth_digest
.expect("must have auth_digest since it must be a V5 transaction")
.0,
test.auth_digest
);
}
Ok(())
}
#[test]
fn test_vec143_1() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP143_1.zcash_deserialize_into::<Transaction>()?;
let hasher = SigHasher::new(
&transaction,
NetworkUpgrade::Overwinter,
Arc::new(Vec::new()),
)
.expect("network upgrade is valid for tx");
let hash = hasher.sighash(HashType::ALL, None);
let expected = "a1f1a4e5cd9bd522322d661edd2af1bf2a7019cfab94ece18f4ba935b0a19073";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec143_2() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP143_2.zcash_deserialize_into::<Transaction>()?;
let value = hex::decode("2f6e04963b4c0100")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&hex::decode("53")?);
let input_ind = 1;
let output = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let all_previous_outputs = mock_pre_v5_output_list(output, input_ind);
let hasher = SigHasher::new(
&transaction,
NetworkUpgrade::Overwinter,
Arc::new(all_previous_outputs),
)
.expect("network upgrade is valid for tx");
let hash = hasher.sighash(
HashType::SINGLE,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "23652e76cb13b85a0e3363bb5fca061fa791c40c533eccee899364e6e60bb4f7";
let result: &[u8] = hash.as_ref();
let result = hex::encode(result);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec243_1() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP243_1.zcash_deserialize_into::<Transaction>()?;
let hasher = SigHasher::new(&transaction, NetworkUpgrade::Sapling, Arc::new(Vec::new()))
.expect("network upgrade is valid for tx");
let hash = hasher.sighash(HashType::ALL, None);
let expected = "63d18534de5f2d1c9e169b73f9c783718adbef5c8a7d55b5e7a37affa1dd3ff3";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
let precomputed_tx_data =
PrecomputedTxData::new(&transaction, NetworkUpgrade::Sapling, Arc::new(Vec::new()))
.expect("network upgrade is valid for tx");
let alt_sighash =
crate::primitives::zcash_primitives::sighash(&precomputed_tx_data, HashType::ALL, None);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec243_2() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP243_2.zcash_deserialize_into::<Transaction>()?;
let value = hex::decode("adedf02996510200")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&[]);
let input_ind = 1;
let output = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let all_previous_outputs = mock_pre_v5_output_list(output, input_ind);
let hasher = SigHasher::new(
&transaction,
NetworkUpgrade::Sapling,
Arc::new(all_previous_outputs),
)
.expect("network upgrade is valid for tx");
let hash = hasher.sighash(
HashType::NONE,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "bbe6d84f57c56b29b914c694baaccb891297e961de3eb46c68e3c89c47b1a1db";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
let lock_script = Script::new(&[]);
let prevout = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let index = input_ind;
let all_previous_outputs = mock_pre_v5_output_list(prevout, input_ind);
let precomputed_tx_data = PrecomputedTxData::new(
&transaction,
NetworkUpgrade::Sapling,
Arc::new(all_previous_outputs),
)
.expect("network upgrade is valid for tx");
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&precomputed_tx_data,
HashType::NONE,
Some((index, lock_script.as_raw_bytes().to_vec())),
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn test_vec243_3() -> Result<()> {
let _init_guard = zebra_test::init();
let transaction = ZIP243_3.zcash_deserialize_into::<Transaction>()?;
let value = hex::decode("80f0fa0200000000")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&hex::decode(
"76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac",
)?);
let input_ind = 0;
let all_previous_outputs = vec![transparent::Output {
value,
lock_script: lock_script.clone(),
}];
let hasher = SigHasher::new(
&transaction,
NetworkUpgrade::Sapling,
Arc::new(all_previous_outputs),
)
.expect("network upgrade is valid for tx");
let hash = hasher.sighash(
HashType::ALL,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "f3148f80dfab5e573d5edfe7a850f5fd39234f80b5429d3a57edcc11e34c585b";
let result = hex::encode(hash);
let span = tracing::span!(
tracing::Level::ERROR,
"compare_final",
expected.len = expected.len(),
buf.len = result.len()
);
let _guard = span.enter();
assert_eq!(expected, result);
let lock_script = Script::new(&hex::decode(
"76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac",
)?);
let prevout = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let index = input_ind;
let all_previous_outputs = vec![prevout];
let precomputed_tx_data = PrecomputedTxData::new(
&transaction,
NetworkUpgrade::Sapling,
Arc::new(all_previous_outputs),
)
.expect("network upgrade is valid for tx");
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&precomputed_tx_data,
HashType::ALL,
Some((index, lock_script.as_raw_bytes().to_vec())),
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn zip143_sighash() -> Result<()> {
let _init_guard = zebra_test::init();
for (i, test) in zip0143::TEST_VECTORS.iter().enumerate() {
let transaction = test.tx.zcash_deserialize_into::<Transaction>()?;
let (input_index, output) = match test.transparent_input {
Some(transparent_input) => (
Some(transparent_input as usize),
Some(transparent::Output {
value: test.amount.try_into()?,
lock_script: transparent::Script::new(test.script_code.as_ref()),
}),
),
None => (None, None),
};
let all_previous_outputs: Vec<_> = match output.clone() {
Some(output) => mock_pre_v5_output_list(output, input_index.unwrap()),
None => vec![],
};
let result = hex::encode(
transaction
.sighash(
NetworkUpgrade::try_from(test.consensus_branch_id).expect("network upgrade"),
HashType::from_bits(test.hash_type).expect("must be a valid HashType"),
Arc::new(all_previous_outputs),
input_index.map(|input_index| {
(
input_index,
output.unwrap().lock_script.as_raw_bytes().to_vec(),
)
}),
)
.expect("network upgrade is valid for tx"),
);
let expected = hex::encode(test.sighash);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
Ok(())
}
#[test]
fn zip243_sighash() -> Result<()> {
let _init_guard = zebra_test::init();
for (i, test) in zip0243::TEST_VECTORS.iter().enumerate() {
let transaction = test.tx.zcash_deserialize_into::<Transaction>()?;
let (input_index, output) = match test.transparent_input {
Some(transparent_input) => (
Some(transparent_input as usize),
Some(transparent::Output {
value: test.amount.try_into()?,
lock_script: transparent::Script::new(test.script_code.as_ref()),
}),
),
None => (None, None),
};
let all_previous_outputs: Vec<_> = match output.clone() {
Some(output) => mock_pre_v5_output_list(output, input_index.unwrap()),
None => vec![],
};
let result = hex::encode(
transaction
.sighash(
NetworkUpgrade::try_from(test.consensus_branch_id).expect("network upgrade"),
HashType::from_bits(test.hash_type).expect("must be a valid HashType"),
Arc::new(all_previous_outputs),
input_index.map(|input_index| {
(
input_index,
output.unwrap().lock_script.as_raw_bytes().to_vec(),
)
}),
)
.expect("network upgrade is valid for tx"),
);
let expected = hex::encode(test.sighash);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
Ok(())
}
#[test]
fn zip244_sighash() -> Result<()> {
let _init_guard = zebra_test::init();
for (i, test) in zip0244::TEST_VECTORS.iter().enumerate() {
let transaction = test.tx.zcash_deserialize_into::<Transaction>()?;
let all_previous_outputs: Arc<Vec<_>> = Arc::new(
test.amounts
.iter()
.zip(test.script_pubkeys.iter())
.map(|(amount, script_pubkey)| transparent::Output {
value: (*amount).try_into().unwrap(),
lock_script: transparent::Script::new(script_pubkey.as_ref()),
})
.collect(),
);
let result = hex::encode(
transaction
.sighash(
NetworkUpgrade::Nu5,
HashType::ALL,
all_previous_outputs.clone(),
None,
)
.expect("network upgrade is valid for tx"),
);
let expected = hex::encode(test.sighash_shielded);
assert_eq!(expected, result, "test #{i}: sighash does not match");
if let Some(sighash_all) = test.sighash_all {
let result = hex::encode(
transaction
.sighash(
NetworkUpgrade::Nu5,
HashType::ALL,
all_previous_outputs,
test.transparent_input
.map(|idx| (idx as _, test.script_pubkeys[idx as usize].clone())),
)
.expect("network upgrade is valid for tx"),
);
let expected = hex::encode(sighash_all);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
}
Ok(())
}
#[test]
fn orchard_proof_size_is_canonical() {
let mut checked = 0;
for net in Network::iter() {
for tx in v5_transactions(net.block_iter()) {
let Some(bundle) = tx.orchard_bundle() else {
continue;
};
assert!(
tx.orchard_proof_size_is_canonical(),
"a real Orchard proof should be canonically sized"
);
let proof_len = bundle.authorization().proof().as_ref().len();
assert_ne!(
proof_len + 1,
crate::orchard::shielded_data::expected_proof_size(bundle.actions().len()),
"a padded Orchard proof must not be considered canonical"
);
checked += 1;
}
}
assert!(
checked > 0,
"expected at least one Orchard transaction in the test vectors"
);
}
#[test]
fn consensus_branch_id() {
for net in Network::iter() {
for tx in v5_transactions(net.block_iter()).filter(|tx| {
!tx.has_transparent_inputs() && tx.has_shielded_data() && tx.network_upgrade().is_some()
}) {
let tx_nu = tx
.network_upgrade()
.expect("this test shouldn't use txs without a network upgrade");
let any_other_nu = NetworkUpgrade::iter()
.filter(|&nu| nu != tx_nu)
.choose(&mut rand::rng())
.expect("there must be a network upgrade other than the tx one");
PrecomputedTxData::new(&tx, tx_nu, Arc::new(Vec::new()))
.expect("network upgrade is valid for tx");
sighash::SigHasher::new(&tx, tx_nu, Arc::new(Vec::new()))
.expect("network upgrade is valid for tx");
tx.sighash(tx_nu, HashType::ALL, Arc::new(Vec::new()), None)
.expect("network upgrade is valid for tx");
let err = PrecomputedTxData::new(&tx, any_other_nu, Arc::new(Vec::new())).unwrap_err();
assert!(
matches!(err, crate::Error::InvalidConsensusBranchId),
"precomputing tx sighash data errors under nu other than the tx one"
);
let err = sighash::SigHasher::new(&tx, any_other_nu, Arc::new(Vec::new())).unwrap_err();
assert!(
matches!(err, crate::Error::InvalidConsensusBranchId),
"creating the sighasher errors under nu other than the tx one"
);
let err = tx
.sighash(any_other_nu, HashType::ALL, Arc::new(Vec::new()), None)
.unwrap_err();
assert!(
matches!(err, crate::Error::InvalidConsensusBranchId),
"the sighash computation errors under nu other than the tx one"
);
}
}
}
#[test]
fn binding_signatures() {
let _init_guard = zebra_test::init();
for net in Network::iter() {
let sapling_activation_height = NetworkUpgrade::Sapling
.activation_height(&net)
.expect("a valid height")
.0;
let mut at_least_one_v4_checked = false;
let mut at_least_one_v5_checked = false;
for (height, block) in net
.block_iter()
.skip_while(|(height, _)| **height < sapling_activation_height)
{
let nu = NetworkUpgrade::current(&net, Height(*height));
for tx in block
.zcash_deserialize_into::<Block>()
.expect("a valid block")
.transactions
{
if let Some(bundle) = tx.inner().sapling_bundle() {
let version = tx.version();
if version >= 5 && tx.has_transparent_inputs() {
continue;
}
let sighash = tx
.sighash(nu, HashType::ALL, Arc::new(Vec::new()), None)
.expect("network upgrade is valid for tx");
let cv_spends: sapling_crypto::value::CommitmentSum =
bundle.shielded_spends().iter().map(|s| s.cv()).sum();
let cv_outputs: sapling_crypto::value::CommitmentSum =
bundle.shielded_outputs().iter().map(|o| o.cv()).sum();
let bvk = (cv_spends - cv_outputs).into_bvk(i64::from(bundle.value_balance()));
bvk.verify(sighash.as_ref(), &bundle.authorization().binding_sig)
.expect("must pass binding signature verification");
if version == 4 {
at_least_one_v4_checked = true;
} else {
at_least_one_v5_checked = true;
}
}
}
}
assert!(at_least_one_v4_checked);
assert!(at_least_one_v5_checked);
}
}
#[test]
fn v6_ironwood_txid_and_roundtrip() {
let _init_guard = zebra_test::init();
let tx = Transaction::test_v6(
NetworkUpgrade::Nu6_3,
Vec::new(),
Vec::new(),
LockTime::min_lock_time_timestamp(),
block::Height(0),
);
let txid = tx.hash();
let bytes = tx
.zcash_serialize_to_vec()
.expect("v6 transaction serializes");
let tx2: Transaction = bytes
.zcash_deserialize_into()
.expect("v6 transaction deserializes");
assert_eq!(tx, tx2);
assert_eq!(tx2.hash(), txid, "txid is stable across serialization");
}
#[test]
fn v6_transaction_rejects_pre_nu6_3_branch_ids() {
let _test_guard = zebra_test::init();
let orchard = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
ZatBalance::from_i64(0).expect("zero is a valid balance"),
1,
1,
BundleVersion::orchard_v3(),
);
let transaction = fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), None);
let mut transaction_bytes = transaction
.zcash_serialize_to_vec()
.expect("the NU6.3 fixture has a serializable V6 Orchard bundle");
let deserialized_transaction = Transaction::zcash_deserialize(&transaction_bytes[..])
.expect("the unmodified NU6.3 fixture parses");
assert_eq!(deserialized_transaction, transaction);
for branch_id in [
BranchId::Nu5,
BranchId::Nu6,
BranchId::Nu6_1,
BranchId::Nu6_2,
] {
transaction_bytes[8..12].copy_from_slice(&u32::from(branch_id).to_le_bytes());
let result_without_context = Transaction::zcash_deserialize(&transaction_bytes[..]);
let result_with_context =
Transaction::zcash_deserialize_with_context(&transaction_bytes[..], &BranchId::Nu6_3);
assert!(
matches!(
result_with_context,
Err(SerializationError::Parse(
"v6 transaction must have a NU6.3 or later consensus branch ID"
))
),
"V6 with {branch_id:?} must fail to parse with context. \
Result: {result_with_context:?}",
);
assert!(
matches!(
result_without_context,
Err(SerializationError::Parse(
"v6 transaction must have a NU6.3 or later consensus branch ID"
))
),
"V6 with {branch_id:?} must fail to parse without context. \
Result: {result_without_context:?}",
);
}
}
#[test]
fn v6_transaction_with_bundles_round_trips() {
use crate::transaction::arbitrary::shielded::{fake_orchard_bundle, fake_v6_transaction};
use ::orchard::bundle::{BundleVersion, Flags};
use zcash_protocol::value::ZatBalance;
let _init_guard = zebra_test::init();
let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
let orchard = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
1,
1,
BundleVersion::orchard_v3(),
);
let ironwood = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
2,
1000,
BundleVersion::ironwood_v3(),
);
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), Some(ironwood));
let bytes = tx
.zcash_serialize_to_vec()
.expect("v6 transaction serializes");
let tx2: Transaction = bytes
.zcash_deserialize_into()
.expect("v6 transaction deserializes");
assert_eq!(tx.hash(), tx2.hash());
assert_eq!(tx2.orchard_actions().count(), 1);
assert_eq!(tx2.ironwood_actions().count(), 2);
}
#[test]
fn v6_orchard_bundle_rejects_cross_address_flag_on_the_wire() {
use crate::transaction::arbitrary::shielded::{
fake_orchard_bundle, fake_v6_transaction, v6_orchard_flags_offset,
};
use ::orchard::bundle::{Authorized, Bundle, BundleVersion, Flags};
use zcash_protocol::value::ZatBalance;
let _init_guard = zebra_test::init();
let zero = ZatBalance::from_i64(0).expect("zero is a valid balance");
let orchard = fake_orchard_bundle(
Flags::CROSS_ADDRESS_DISABLED,
zero,
1,
1,
BundleVersion::orchard_v3(),
);
assert!(
Bundle::<Authorized, ZatBalance>::try_from_parts(
orchard.actions().clone(),
Flags::ENABLED,
zero,
*orchard.anchor(),
orchard.authorization().clone(),
BundleVersion::orchard_v3(),
)
.is_err(),
"an Orchard-pool bundle with enableCrossAddress must not be constructible",
);
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, Some(orchard), None);
let mut bytes = tx
.zcash_serialize_to_vec()
.expect("v6 transaction serializes");
let flags_offset = v6_orchard_flags_offset(1);
assert_eq!(
bytes[flags_offset],
Flags::CROSS_ADDRESS_DISABLED
.to_byte(BundleVersion::orchard_v3())
.expect("the flags are representable"),
"the computed offset must point at the Orchard flag byte",
);
bytes[flags_offset] |= 0b0000_0100;
let result: Result<Transaction, _> = bytes.zcash_deserialize_into();
assert!(
result.is_err(),
"a v6 Orchard bundle with enableCrossAddress must be rejected on the wire",
);
let ironwood = fake_orchard_bundle(Flags::ENABLED, zero, 1, 1000, BundleVersion::ironwood_v3());
assert!(
ironwood.flags().cross_address_enabled(),
"the Ironwood pool permits enableCrossAddress",
);
let tx = fake_v6_transaction(NetworkUpgrade::Nu6_3, None, Some(ironwood));
let bytes = tx
.zcash_serialize_to_vec()
.expect("v6 transaction serializes");
let tx2: Transaction = bytes
.zcash_deserialize_into()
.expect("a v6 Ironwood bundle with enableCrossAddress round-trips");
assert_eq!(tx.hash(), tx2.hash());
assert!(
tx2.ironwood_flags()
.expect("the round-tripped transaction has an Ironwood bundle")
.cross_address_enabled(),
"enableCrossAddress must survive the round trip on the Ironwood bundle",
);
}
#[test]
fn test_coinbase_script() -> Result<()> {
let _init_guard = zebra_test::init();
let tx = hex::decode("0400008085202f89010000000000000000000000000000000000000000000000000000000000000000ffffffff0503b0e72100ffffffff04e8bbe60e000000001976a914ba92ff06081d5ff6542af8d3b2d209d29ba6337c88ac40787d010000000017a914931fec54c1fea86e574462cc32013f5400b891298738c94d010000000017a914c7a4285ed7aed78d8c0e28d7f1839ccb4046ab0c87286bee000000000017a914d45cb1adffb5215a42720532a076f02c7c778c908700000000b0e721000000000000000000000000").unwrap();
let transaction = tx.zcash_deserialize_into::<Transaction>()?;
let recoded_tx = transaction.zcash_serialize_to_vec().unwrap();
assert_eq!(tx, recoded_tx);
let data = transaction.inputs()[0].coinbase_script().unwrap();
let expected = hex::decode("03b0e72100").unwrap();
assert_eq!(data, expected);
Ok(())
}
#[test]
fn orchard_rk_identity_point() {
use crate::transaction::arbitrary::shielded::{
fake_bundle_for_branch, V5_FIRST_ACTION_RK_OFFSET,
};
let _init_guard = zebra_test::init();
let branch_id = zcash_protocol::consensus::BranchId::Nu5;
let bundle = fake_bundle_for_branch(branch_id, ::orchard::ValuePool::Orchard, 1, 7)
.expect("the Orchard pool is defined at NU5");
let expected_rk: [u8; 32] = bundle.actions().head.rk().into();
let tx = Transaction::test_v5_with_orchard(
NetworkUpgrade::Nu5,
Vec::new(),
Vec::new(),
LockTime::unlocked(),
block::Height(0),
Some(bundle),
);
let mut tx_bytes = tx
.zcash_serialize_to_vec()
.expect("the crafted transaction must serialize");
assert_eq!(
&tx_bytes[V5_FIRST_ACTION_RK_OFFSET..V5_FIRST_ACTION_RK_OFFSET + 32],
&expected_rk[..],
"the computed offset must point at the first action's rk",
);
tx_bytes[V5_FIRST_ACTION_RK_OFFSET..V5_FIRST_ACTION_RK_OFFSET + 32].fill(0);
Transaction::zcash_deserialize(&tx_bytes[..]).expect_err("rk = identity should fail");
}
#[test]
fn coinbase_v5_with_sapling_spends_deserializes_successfully() {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let tx_with_spends =
arbitrary::test_transactions(&network).find(|(_, tx)| tx.sapling_spends().count() > 0);
let Some((height, original_tx)) = tx_with_spends else {
panic!("test block vectors must contain at least one transaction with Sapling spends");
};
let original_spend_count = original_tx.sapling_spends().count();
assert!(
original_spend_count > 0,
"source transaction must have Sapling spends"
);
let fake_v5 = arbitrary::transaction_to_fake_v5(&original_tx, &network, height);
assert!(
fake_v5.sapling_spends().count() > 0,
"converted V5 must retain Sapling shielded data with spends",
);
let outputs = fake_v5.outputs();
let outputs = if outputs.is_empty() {
vec![transparent::Output {
value: crate::amount::Amount::zero(),
lock_script: Script::new(&[0u8; 20]),
}]
} else {
outputs
};
let coinbase_tx = fake_v5
.with_transparent_inputs(vec![transparent::Input::Coinbase {
height,
data: vec![0x00; 4],
sequence: 0xFFFF_FFFF,
}])
.with_transparent_outputs(outputs);
assert!(coinbase_tx.is_coinbase(), "transaction must be coinbase");
assert!(
coinbase_tx.sapling_spends().count() > 0,
"coinbase transaction has Sapling spends"
);
let serialized = coinbase_tx
.zcash_serialize_to_vec()
.expect("coinbase V5 with Sapling spends must serialize");
let err = serialized
.zcash_deserialize_into::<Transaction>()
.expect_err("coinbase with Sapling spends must be rejected during deserialization");
assert!(
err.to_string()
.contains("coinbase transaction must not have Sapling spends"),
"unexpected error: {err}"
);
}
fn deserialize_coinbase_tx_with_data(
height: u32,
data_len: usize,
) -> Result<Transaction, SerializationError> {
let tx = Transaction::test_v1(
vec![transparent::Input::Coinbase {
height: Height(height),
data: vec![0x5a; data_len],
sequence: 0xFFFF_FFFF,
}],
vec![transparent::Output {
value: crate::amount::Amount::try_from(1).expect("valid amount"),
lock_script: Script::new(&[]),
}],
LockTime::min_lock_time_timestamp(),
);
let serialized = tx
.zcash_serialize_to_vec()
.expect("coinbase transaction must serialize");
serialized.zcash_deserialize_into::<Transaction>()
}
#[test]
fn coinbase_script_len_bounds_enforced_at_parse() {
let _init_guard = zebra_test::init();
const ONE_BYTE_PUSH_HEIGHT: u32 = 1;
const FOUR_BYTE_PUSH_HEIGHT: u32 = 500_000;
assert!(
matches!(
deserialize_coinbase_tx_with_data(ONE_BYTE_PUSH_HEIGHT, 0),
Err(SerializationError::Parse("Coinbase script is too short"))
),
"1-byte coinbase script must be rejected"
);
assert!(
matches!(
deserialize_coinbase_tx_with_data(FOUR_BYTE_PUSH_HEIGHT, 97),
Err(SerializationError::Parse("Coinbase script is too long"))
),
"101-byte coinbase script must be rejected"
);
deserialize_coinbase_tx_with_data(ONE_BYTE_PUSH_HEIGHT, 1)
.expect("2-byte coinbase script must be accepted");
deserialize_coinbase_tx_with_data(FOUR_BYTE_PUSH_HEIGHT, 96)
.expect("100-byte coinbase script must be accepted");
Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..])
.expect("genesis block must deserialize");
}
#[test]
fn expiry_height_preserves_out_of_range_values() {
let _init_guard = zebra_test::init();
for raw in [0, 499_999_999, 500_000_000, 2_147_483_648, u32::MAX] {
let tx = Transaction::test_v5(
NetworkUpgrade::Nu5,
Vec::new(),
Vec::new(),
LockTime::min_lock_time_timestamp(),
block::Height(raw),
);
let expected = (raw != 0).then_some(Height(raw));
assert_eq!(tx.expiry_height(), expected);
let serialized = tx
.zcash_serialize_to_vec()
.expect("transaction must serialize");
let parsed = serialized
.zcash_deserialize_into::<Transaction>()
.expect("parsing does not enforce the expiry maximum, the verifier does");
assert_eq!(parsed.expiry_height(), expected);
}
}
#[test]
fn sprout_aggregate_value_balance_out_of_range_is_rejected() {
let _init_guard = zebra_test::init();
let zero = Amount::zero();
let max_money = Amount::try_from(MAX_MONEY).expect("MAX_MONEY is a valid nonnegative amount");
let mac = sprout::note::Mac::from([0u8; 32]);
let joinsplit = sprout::JoinSplit {
vpub_old: zero,
vpub_new: max_money,
anchor: sprout::tree::Root::default(),
nullifiers: [
sprout::note::Nullifier([0u8; 32].into()),
sprout::note::Nullifier([1u8; 32].into()),
],
commitments: [sprout::commitment::NoteCommitment::from([0u8; 32]); 2],
ephemeral_key: x25519::PublicKey::from([0u8; 32]),
random_seed: sprout::RandomSeed::from([0u8; 32]),
vmacs: [mac.clone(), mac],
zkproof: Groth16Proof([0u8; 192]),
enc_ciphertexts: [sprout::note::EncryptedNote([0u8; 601]); 2],
};
let in_range = Transaction::test_v4_with_joinsplit_data(Some(&JoinSplitData {
first: joinsplit.clone(),
rest: vec![],
pub_key: [0u8; 32].into(),
sig: [0u8; 64].into(),
}));
in_range
.sprout_value_balance()
.expect("an aggregate of MAX_MONEY is in range");
let out_of_range = Transaction::test_v4_with_joinsplit_data(Some(&JoinSplitData {
first: joinsplit.clone(),
rest: vec![joinsplit],
pub_key: [0u8; 32].into(),
sig: [0u8; 64].into(),
}));
assert!(
matches!(
out_of_range.sprout_value_balance(),
Err(ValueBalanceError::Sprout(_))
),
"an out-of-range Sprout aggregate must error, not zero"
);
assert!(
out_of_range
.value_balance(&std::collections::HashMap::new())
.is_err(),
"the transaction value balance must propagate the error"
);
}
#[test]
fn non_coinbase_with_null_prevout_input_is_not_valid_non_coinbase() {
let mut raw = Vec::new();
raw.extend_from_slice(&1_u32.to_le_bytes()); raw.push(2); raw.extend_from_slice(&[0; 32]);
raw.extend_from_slice(&0xFFFF_FFFF_u32.to_le_bytes());
raw.push(2);
raw.extend_from_slice(&[0x51, 0x00]);
raw.extend_from_slice(&0xFFFF_FFFF_u32.to_le_bytes());
raw.extend_from_slice(&[1; 32]);
raw.extend_from_slice(&0_u32.to_le_bytes());
raw.push(0);
raw.extend_from_slice(&0xFFFF_FFFF_u32.to_le_bytes());
raw.push(1); raw.extend_from_slice(&1_u64.to_le_bytes());
raw.push(0);
raw.extend_from_slice(&0_u32.to_le_bytes());
let tx: Transaction = raw
.zcash_deserialize_into()
.expect("a null-prevout input with a valid height script parses");
assert!(
matches!(tx.inputs()[0], crate::transparent::Input::Coinbase { .. }),
"the null-prevout input is parsed as a coinbase input"
);
assert!(!tx.is_coinbase(), "two inputs is never a coinbase");
assert!(
!tx.is_valid_non_coinbase(),
"a non-coinbase transaction with a null-prevout input is invalid"
);
let mut regular = raw.clone();
regular[4] = 1; regular.drain(5..5 + 32 + 4 + 1 + 2 + 4); let tx: Transaction = regular
.zcash_deserialize_into()
.expect("a single regular input parses");
assert!(!tx.is_coinbase());
assert!(tx.is_valid_non_coinbase());
}