Restructure batch decryption to avoid cartesian product of results.

While it is necessary in the worst case to perform `m * n` decryptions,
where `m` is the number of outputs being decrypted and `n` is the number
of IVKs, it is possible to stop performing trial decryptions when the
first successful decryption is performed. Also, it's inconvenient and
unnecessary to return the full cartesian product of these results, as
only one IVK will decrypt a given output. This commit modifies batch
trial decryption to stop on the first successful decryption, and instead
of returning the cartesian product of results we return the index of the
input IVK along with the output it decrypted. Note that this means that
trial decryption is not constant-time with respect to the number and/or
order of IVKs.
This commit is contained in:
Kris Nuttycombe 2022-07-25 19:06:53 -06:00
parent 37fc28634e
commit 5873950648
4 changed files with 57 additions and 35 deletions

View File

@ -7,6 +7,15 @@ and this library adheres to Rust's notion of
## [Unreleased]
- Changes to batch decryption APIs:
- The return types of `batch::try_note_decryption` and
`batch::try_compact_note_decryption` have changed. Now, instead of
returning entries corresponding to the cartesian product of the IVKs used for
decryption with the outputs being decrypted, this now returns a vector of
decryption results of the same length and in the same order as the `outputs`
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.

View File

@ -1,7 +1,6 @@
//! APIs for batch trial decryption.
use alloc::vec::Vec; // module is alloc only
use core::iter;
use crate::{
try_compact_note_decryption_inner, try_note_decryption_inner, BatchDomain, EphemeralKeyBytes,
@ -11,21 +10,32 @@ use crate::{
/// Trial decryption of a batch of notes with a set of recipients.
///
/// This is the batched version of [`crate::try_note_decryption`].
///
/// Returns a vector containing the decrypted result for each output,
/// with the same length and in the same order as the outputs were
/// provided, along with the index in the `ivks` slice associated with
/// the IVK that successfully decrypted the output.
#[allow(clippy::type_complexity)]
pub fn try_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
) -> Vec<Option<(D::Note, D::Recipient, D::Memo)>> {
) -> Vec<Option<((D::Note, D::Recipient, D::Memo), usize)>> {
batch_note_decryption(ivks, outputs, try_note_decryption_inner)
}
/// Trial decryption of a batch of notes for light clients with a set of recipients.
///
/// This is the batched version of [`crate::try_compact_note_decryption`].
///
/// Returns a vector containing the decrypted result for each output,
/// with the same length and in the same order as the outputs were
/// provided, along with the index in the `ivks` slice associated with
/// the IVK that successfully decrypted the output.
#[allow(clippy::type_complexity)]
pub fn try_compact_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
) -> Vec<Option<(D::Note, D::Recipient)>> {
) -> Vec<Option<((D::Note, D::Recipient), usize)>> {
batch_note_decryption(ivks, outputs, try_compact_note_decryption_inner)
}
@ -33,17 +43,17 @@ fn batch_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, CS>, F, FR, c
ivks: &[D::IncomingViewingKey],
outputs: &[(D, Output)],
decrypt_inner: F,
) -> Vec<Option<FR>>
) -> Vec<Option<(FR, usize)>>
where
F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, D::SymmetricKey) -> Option<FR>,
F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, &D::SymmetricKey) -> Option<FR>,
{
// Fetch the ephemeral keys for each output and batch-parse them.
let ephemeral_keys = D::batch_epk(outputs.iter().map(|(_, output)| output.ephemeral_key()));
// Derive the shared secrets for all combinations of (ivk, output).
// The scalar multiplications cannot benefit from batching.
let items = ivks.iter().flat_map(|ivk| {
ephemeral_keys.iter().map(move |(epk, ephemeral_key)| {
let items = ephemeral_keys.iter().flat_map(|(epk, ephemeral_key)| {
ivks.iter().map(move |ivk| {
(
epk.as_ref().map(|epk| D::ka_agree_dec(ivk, epk)),
ephemeral_key,
@ -55,17 +65,18 @@ where
let keys = D::batch_kdf(items);
// Finish the trial decryption!
ivks.iter()
.flat_map(|ivk| {
// Reconstruct the matrix of (ivk, output) combinations.
iter::repeat(ivk)
.zip(ephemeral_keys.iter())
.zip(outputs.iter())
keys.chunks(ivks.len())
.zip(ephemeral_keys.iter().zip(outputs.iter()))
.map(|(key_chunk, ((_, ephemeral_key), (domain, output)))| {
key_chunk
.iter()
.zip(ivks.iter().enumerate())
.filter_map(|(key, (i, ivk))| {
key.as_ref()
.and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key))
.map(|out| (out, i))
})
.next()
})
.zip(keys)
.map(|(((ivk, (_, ephemeral_key)), (domain, output)), key)| {
// The `and_then` propagates any potential rejection from `D::epk`.
key.and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key))
})
.collect()
.collect::<Vec<Option<_>>>()
}

View File

@ -520,7 +520,7 @@ pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_S
let shared_secret = D::ka_agree_dec(ivk, &epk);
let key = D::kdf(shared_secret, &ephemeral_key);
try_note_decryption_inner(domain, ivk, &ephemeral_key, output, key)
try_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
}
fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
@ -528,7 +528,7 @@ fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT
ivk: &D::IncomingViewingKey,
ephemeral_key: &EphemeralKeyBytes,
output: &Output,
key: D::SymmetricKey,
key: &D::SymmetricKey,
) -> Option<(D::Note, D::Recipient, D::Memo)> {
let enc_ciphertext = output.enc_ciphertext();
@ -617,7 +617,7 @@ pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_
let shared_secret = D::ka_agree_dec(ivk, &epk);
let key = D::kdf(shared_secret, &ephemeral_key);
try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, key)
try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
}
fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
@ -625,7 +625,7 @@ fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPAC
ivk: &D::IncomingViewingKey,
ephemeral_key: &EphemeralKeyBytes,
output: &Output,
key: D::SymmetricKey,
key: &D::SymmetricKey,
) -> Option<(D::Note, D::Recipient)> {
// Start from block 1 to skip over Poly1305 keying output
let mut plaintext = [0; COMPACT_NOTE_SIZE];

View File

@ -1390,10 +1390,11 @@ mod tests {
)],
)[..]
{
[Some((decrypted_note, decrypted_to, decrypted_memo))] => {
[Some(((decrypted_note, decrypted_to, decrypted_memo), i))] => {
assert_eq!(decrypted_note, &note);
assert_eq!(decrypted_to, &to);
assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
assert_eq!(*i, 0);
}
_ => panic!("Note decryption failed"),
}
@ -1406,9 +1407,10 @@ mod tests {
)],
)[..]
{
[Some((decrypted_note, decrypted_to))] => {
[Some(((decrypted_note, decrypted_to), i))] => {
assert_eq!(decrypted_note, &note);
assert_eq!(decrypted_to, &to);
assert_eq!(*i, 0);
}
_ => panic!("Note decryption failed"),
}
@ -1450,22 +1452,22 @@ mod tests {
})
.collect();
let res = batch::try_note_decryption(&[invalid_ivk.clone(), valid_ivk.clone()], &outputs);
assert_eq!(res.len(), 20);
// The batched trial decryptions with invalid_ivk failed.
assert_eq!(&res[..10], &vec![None; 10][..]);
for (result, (_, output)) in res[10..].iter().zip(outputs.iter()) {
// Confirm that the outputs should indeed have failed with invalid_ivk
assert_eq!(
try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, output),
None
);
// Check that batched trial decryptions with invalid_ivk fails.
let res = batch::try_note_decryption(&[invalid_ivk.clone()], &outputs);
assert_eq!(res.len(), 10);
assert_eq!(&res[..], &vec![None; 10][..]);
// Check that batched trial decryptions with valid_ivk succeeds.
let res = batch::try_note_decryption(&[invalid_ivk, valid_ivk.clone()], &outputs);
assert_eq!(res.len(), 10);
for (result, (_, output)) in res.iter().zip(outputs.iter()) {
// Confirm the successful batched trial decryptions gave the same result.
// In all cases, the index of the valid ivk is returned.
assert!(result.is_some());
assert_eq!(
result,
&try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, output)
.map(|r| (r, 1))
);
}
}