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:
parent
515b0a40ec
commit
20e869f501
|
@ -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" }
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue