Merge pull request #633 from daira/prepare-epks-and-ivks

Add APIs to prepare ivk and epk and implement them for Sapling
This commit is contained in:
str4d 2022-09-15 12:45:33 +01:00 committed by GitHub
commit 84835035d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 156 additions and 73 deletions

View File

@ -21,3 +21,5 @@ codegen-units = 1
[patch.crates-io]
zcash_encoding = { path = "components/zcash_encoding" }
zcash_note_encryption = { path = "components/zcash_note_encryption" }
orchard = { git = "https://github.com/zcash/orchard.git", rev = "33f1c1141e50adb68715f3359bd75378b4756cca" }
group = { git = "https://github.com/zkcrypto/group.git", rev = "a7f3ceb2373e9fe536996f7b4d55c797f3e667f0" }

View File

@ -6,7 +6,17 @@ and this library adheres to Rust's notion of
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- `zcash_note_encryption::Domain`:
- `Domain::PreparedEphemeralPublicKey` associated type.
- `Domain::prepare_epk` method, which produces the above type.
### Changed
- MSRV is now 1.56.1.
- Migrated to `group 0.13`.
- `zcash_note_encryption::Domain` now requires `epk` to be converted to
`Domain::PreparedEphemeralPublicKey` before being passed to
`Domain::ka_agree_dec`.
- Changes to batch decryption APIs:
- The return types of `batch::try_note_decryption` and
`batch::try_compact_note_decryption` have changed. Now, instead of
@ -16,8 +26,5 @@ and this library adheres to Rust's notion of
argument to the function. Each successful result includes the index of the
entry in `ivks` used to decrypt the value.
### Changed
- MSRV is now 1.56.1.
## [0.1.0] - 2021-12-17
Initial release.

View File

@ -21,6 +21,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
chacha20 = { version = "0.8", default-features = false }
chacha20poly1305 = { version = "0.9", default-features = false }
group = "0.12"
rand_core = { version = "0.6", default-features = false }
subtle = { version = "2.2.3", default-features = false }

View File

@ -51,7 +51,7 @@ where
return (0..outputs.len()).map(|_| None).collect();
};
// Fetch the ephemeral keys for each output and batch-parse them.
// Fetch the ephemeral keys for each output, and batch-parse and prepare them.
let ephemeral_keys = D::batch_epk(outputs.iter().map(|(_, output)| output.ephemeral_key()));
// Derive the shared secrets for all combinations of (ivk, output).

View File

