zcash_primitives: Use prepared epk and ivk in Sapling note decryption

Co-authored-by: Jack Grigg <jack@electriccoin.co>
Signed-off-by: Daira Hopwood <daira@jacaranda.org>
This commit is contained in:
Daira Hopwood 2022-09-12 22:54:09 +01:00
parent 515b0a40ec
commit 20e869f501
7 changed files with 99 additions and 36 deletions

View File

@ -21,4 +21,5 @@ codegen-units = 1
[patch.crates-io] [patch.crates-io]
zcash_encoding = { path = "components/zcash_encoding" } zcash_encoding = { path = "components/zcash_encoding" }
zcash_note_encryption = { path = "components/zcash_note_encryption" } 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" } group = { git = "https://github.com/zkcrypto/group.git", rev = "a7f3ceb2373e9fe536996f7b4d55c797f3e667f0" }

View File

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

View File

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

View File

@ -11,7 +11,7 @@ use zcash_primitives::{
sapling::{ sapling::{
self, self,
keys::{DiversifiableFullViewingKey, Scope}, keys::{DiversifiableFullViewingKey, Scope},
note_encryption::SaplingDomain, note_encryption::{PreparedIncomingViewingKey, SaplingDomain},
Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk, Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk,
}, },
transaction::components::sapling::CompactOutputDescription, transaction::components::sapling::CompactOutputDescription,
@ -325,7 +325,8 @@ pub(crate) fn scan_block_with_runner<P: consensus::Parameters + Send + 'static,
let ivks = vks let ivks = vks
.iter() .iter()
.map(|(_, ivk, _)| (*ivk).clone()) .map(|(_, ivk, _)| ivk)
.map(PreparedIncomingViewingKey::new)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
batch::try_compact_note_decryption(&ivks, decoded) batch::try_compact_note_decryption(&ivks, decoded)
@ -413,8 +414,9 @@ mod tests {
memo::MemoBytes, memo::MemoBytes,
merkle_tree::CommitmentTree, merkle_tree::CommitmentTree,
sapling::{ sapling::{
note_encryption::sapling_note_encryption, util::generate_random_rseed, Note, Nullifier, note_encryption::{sapling_note_encryption, PreparedIncomingViewingKey},
SaplingIvk, util::generate_random_rseed,
Note, Nullifier, SaplingIvk,
}, },
transaction::components::Amount, transaction::components::Amount,
zip32::{AccountId, ExtendedFullViewingKey, ExtendedSpendingKey}, zip32::{AccountId, ExtendedFullViewingKey, ExtendedSpendingKey},
@ -557,7 +559,8 @@ mod tests {
extfvk extfvk
.to_sapling_keys() .to_sapling_keys()
.iter() .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); add_block_to_runner(&Network::TestNetwork, cb.clone(), &mut runner);
@ -620,7 +623,8 @@ mod tests {
extfvk extfvk
.to_sapling_keys() .to_sapling_keys()
.iter() .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); 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` - `Scope`
- `ExpandedSpendingKey::from_bytes` - `ExpandedSpendingKey::from_bytes`
- `ExtendedSpendingKey::{from_bytes, to_bytes}` - `ExtendedSpendingKey::{from_bytes, to_bytes}`
- `zcash_primitives::sapling::note_encryption`:
- `PreparedIncomingViewingKey`
- `PreparedEphemeralPublicKey`
- Added in `zcash_primitives::zip32` - Added in `zcash_primitives::zip32`
- `ChainCode::as_bytes` - `ChainCode::as_bytes`
- `DiversifierKey::{from_bytes, as_bytes}` - `DiversifierKey::{from_bytes, as_bytes}`
@ -35,6 +38,7 @@ and this library adheres to Rust's notion of
- `Bundle::value_balance` - `Bundle::value_balance`
### Changed ### Changed
- Migrated to `group 0.13`.
- `zcash_primitives::sapling::ViewingKey` now stores `nk` as a - `zcash_primitives::sapling::ViewingKey` now stores `nk` as a
`NullifierDerivingKey` instead of as a bare `jubjub::SubgroupPoint`. `NullifierDerivingKey` instead of as a bare `jubjub::SubgroupPoint`.
- The signature of `zcash_primitives::sapling::Note::nf` has changed to - 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. rather than the full `ViewingKey` as its first argument.
- Made the internals of `zip32::DiversifierKey` private; use `from_bytes` and - Made the internals of `zip32::DiversifierKey` private; use `from_bytes` and
`as_bytes` on this type instead. `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 ## [0.7.0] - 2022-06-24
### Changed ### Changed

View File

@ -11,7 +11,7 @@ use zcash_primitives::{
sapling::{ sapling::{
note_encryption::{ note_encryption::{
sapling_note_encryption, try_sapling_compact_note_decryption, sapling_note_encryption, try_sapling_compact_note_decryption,
try_sapling_note_decryption, SaplingDomain, try_sapling_note_decryption, PreparedIncomingViewingKey, SaplingDomain,
}, },
util::generate_random_rseed, util::generate_random_rseed,
Diversifier, PaymentAddress, SaplingIvk, ValueCommitment, 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"); let mut group = c.benchmark_group("sapling-note-decryption");
group.throughput(Throughput::Elements(1)); group.throughput(Throughput::Elements(1));

View File

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