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]
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

@ -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));

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);