use std::{
future,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
task::{Context, Poll},
};
use tower::{Service, ServiceExt};
use zebra_chain::{
block::{Block, Height},
parameters::{Network, NetworkUpgrade},
serialization::ZcashDeserializeInto,
transaction::{HashType, Transaction, TxVersion},
transparent,
};
use crate::{error::TransactionError, BoxError};
use super::{batch_fallback_verifier, BatchFallbackService, CacheKey, Cached, CachedItem, Item};
const TEST_CACHE_VERIFIER_LABEL: &str = "groth16_sapling_test";
fn mined_sapling_transactions() -> Vec<(NetworkUpgrade, Transaction)> {
let mut transactions = Vec::new();
for (height, bytes) in zebra_test::vectors::MAINNET_BLOCKS.iter() {
let block: Block = bytes
.zcash_deserialize_into()
.expect("hard-coded test vector must deserialize");
let nu = NetworkUpgrade::current(&Network::Mainnet, Height(*height));
for tx in &block.transactions {
if !tx.inputs().is_empty() {
continue;
}
if item(tx, nu).is_some() {
transactions.push((nu, tx.as_ref().clone()));
}
}
}
assert!(
!transactions.is_empty(),
"mainnet test blocks must contain a transparent-input-free Sapling transaction"
);
transactions
}
fn sapling_transactions() -> Vec<Transaction> {
mined_sapling_transactions()
.into_iter()
.map(|(_, tx)| tx)
.collect()
}
fn sapling_transaction() -> Transaction {
sapling_transactions()
.into_iter()
.next()
.expect("there is at least one Sapling transaction")
}
fn mined_v4_sapling_transaction() -> (NetworkUpgrade, Transaction) {
mined_sapling_transactions()
.into_iter()
.find(|(_, tx)| tx.tx_version() == TxVersion::V4)
.expect("mainnet test blocks must contain a V4 Sapling transaction")
}
fn item(tx: &Transaction, nu: NetworkUpgrade) -> Option<Item> {
let all_previous_outputs: Arc<Vec<transparent::Output>> = Arc::new(Vec::new());
let sighasher = tx.sighasher(nu, all_previous_outputs).ok()?;
let bundle = sighasher.sapling_bundle()?;
Some(Item::new(
bundle,
sighasher.sighash(HashType::ALL, None),
tx.unmined_id(),
))
}
fn cache_key(tx: &Transaction, nu: NetworkUpgrade) -> CacheKey {
item(tx, nu)
.expect("the transaction was selected for having a Sapling bundle")
.cache_key()
.expect("every Sapling item carries a cache key")
}
#[test]
fn cache_key_is_deterministic() {
let tx = sapling_transaction();
assert_eq!(
cache_key(&tx, NetworkUpgrade::Nu5),
cache_key(&tx, NetworkUpgrade::Nu5),
"the same transaction, sighash, and pool must always produce the same key"
);
}
#[test]
fn cache_key_distinguishes_different_transactions() {
let transactions = sapling_transactions();
let first = transactions.first().expect("there is a first transaction");
let second = transactions
.iter()
.find(|tx| tx.hash() != first.hash())
.expect("mainnet test blocks must contain two distinct Sapling transactions");
assert_ne!(
cache_key(first, NetworkUpgrade::Nu5),
cache_key(second, NetworkUpgrade::Nu5),
"two transactions must not share a cache key"
);
}
#[test]
fn cache_key_commits_to_the_sighash() {
let (_nu, tx) = mined_v4_sapling_transaction();
assert_ne!(
cache_key(&tx, NetworkUpgrade::Canopy),
cache_key(&tx, NetworkUpgrade::Nu5),
"the same bundle verified under two branch ids must not share a cache key"
);
}
#[derive(Clone)]
struct CountingVerifier {
calls: Arc<AtomicUsize>,
succeeds: bool,
}
impl CountingVerifier {
fn new(succeeds: bool) -> Self {
Self {
calls: Arc::new(AtomicUsize::new(0)),
succeeds,
}
}
fn calls(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
impl Service<Item> for CountingVerifier {
type Response = ();
type Error = BoxError;
type Future = future::Ready<Result<(), BoxError>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _item: Item) -> Self::Future {
self.calls.fetch_add(1, Ordering::SeqCst);
future::ready(if self.succeeds {
Ok(())
} else {
Err(TransactionError::SaplingVerificationFailed.into())
})
}
}
#[tokio::test]
async fn cache_skips_the_inner_service_for_an_already_verified_bundle() {
let tx = sapling_transaction();
let inner = CountingVerifier::new(true);
let mut verifier = Cached::new(inner.clone(), 8, TEST_CACHE_VERIFIER_LABEL);
for _ in 0..3 {
verifier
.ready()
.await
.expect("the cache must become ready")
.call(item(&tx, NetworkUpgrade::Nu5).expect("the transaction has a bundle"))
.await
.expect("a valid item must verify");
}
assert_eq!(
inner.calls(),
1,
"only the first verification of a bundle may reach the inner service"
);
}
#[tokio::test]
async fn cache_does_not_reuse_a_result_across_bundles() {
let transactions = sapling_transactions();
let inner = CountingVerifier::new(true);
let mut verifier = Cached::new(inner.clone(), 8, TEST_CACHE_VERIFIER_LABEL);
for tx in transactions.iter().take(2) {
verifier
.ready()
.await
.expect("the cache must become ready")
.call(item(tx, NetworkUpgrade::Nu5).expect("the transaction has a bundle"))
.await
.expect("the inner service accepts everything in this test");
}
assert_eq!(
inner.calls(),
2,
"bundles with different keys must each be verified"
);
}
#[tokio::test]
async fn cache_does_not_remember_failures() {
let tx = sapling_transaction();
let inner = CountingVerifier::new(false);
let mut verifier = Cached::new(inner.clone(), 8, TEST_CACHE_VERIFIER_LABEL);
for _ in 0..3 {
verifier
.ready()
.await
.expect("the cache must become ready")
.call(item(&tx, NetworkUpgrade::Nu5).expect("the transaction has a bundle"))
.await
.expect_err("the inner service rejects everything in this test");
}
assert_eq!(
inner.calls(),
3,
"a failed verification must not be remembered"
);
}
fn uncached_verification_behind_a_fresh_cache() -> Cached<BatchFallbackService> {
Cached::new(batch_fallback_verifier(), 8, TEST_CACHE_VERIFIER_LABEL)
}
#[tokio::test(flavor = "multi_thread")]
async fn a_bundle_verified_under_one_upgrade_is_not_reused_under_another() {
let _init_guard = zebra_test::init();
let (mined_upgrade, tx) = mined_v4_sapling_transaction();
let other_upgrade = if mined_upgrade == NetworkUpgrade::Nu5 {
NetworkUpgrade::Canopy
} else {
NetworkUpgrade::Nu5
};
let mined_item = item(&tx, mined_upgrade).expect("the transaction has a bundle");
let other_item = item(&tx, other_upgrade).expect("the transaction has a bundle");
assert_eq!(
(
mined_item.bundle.shielded_spends().len(),
mined_item.bundle.shielded_outputs().len(),
*mined_item.bundle.value_balance(),
),
(
other_item.bundle.shielded_spends().len(),
other_item.bundle.shielded_outputs().len(),
*other_item.bundle.value_balance(),
),
"the branch id must not change the parsed bundle, or this test proves nothing"
);
assert_ne!(
mined_item.cache_key(),
other_item.cache_key(),
"the two sighashes must produce different keys"
);
let verifier = uncached_verification_behind_a_fresh_cache();
verifier
.clone()
.oneshot(mined_item)
.await
.expect("a real mainnet Sapling bundle must verify under the upgrade that mined it");
let error = verifier
.clone()
.oneshot(other_item)
.await
.expect_err("the same bundle must not verify against another upgrade's sighash");
let error = error
.downcast::<TransactionError>()
.expect("the verifier reports a typed transaction error");
assert!(
matches!(*error, TransactionError::SaplingVerificationFailed),
"expected SaplingVerificationFailed, got: {error:?}"
);
}