diff --git a/CHANGELOG.md b/CHANGELOG.md index b1887d89..3ebdf382 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,21 @@ and this project adheres to Rust's notion of ### Added - `orchard::Proof::add_to_batch` - `orchard::address::Address::diversifier` -- `orchard::keys::Diversifier::from_bytes` +- `orchard::keys:`: + - `Diversifier::from_bytes` + - `PreparedEphemeralPublicKey` + - `PreparedIncomingViewingKey` - `orchard::note`: - `RandomSeed` - `Note::{from_parts, rseed}` + - `impl memuse::DynamicUsage for Nullifier` +- `orchard::note_encryption`: + - `impl memuse::DynamicUsage for OrchardDomain` - `orchard::builder::SpendInfo::new` - `orchard::circuit::Circuit::from_action_context` +- impls of `Eq` for: + - `orchard::zip32::ChildIndex` + - `orchard::value::ValueSum` ### Changed - Migrated to `zcash_note_encryption 0.2`. diff --git a/Cargo.toml b/Cargo.toml index a870ff45..813b0aee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,12 +28,12 @@ bitvec = "1" blake2b_simd = "1" ff = "0.12" fpe = "0.5" -group = "0.12" +group = { version = "0.12.1", features = ["wnaf-memuse"] } halo2_gadgets = "0.2" halo2_proofs = "0.2" hex = "0.4" lazy_static = "1" -memuse = { version = "0.2", features = ["nonempty"] } +memuse = { version = "0.2.1", features = ["nonempty"] } pasta_curves = "0.4" proptest = { version = "1.0.0", optional = true } rand = "0.8" diff --git a/benches/note_decryption.rs b/benches/note_decryption.rs index cab432fe..3ebbc6ed 100644 --- a/benches/note_decryption.rs +++ b/benches/note_decryption.rs @@ -3,7 +3,7 @@ use orchard::{ builder::Builder, bundle::Flags, circuit::ProvingKey, - keys::{FullViewingKey, Scope, SpendingKey}, + keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendingKey}, note_encryption::{CompactAction, OrchardDomain}, value::NoteValue, Anchor, Bundle, @@ -21,6 +21,7 @@ fn bench_note_decryption(c: &mut Criterion) { let fvk = FullViewingKey::from(&SpendingKey::from_bytes([7; 32]).unwrap()); let valid_ivk = fvk.to_ivk(Scope::External); let recipient = valid_ivk.address_at(0u32); + let valid_ivk = PreparedIncomingViewingKey::new(&valid_ivk); // Compact actions don't have the full AEAD ciphertext, so ZIP 307 trial-decryption // relies on an invalid ivk resulting in random noise for which the note commitment @@ -31,15 +32,15 @@ fn bench_note_decryption(c: &mut Criterion) { // // Our fixed (action, invalid ivk) tuple will always fall into a specific rejection // case. In order to reflect the real behaviour in the benchmarks, we trial-decrypt - // with 1000 invalid ivks (each of which will result in a different uniformly-random - // plaintext); this is equivalent to trial-decrypting 1000 different actions with the + // with 10240 invalid ivks (each of which will result in a different uniformly-random + // plaintext); this is equivalent to trial-decrypting 10240 different actions with the // same ivk, but is faster to set up. - let invalid_ivks: Vec<_> = (0u32..1000) + let invalid_ivks: Vec<_> = (0u32..10240) .map(|i| { let mut sk = [0; 32]; sk[..4].copy_from_slice(&i.to_le_bytes()); let fvk = FullViewingKey::from(&SpendingKey::from_bytes(sk).unwrap()); - fvk.to_ivk(Scope::External) + PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::External)) }) .collect(); diff --git a/src/bundle.rs b/src/bundle.rs index c70ca635..239cce7c 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -17,7 +17,7 @@ use crate::{ address::Address, bundle::commitments::{hash_bundle_auth_data, hash_bundle_txid_data}, circuit::{Instance, Proof, VerifyingKey}, - keys::{IncomingViewingKey, OutgoingViewingKey}, + keys::{IncomingViewingKey, OutgoingViewingKey, PreparedIncomingViewingKey}, note::Note, note_encryption::OrchardDomain, primitives::redpallas::{self, Binding, SpendAuth}, @@ -284,14 +284,18 @@ impl Bundle { &self, keys: &[IncomingViewingKey], ) -> Vec<(usize, IncomingViewingKey, Note, Address, [u8; 512])> { + let prepared_keys: Vec<_> = keys + .iter() + .map(|ivk| (ivk, PreparedIncomingViewingKey::new(ivk))) + .collect(); self.actions .iter() .enumerate() .filter_map(|(idx, action)| { let domain = OrchardDomain::for_action(action); - keys.iter().find_map(move |ivk| { - try_note_decryption(&domain, ivk, action) - .map(|(n, a, m)| (idx, ivk.clone(), n, a, m)) + prepared_keys.iter().find_map(|(ivk, prepared_ivk)| { + try_note_decryption(&domain, prepared_ivk, action) + .map(|(n, a, m)| (idx, (*ivk).clone(), n, a, m)) }) }) .collect() @@ -305,9 +309,10 @@ impl Bundle { action_idx: usize, key: &IncomingViewingKey, ) -> Option<(Note, Address, [u8; 512])> { + let prepared_ivk = PreparedIncomingViewingKey::new(key); self.actions.get(action_idx).and_then(move |action| { let domain = OrchardDomain::for_action(action); - try_note_decryption(&domain, key, action) + try_note_decryption(&domain, &prepared_ivk, action) }) } diff --git a/src/keys.rs b/src/keys.rs index e277264d..a7856f91 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -20,8 +20,9 @@ use crate::{ address::Address, primitives::redpallas::{self, SpendAuth}, spec::{ - commit_ivk, diversify_hash, extract_p, ka_orchard, prf_nf, to_base, to_scalar, - NonIdentityPallasPoint, NonZeroPallasBase, NonZeroPallasScalar, PrfExpand, + commit_ivk, diversify_hash, extract_p, ka_orchard, ka_orchard_prepared, prf_nf, to_base, + to_scalar, NonIdentityPallasPoint, NonZeroPallasBase, NonZeroPallasScalar, + PreparedNonIdentityBase, PreparedNonZeroScalar, PrfExpand, }, zip32::{self, ChildIndex, ExtendedSpendingKey}, }; @@ -627,7 +628,8 @@ impl KeyAgreementPrivateKey { /// Returns the payment address for this key corresponding to the given diversifier. fn address(&self, d: Diversifier) -> Address { - let pk_d = DiversifiedTransmissionKey::derive_inner(self, &d); + let prepared_ivk = PreparedIncomingViewingKey::new_inner(self); + let pk_d = DiversifiedTransmissionKey::derive(&prepared_ivk, &d); Address::from_parts(d, pk_d) } } @@ -704,6 +706,32 @@ impl IncomingViewingKey { } } +/// An Orchard incoming viewing key that has been precomputed for trial decryption. +#[derive(Clone, Debug)] +pub struct PreparedIncomingViewingKey(PreparedNonZeroScalar); + +impl memuse::DynamicUsage for PreparedIncomingViewingKey { + fn dynamic_usage(&self) -> usize { + self.0.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + self.0.dynamic_usage_bounds() + } +} + +impl PreparedIncomingViewingKey { + /// Performs the necessary precomputations to use an `IncomingViewingKey` for note + /// decryption. + pub fn new(ivk: &IncomingViewingKey) -> Self { + Self::new_inner(&ivk.ivk) + } + + fn new_inner(ivk: &KeyAgreementPrivateKey) -> Self { + Self(PreparedNonZeroScalar::new(&ivk.0)) + } +} + /// A key that provides the capability to recover outgoing transaction information from /// the block chain. /// @@ -753,13 +781,9 @@ impl DiversifiedTransmissionKey { /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents - pub(crate) fn derive(ivk: &IncomingViewingKey, d: &Diversifier) -> Self { - Self::derive_inner(&ivk.ivk, d) - } - - fn derive_inner(ivk: &KeyAgreementPrivateKey, d: &Diversifier) -> Self { - let g_d = diversify_hash(d.as_array()); - DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d)) + pub(crate) fn derive(ivk: &PreparedIncomingViewingKey, d: &Diversifier) -> Self { + let g_d = PreparedNonIdentityBase::new(diversify_hash(d.as_array())); + DiversifiedTransmissionKey(ka_orchard_prepared(&ivk.0, &g_d)) } /// $abst_P(bytes)$ @@ -841,6 +865,20 @@ impl EphemeralPublicKey { } } +/// An Orchard ephemeral public key that has been precomputed for trial decryption. +#[derive(Clone, Debug)] +pub struct PreparedEphemeralPublicKey(PreparedNonIdentityBase); + +impl PreparedEphemeralPublicKey { + pub(crate) fn new(epk: EphemeralPublicKey) -> Self { + PreparedEphemeralPublicKey(PreparedNonIdentityBase::new(epk.0)) + } + + pub(crate) fn agree(&self, ivk: &PreparedIncomingViewingKey) -> SharedSecret { + SharedSecret(ka_orchard_prepared(&ivk.0, &self.0)) + } +} + /// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{SharedSecret} := \mathbb{P}^{\ast}$ /// /// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement]. diff --git a/src/note/nullifier.rs b/src/note/nullifier.rs index 74efe5cd..591ea655 100644 --- a/src/note/nullifier.rs +++ b/src/note/nullifier.rs @@ -1,5 +1,6 @@ use group::{ff::PrimeField, Group}; use halo2_proofs::arithmetic::CurveExt; +use memuse::DynamicUsage; use pasta_curves::pallas; use rand::RngCore; use subtle::CtOption; @@ -14,6 +15,9 @@ use crate::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Nullifier(pub(crate) pallas::Base); +// We know that `pallas::Base` doesn't allocate internally. +memuse::impl_no_dynamic_usage!(Nullifier); + impl Nullifier { /// Generates a dummy nullifier for use as $\rho$ in dummy spent notes. /// diff --git a/src/note_encryption.rs b/src/note_encryption.rs index 0b249eec..8ad06c8a 100644 --- a/src/note_encryption.rs +++ b/src/note_encryption.rs @@ -14,7 +14,7 @@ use crate::{ action::Action, keys::{ DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey, - IncomingViewingKey, OutgoingViewingKey, SharedSecret, + OutgoingViewingKey, PreparedEphemeralPublicKey, PreparedIncomingViewingKey, SharedSecret, }, note::{ExtractedNoteCommitment, Nullifier, RandomSeed}, spec::diversify_hash, @@ -85,6 +85,16 @@ pub struct OrchardDomain { rho: Nullifier, } +impl memuse::DynamicUsage for OrchardDomain { + fn dynamic_usage(&self) -> usize { + self.rho.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + self.rho.dynamic_usage_bounds() + } +} + impl OrchardDomain { /// Constructs a domain that can be used to trial-decrypt this action's output note. pub fn for_action(act: &Action) -> Self { @@ -102,13 +112,13 @@ impl OrchardDomain { impl Domain for OrchardDomain { type EphemeralSecretKey = EphemeralSecretKey; type EphemeralPublicKey = EphemeralPublicKey; - type PreparedEphemeralPublicKey = EphemeralPublicKey; + type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey; type SharedSecret = SharedSecret; type SymmetricKey = Hash; type Note = Note; type Recipient = Address; type DiversifiedTransmissionKey = DiversifiedTransmissionKey; - type IncomingViewingKey = IncomingViewingKey; + type IncomingViewingKey = PreparedIncomingViewingKey; type OutgoingViewingKey = OutgoingViewingKey; type ValueCommitment = ValueCommitment; type ExtractedCommitment = ExtractedNoteCommitment; @@ -124,7 +134,7 @@ impl Domain for OrchardDomain { } fn prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey { - epk + PreparedEphemeralPublicKey::new(epk) } fn ka_derive_public( @@ -352,7 +362,7 @@ mod tests { action::Action, keys::{ DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey, - OutgoingViewingKey, + OutgoingViewingKey, PreparedIncomingViewingKey, }, note::{ExtractedNoteCommitment, Nullifier, RandomSeed, TransmittedNoteCiphertext}, primitives::redpallas, @@ -370,7 +380,9 @@ mod tests { // // Recipient key material - let ivk = IncomingViewingKey::from_bytes(&tv.incoming_viewing_key).unwrap(); + let ivk = PreparedIncomingViewingKey::new( + &IncomingViewingKey::from_bytes(&tv.incoming_viewing_key).unwrap(), + ); let ovk = OutgoingViewingKey::from(tv.ovk); let d = Diversifier::from_bytes(tv.default_d); let pk_d = DiversifiedTransmissionKey::from_bytes(&tv.default_pk_d).unwrap(); diff --git a/src/spec.rs b/src/spec.rs index 67f7a7c4..3af8b97d 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -4,10 +4,10 @@ use core::iter; use core::ops::Deref; use ff::{Field, PrimeField, PrimeFieldBits}; -use group::GroupEncoding; -use group::{Curve, Group}; +use group::{Curve, Group, GroupEncoding, WnafBase, WnafScalar}; use halo2_gadgets::{poseidon::primitives as poseidon, sinsemilla::primitives as sinsemilla}; use halo2_proofs::arithmetic::{CurveAffine, CurveExt, FieldExt}; +use memuse::DynamicUsage; use pasta_curves::pallas; use subtle::{ConditionallySelectable, CtOption}; @@ -140,6 +140,36 @@ impl Deref for NonZeroPallasScalar { } } +const PREPARED_WINDOW_SIZE: usize = 4; + +#[derive(Clone, Debug)] +pub(crate) struct PreparedNonIdentityBase(WnafBase); + +impl PreparedNonIdentityBase { + pub(crate) fn new(base: NonIdentityPallasPoint) -> Self { + PreparedNonIdentityBase(WnafBase::new(base.0)) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PreparedNonZeroScalar(WnafScalar); + +impl DynamicUsage for PreparedNonZeroScalar { + fn dynamic_usage(&self) -> usize { + self.0.dynamic_usage() + } + + fn dynamic_usage_bounds(&self) -> (usize, Option) { + self.0.dynamic_usage_bounds() + } +} + +impl PreparedNonZeroScalar { + pub(crate) fn new(scalar: &NonZeroPallasScalar) -> Self { + PreparedNonZeroScalar(WnafScalar::new(scalar)) + } +} + /// $\mathsf{ToBase}^\mathsf{Orchard}(x) := LEOS2IP_{\ell_\mathsf{PRFexpand}}(x) (mod q_P)$ /// /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. @@ -213,8 +243,20 @@ pub(crate) fn ka_orchard( sk: &NonZeroPallasScalar, b: &NonIdentityPallasPoint, ) -> NonIdentityPallasPoint { - let mut wnaf = group::Wnaf::new(); - NonIdentityPallasPoint(wnaf.scalar(sk.deref()).base(*b.deref())) + ka_orchard_prepared( + &PreparedNonZeroScalar::new(sk), + &PreparedNonIdentityBase::new(*b), + ) +} + +/// Defined in [Zcash Protocol Spec § 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement]. +/// +/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement +pub(crate) fn ka_orchard_prepared( + sk: &PreparedNonZeroScalar, + b: &PreparedNonIdentityBase, +) -> NonIdentityPallasPoint { + NonIdentityPallasPoint(&b.0 * &sk.0) } /// Coordinate extractor for Pallas. diff --git a/src/value.rs b/src/value.rs index 8945ef9d..1cef61c8 100644 --- a/src/value.rs +++ b/src/value.rs @@ -144,7 +144,7 @@ pub(crate) enum Sign { } /// A sum of Orchard note values. -#[derive(Clone, Copy, Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ValueSum(i128); impl ValueSum { diff --git a/src/zip32.rs b/src/zip32.rs index ab0d30b3..3ba8bd57 100644 --- a/src/zip32.rs +++ b/src/zip32.rs @@ -65,7 +65,7 @@ impl FvkTag { } /// A hardened child index for a derived key. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ChildIndex(u32); impl TryFrom for ChildIndex { diff --git a/tests/builder.rs b/tests/builder.rs index 35c53976..f9b0f271 100644 --- a/tests/builder.rs +++ b/tests/builder.rs @@ -3,7 +3,7 @@ use orchard::{ builder::Builder, bundle::{Authorized, Flags}, circuit::{ProvingKey, VerifyingKey}, - keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}, + keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey}, note::ExtractedNoteCommitment, note_encryption::OrchardDomain, tree::{MerkleHashOrchard, MerklePath}, @@ -57,7 +57,7 @@ fn bundle_chain() { // Create a shielded bundle spending the previous output. let shielded_bundle: Bundle<_, i64> = { - let ivk = fvk.to_ivk(Scope::External); + let ivk = PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::External)); let (note, _, _) = shielding_bundle .actions() .iter()