From d8c193babf81d12997d0c6534bbae837650b9a72 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Sat, 11 Jul 2026 09:41:53 +0200 Subject: [PATCH] Add BatchDomain::batch_ka_agree_dec, a per-viewing-key batched agreement hook Trial decryption computes ka_agree_dec for many ephemeral keys against the same incoming viewing key. For domains where same-scalar multiplications can share work (for example, lockstep ladders over a shared batched field inversion), this dominates the cost of batched trial decryption, but the per-item Domain::ka_agree_dec interface gives implementations no opportunity to exploit it. This adds a provided BatchDomain method that computes the agreements for a whole batch against one viewing key, defaulting to the per-item computation, and routes the batch module's shared-secret derivation through it (one call per viewing key, preserving the output-major order the downstream batch KDF expects). Existing BatchDomain implementations are unaffected. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 7 +++++++ src/batch.rs | 38 +++++++++++++++++++++++++++++--------- src/lib.rs | 23 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce54830..285ffc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this library adheres to Rust's notion of ### Added - A blanket `impl ShieldedOutput for &O` where `D: Domain, O: ShieldedOutput` +- `BatchDomain::batch_ka_agree_dec`, a provided method that computes + `Domain::ka_agree_dec` for a batch of prepared ephemeral keys against a single + incoming viewing key. Domains for which same-scalar multiplications can share + work can override it; the default implementation (and therefore the behavior + of existing `BatchDomain` implementations) is the per-item computation. The + batch decryption functions in the `batch` module now derive their shared + secrets through this method, one call per viewing key. ### Changed - **Breaking change:** removed the constants `COMPACT_NOTE_SIZE`, diff --git a/src/batch.rs b/src/batch.rs index 08f3ea2..c61c19f 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -54,16 +54,36 @@ where // 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). - // The scalar multiplications cannot benefit from batching. - 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, - ) + // Derive the shared secrets for all combinations of (ivk, output), one batched + // same-key agreement per ivk: domains for which same-scalar multiplications can + // share work accelerate here, and the default `batch_ka_agree_dec` implementation + // is exactly the previous per-item computation. + // Reassembly below is in the (output-major, ivk-minor) order the batch-KDF + // expects, moving values out of the per-ivk columns (`SharedSecret` need not be + // `Clone`). + let mut columns: Vec<_> = ivks + .iter() + .map(|ivk| { + D::batch_ka_agree_dec(ivk, ephemeral_keys.iter().map(|(epk, _)| epk.as_ref())) + .into_iter() }) - }); + .collect(); + let mut secrets: Vec> = + Vec::with_capacity(ephemeral_keys.len() * ivks.len()); + for _ in 0..ephemeral_keys.len() { + for column in columns.iter_mut() { + secrets.push( + column + .next() + .expect("all columns have one entry per output"), + ); + } + } + let items = secrets.into_iter().zip( + ephemeral_keys + .iter() + .flat_map(|(_, ephemeral_key)| core::iter::repeat(ephemeral_key).take(ivks.len())), + ); // Run the batch-KDF to obtain the symmetric keys from the shared secrets. let keys = D::batch_kdf(items); diff --git a/src/lib.rs b/src/lib.rs index 23d5563..b253530 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -349,6 +349,29 @@ pub trait BatchDomain: Domain { }) .collect() } + + /// Computes `Self::ka_agree_dec` on a batch of prepared ephemeral keys against a + /// single incoming viewing key. + /// + /// For each item, if the prepared ephemeral key is `None` (i.e. its encoding could + /// not be parsed), this returns `None` at that position. + /// + /// Trial decryption multiplies many ephemeral keys by the same viewing key, so + /// domains for which same-scalar multiplications can share work (for example, + /// lockstep ladders over a shared batched field inversion) can override this to + /// reduce the cost of the scalar multiplications, which dominate batched trial + /// decryption. The default implementation performs the per-item computation. + fn batch_ka_agree_dec<'a>( + ivk: &Self::IncomingViewingKey, + epks: impl Iterator>, + ) -> Vec> + where + Self::PreparedEphemeralPublicKey: 'a, + { + // Default implementation: do the non-batched thing. + epks.map(|epk| epk.map(|epk| Self::ka_agree_dec(ivk, epk))) + .collect() + } } /// Trait that provides access to the components of an encrypted transaction output.