@ -113,6 +113,7 @@ enum NoteValidity {
pub trait Domain {
type EphemeralSecretKey: ConstantTimeEq;
type EphemeralPublicKey;
type PreparedEphemeralPublicKey;
type SharedSecret;
type SymmetricKey: AsRef<[u8]>;
type Note;
@ -136,6 +137,9 @@ pub trait Domain {
/// Extracts the `DiversifiedTransmissionKey` from the note.
fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey;
/// Prepare an ephemeral public key for more efficient scalar multiplication.
fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey;
/// Derives `EphemeralPublicKey` from `esk` and the note's diversifier.
fn ka_derive_public(
note: &Self::Note,
@ -152,7 +156,7 @@ pub trait Domain {
/// decryption.
fn ka_agree_dec(
ivk: &Self::IncomingViewingKey,
epk: &Self::EphemeralPublicKey,
epk: &Self::PreparedEphemeralPublicKey,
) -> Self::SharedSecret;
/// Derives the `SymmetricKey` used to encrypt the note plaintext.
@ -306,10 +310,15 @@ pub trait BatchDomain: Domain {
/// them.
fn batch_epk(
ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
) -> Vec<(Option<Self::EphemeralPublicKey>, EphemeralKeyBytes)> {
) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
// Default implementation: do the non-batched thing.
ephemeral_keys
.map(|ephemeral_key| (Self::epk(&ephemeral_key), ephemeral_key))
.map(|ephemeral_key| {
(
Self::epk(&ephemeral_key).map(Self::prepare_epk),
ephemeral_key,
)
})
.collect()
}
}
@ -514,7 +523,7 @@ pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_S
) -> Option<(D::Note, D::Recipient, D::Memo)> {
let ephemeral_key = output.ephemeral_key();
let epk = D::epk(&ephemeral_key)?;
let epk = D::prepare_epk(D::epk(&ephemeral_key)?);
let shared_secret = D::ka_agree_dec(ivk, &epk);
let key = D::kdf(shared_secret, &ephemeral_key);
@ -611,7 +620,7 @@ pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_
) -> Option<(D::Note, D::Recipient)> {
let ephemeral_key = output.ephemeral_key();
let epk = D::epk(&ephemeral_key)?;
let epk = D::prepare_epk(D::epk(&ephemeral_key)?);
let shared_secret = D::ka_agree_dec(ivk, &epk);
let key = D::kdf(shared_secret, &ephemeral_key);

View File

@ -85,7 +85,7 @@ use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, NetworkUpgrade},
merkle_tree::CommitmentTree,
sapling::{keys::Scope, Nullifier},
sapling::{keys::Scope, note_encryption::PreparedIncomingViewingKey, Nullifier},
};
use crate::{
@ -234,12 +234,15 @@ where
let mut batch_runner = BatchRunner::new(
100,
dfvks.iter().flat_map(|(account, dfvk)| {
[
((**account, Scope::External), dfvk.to_ivk(Scope::External)),
((**account, Scope::Internal), dfvk.to_ivk(Scope::Internal)),
]
}),
dfvks
.iter()
.flat_map(|(account, dfvk)| {
[
((**account, Scope::External), dfvk.to_ivk(Scope::External)),
((**account, Scope::Internal), dfvk.to_ivk(Scope::Internal)),
]
})
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(&ivk))),
);
cache.with_blocks(last_height, limit, |block: CompactBlock| {

View File

@ -4,7 +4,9 @@ use zcash_primitives::{
consensus::{self, BlockHeight},
memo::MemoBytes,
sapling::{
note_encryption::{try_sapling_note_decryption, try_sapling_output_recovery},
note_encryption::{
try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey,
},
Note, PaymentAddress,
},
transaction::Transaction,
@ -47,7 +49,7 @@ pub fn decrypt_transaction<P: consensus::Parameters>(
if let Some(bundle) = tx.sapling_bundle() {
for (account, ufvk) in ufvks.iter() {
if let Some(dfvk) = ufvk.sapling() {
let ivk = dfvk.fvk().vk.ivk();
let ivk = PreparedIncomingViewingKey::new(&dfvk.fvk().vk.ivk());
let ovk = dfvk.fvk().ovk;
for (index, output) in bundle.shielded_outputs.iter().enumerate() {

View File

@ -11,7 +11,7 @@ use zcash_primitives::{
sapling::{
self,
keys::{DiversifiableFullViewingKey, Scope},
note_encryption::SaplingDomain,
note_encryption::{PreparedIncomingViewingKey, SaplingDomain},
Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk,
},
transaction::components::sapling::CompactOutputDescription,
@ -325,7 +325,8 @@ pub(crate) fn scan_block_with_runner<P: consensus::Parameters + Send + 'static,
let ivks = vks
.iter()
.map(|(_, ivk, _)| (*ivk).clone())
.map(|(_, ivk, _)| ivk)
.map(PreparedIncomingViewingKey::new)
.collect::<Vec<_>>();
batch::try_compact_note_decryption(&ivks, decoded)
@ -413,8 +414,9 @@ mod tests {
memo::MemoBytes,
merkle_tree::CommitmentTree,
sapling::{
note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier,
SaplingIvk,
note_encryption::{sapling_note_encryption, PreparedIncomingViewingKey},
util::generate_random_rseed,
Note, Nullifier, SaplingIvk,
},
transaction::components::Amount,
zip32::{AccountId, ExtendedFullViewingKey, ExtendedSpendingKey},
@ -557,7 +559,8 @@ mod tests {
extfvk
.to_sapling_keys()
.iter()
.map(|(scope, ivk, _)| ((account, *scope), ivk.clone())),
.map(|(scope, ivk, _)| ((account, *scope), ivk))
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))),
);
add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner);
@ -620,7 +623,8 @@ mod tests {
extfvk
.to_sapling_keys()
.iter()
.map(|(scope, ivk, _)| ((account, *scope), ivk.clone())),
.map(|(scope, ivk, _)| ((account, *scope), ivk))
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(ivk))),
);
add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner);

View File

@ -17,6 +17,9 @@ and this library adheres to Rust's notion of
- `Scope`
- `ExpandedSpendingKey::from_bytes`
- `ExtendedSpendingKey::{from_bytes, to_bytes}`
- `zcash_primitives::sapling::note_encryption`:
- `PreparedIncomingViewingKey`
- `PreparedEphemeralPublicKey`
- Added in `zcash_primitives::zip32`
- `ChainCode::as_bytes`
- `DiversifierKey::{from_bytes, as_bytes}`
@ -35,6 +38,7 @@ and this library adheres to Rust's notion of
- `Bundle::value_balance`
### Changed
- Migrated to `group 0.13`.
- `zcash_primitives::sapling::ViewingKey` now stores `nk` as a
`NullifierDerivingKey` instead of as a bare `jubjub::SubgroupPoint`.
- The signature of `zcash_primitives::sapling::Note::nf` has changed to
@ -42,6 +46,15 @@ and this library adheres to Rust's notion of
rather than the full `ViewingKey` as its first argument.
- Made the internals of `zip32::DiversifierKey` private; use `from_bytes` and
`as_bytes` on this type instead.
- `zcash_primitives::sapling::note_encryption` APIs now expose precomputations
explicitly (where previously they were performed internally), to enable users
to avoid recomputing incoming viewing key precomputations. Users now need to
call `PreparedIncomingViewingKey::new` to convert their `SaplingIvk`s into
their precomputed forms, and can do so wherever it makes sense in their stack.
- `SaplingDomain::IncomingViewingKey` is now `PreparedIncomingViewingKey`
instead of `SaplingIvk`.
- `try_sapling_note_decryption` and `try_sapling_compact_note_decryption` now
take `&PreparedIncomingViewingKey` instead of `&SaplingIvk`.
## [0.7.0] - 2022-06-24
### Changed

View File

@ -11,7 +11,7 @@ use zcash_primitives::{
sapling::{
note_encryption::{
sapling_note_encryption, try_sapling_compact_note_decryption,
try_sapling_note_decryption, SaplingDomain,
try_sapling_note_decryption, PreparedIncomingViewingKey, SaplingDomain,
},
util::generate_random_rseed,
Diversifier, PaymentAddress, SaplingIvk, ValueCommitment,
@ -67,6 +67,9 @@ fn bench_note_decryption(c: &mut Criterion) {
}
};
let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk);
let invalid_ivk = PreparedIncomingViewingKey::new(&invalid_ivk);
{
let mut group = c.benchmark_group("sapling-note-decryption");
group.throughput(Throughput::Elements(1));
@ -98,40 +101,42 @@ fn bench_note_decryption(c: &mut Criterion) {
}
{
let valid_ivks = vec![valid_ivk];
let invalid_ivks = vec![invalid_ivk];
// We benchmark with one IVK so the overall batch size is equal to the number of
// outputs.
let size = 10;
let outputs: Vec<_> = iter::repeat(output)
.take(size)
.map(|output| (SaplingDomain::for_height(TEST_NETWORK, height), output))
.collect();
let mut group = c.benchmark_group("sapling-batch-note-decryption");
group.throughput(Throughput::Elements(size as u64));
group.bench_function(BenchmarkId::new("valid", size), |b| {
b.iter(|| batch::try_note_decryption(&valid_ivks, &outputs))
});
for (nivks, noutputs) in [(1, 10), (10, 1), (10, 10), (50, 50)] {
let invalid_ivks: Vec<_> = iter::repeat(invalid_ivk.clone()).take(nivks).collect();
let valid_ivks: Vec<_> = iter::repeat(valid_ivk.clone()).take(nivks).collect();
group.bench_function(BenchmarkId::new("invalid", size), |b| {
b.iter(|| batch::try_note_decryption(&invalid_ivks, &outputs))
});
let outputs: Vec<_> = iter::repeat(output.clone())
.take(noutputs)
.map(|output| (SaplingDomain::for_height(TEST_NETWORK, height), output))
.collect();
let compact: Vec<_> = outputs
.into_iter()
.map(|(domain, output)| (domain, CompactOutputDescription::from(output)))
.collect();
group.bench_function(
BenchmarkId::new(format!("valid-{}", nivks), noutputs),
|b| b.iter(|| batch::try_note_decryption(&valid_ivks, &outputs)),
);
group.bench_function(BenchmarkId::new("compact-valid", size), |b| {
b.iter(|| batch::try_compact_note_decryption(&valid_ivks, &compact))
});
group.bench_function(
BenchmarkId::new(format!("invalid-{}", nivks), noutputs),
|b| b.iter(|| batch::try_note_decryption(&invalid_ivks, &outputs)),
);
group.bench_function(BenchmarkId::new("compact-invalid", size), |b| {
b.iter(|| batch::try_compact_note_decryption(&invalid_ivks, &compact))
});
let compact: Vec<_> = outputs
.into_iter()
.map(|(domain, output)| (domain, CompactOutputDescription::from(output)))
.collect();
group.bench_function(
BenchmarkId::new(format!("compact-valid-{}", nivks), noutputs),
|b| b.iter(|| batch::try_compact_note_decryption(&valid_ivks, &compact)),
);
group.bench_function(
BenchmarkId::new(format!("compact-invalid-{}", nivks), noutputs),
|b| b.iter(|| batch::try_compact_note_decryption(&invalid_ivks, &compact)),
);
}
}
}

View File

@ -2,7 +2,7 @@
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
use byteorder::{LittleEndian, WriteBytesExt};
use ff::PrimeField;
use group::{cofactor::CofactorGroup, GroupEncoding};
use group::{cofactor::CofactorGroup, GroupEncoding, WnafBase, WnafScalar};
use jubjub::{AffinePoint, ExtendedPoint};
use rand_core::RngCore;
@ -27,16 +27,39 @@ use crate::{
pub const KDF_SAPLING_PERSONALIZATION: &[u8; 16] = b"Zcash_SaplingKDF";
pub const PRF_OCK_PERSONALIZATION: &[u8; 16] = b"Zcash_Derive_ock";
const PREPARED_WINDOW_SIZE: usize = 4;
type PreparedBase = WnafBase<jubjub::ExtendedPoint, PREPARED_WINDOW_SIZE>;
type PreparedBaseSubgroup = WnafBase<jubjub::SubgroupPoint, PREPARED_WINDOW_SIZE>;
type PreparedScalar = WnafScalar<jubjub::Scalar, PREPARED_WINDOW_SIZE>;
/// A Sapling incoming viewing key that has been precomputed for trial decryption.
#[derive(Clone, Debug)]
pub struct PreparedIncomingViewingKey(PreparedScalar);
impl PreparedIncomingViewingKey {
/// Performs the necessary precomputations to use a `SaplingIvk` for note decryption.
pub fn new(ivk: &SaplingIvk) -> Self {
Self(PreparedScalar::new(&ivk.0))
}
}
/// A Sapling ephemeral public key that has been precomputed for trial decryption.
#[derive(Clone, Debug)]
pub struct PreparedEphemeralPublicKey(PreparedBase);
/// Sapling key agreement for note encryption.
///
/// Implements section 5.4.4.3 of the Zcash Protocol Specification.
pub fn sapling_ka_agree(esk: &jubjub::Fr, pk_d: &jubjub::ExtendedPoint) -> jubjub::SubgroupPoint {
sapling_ka_agree_prepared(&PreparedScalar::new(esk), &PreparedBase::new(*pk_d))
}
fn sapling_ka_agree_prepared(esk: &PreparedScalar, pk_d: &PreparedBase) -> jubjub::SubgroupPoint {
// [8 esk] pk_d
// <ExtendedPoint as CofactorGroup>::clear_cofactor is implemented using
// ExtendedPoint::mul_by_cofactor in the jubjub crate.
let mut wnaf = group::Wnaf::new();
wnaf.scalar(esk).base(*pk_d).clear_cofactor()
(pk_d * esk).clear_cofactor()
}
/// Sapling KDF for note encryption.
@ -132,12 +155,13 @@ impl<P: consensus::Parameters> Domain for SaplingDomain<P> {
// points must not be small-order, and all points with non-canonical serialization
// are small-order.
type EphemeralPublicKey = jubjub::ExtendedPoint;
type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
type SharedSecret = jubjub::SubgroupPoint;
type SymmetricKey = Blake2bHash;
type Note = Note;
type Recipient = PaymentAddress;
type DiversifiedTransmissionKey = jubjub::SubgroupPoint;
type IncomingViewingKey = SaplingIvk;
type IncomingViewingKey = PreparedIncomingViewingKey;
type OutgoingViewingKey = OutgoingViewingKey;
type ValueCommitment = jubjub::ExtendedPoint;
type ExtractedCommitment = bls12_381::Scalar;
@ -152,6 +176,10 @@ impl<P: consensus::Parameters> Domain for SaplingDomain<P> {
note.pk_d
}
fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
PreparedEphemeralPublicKey(PreparedBase::new(epk))
}
fn ka_derive_public(
note: &Self::Note,
esk: &Self::EphemeralSecretKey,
@ -173,9 +201,9 @@ impl<P: consensus::Parameters> Domain for SaplingDomain<P> {
fn ka_agree_dec(
ivk: &Self::IncomingViewingKey,
epk: &Self::EphemeralPublicKey,
epk: &Self::PreparedEphemeralPublicKey,
) -> Self::SharedSecret {
sapling_ka_agree(&ivk.0, epk)
sapling_ka_agree_prepared(&ivk.0, &epk.0)
}
/// Sapling KDF for note encryption.
@ -253,7 +281,7 @@ impl<P: consensus::Parameters> Domain for SaplingDomain<P> {
plaintext: &[u8],
) -> Option<(Self::Note, Self::Recipient)> {
sapling_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
Some(diversifier.g_d()? * ivk.0)
Some(&PreparedBaseSubgroup::new(diversifier.g_d()?) * &ivk.0)
})
}
@ -332,13 +360,18 @@ impl<P: consensus::Parameters> BatchDomain for SaplingDomain<P> {
fn batch_epk(
ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
) -> Vec<(Option<Self::EphemeralPublicKey>, EphemeralKeyBytes)> {
) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
let ephemeral_keys: Vec<_> = ephemeral_keys.collect();
let epks = jubjub::AffinePoint::batch_from_bytes(ephemeral_keys.iter().map(|b| b.0));
epks.into_iter()
.zip(ephemeral_keys.into_iter())
.map(|(epk, ephemeral_key)| {
(epk.map(jubjub::ExtendedPoint::from).into(), ephemeral_key)
(
epk.map(jubjub::ExtendedPoint::from)
.map(Self::prepare_epk)
.into(),
ephemeral_key,
)
})
.collect()
}
@ -391,7 +424,7 @@ pub fn try_sapling_note_decryption<
>(
params: &P,
height: BlockHeight,
ivk: &SaplingIvk,
ivk: &PreparedIncomingViewingKey,
output: &Output,
) -> Option<(Note, PaymentAddress, MemoBytes)> {
let domain = SaplingDomain {
@ -407,7 +440,7 @@ pub fn try_sapling_compact_note_decryption<
>(
params: &P,
height: BlockHeight,
ivk: &SaplingIvk,
ivk: &PreparedIncomingViewingKey,
output: &Output,
) -> Option<(Note, PaymentAddress)> {
let domain = SaplingDomain {
@ -493,7 +526,7 @@ mod tests {
},
keys::OutgoingViewingKey,
memo::MemoBytes,
sapling::util::generate_random_rseed,
sapling::{note_encryption::PreparedIncomingViewingKey, util::generate_random_rseed},
sapling::{Diversifier, PaymentAddress, Rseed, SaplingIvk, ValueCommitment},
transaction::components::{
amount::Amount,
@ -508,18 +541,21 @@ mod tests {
) -> (
OutgoingViewingKey,
OutgoingCipherKey,
SaplingIvk,
PreparedIncomingViewingKey,
OutputDescription<sapling::GrothProofBytes>,
) {
let ivk = SaplingIvk(jubjub::Fr::random(&mut rng));
let prepared_ivk = PreparedIncomingViewingKey::new(&ivk);
let (ovk, ock, output) = random_enc_ciphertext_with(height, &ivk, rng);
assert!(try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output).is_some());
assert!(
try_sapling_note_decryption(&TEST_NETWORK, height, &prepared_ivk, &output).is_some()
);
assert!(try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&ivk,
&prepared_ivk,
&CompactOutputDescription::from(output.clone()),
)
.is_some());
@ -532,7 +568,7 @@ mod tests {
assert!(ock_output_recovery.is_some());
assert_eq!(ovk_output_recovery, ock_output_recovery);
(ovk, ock, ivk, output)
(ovk, ock, prepared_ivk, output)
}
fn random_enc_ciphertext_with<R: RngCore + CryptoRng>(
@ -685,7 +721,7 @@ mod tests {
try_sapling_note_decryption(
&TEST_NETWORK,
height,
&SaplingIvk(jubjub::Fr::random(&mut rng)),
&PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(&mut rng))),
&output
),
None
@ -851,7 +887,7 @@ mod tests {
try_sapling_compact_note_decryption(
&TEST_NETWORK,
height,
&SaplingIvk(jubjub::Fr::random(&mut rng)),
&PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(&mut rng))),
&CompactOutputDescription::from(output)
),
None
@ -1309,7 +1345,7 @@ mod tests {
// Load the test vector components
//
let ivk = SaplingIvk(read_jubjub_scalar!(tv.ivk));
let ivk = PreparedIncomingViewingKey::new(&SaplingIvk(read_jubjub_scalar!(tv.ivk)));
let pk_d = read_point!(tv.default_pk_d).into_subgroup().unwrap();
let rcm = read_jubjub_scalar!(tv.rcm);
let cv = read_point!(tv.cv);
@ -1439,7 +1475,7 @@ mod tests {
let height = TEST_NETWORK.activation_height(Canopy).unwrap();
// Test batch trial-decryption with multiple IVKs and outputs.
let invalid_ivk = SaplingIvk(jubjub::Fr::random(rng));
let invalid_ivk = PreparedIncomingViewingKey::new(&SaplingIvk(jubjub::Fr::random(rng)));
let valid_ivk = SaplingIvk(jubjub::Fr::random(rng));
let outputs: Vec<_> = (0..10)
.map(|_| {
@ -1449,6 +1485,7 @@ mod tests {
)
})
.collect();
let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk);
// Check that batched trial decryptions with invalid_ivk fails.
let res = batch::try_note_decryption(&[invalid_ivk.clone()], &outputs);