diff --git a/CHANGELOG.md b/CHANGELOG.md index fed3804..0973bb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `sapling_crypto::keys`: + - `ProofAuthorizingKey` + - `impl From<&ProofAuthorizingKey> for NullifierDerivingKey` + +### Changed +- `sapling_crypto::keys`: + - `ExpandedSpendingKey.nsk` now has type `ProofAuthorizingKey`. + - `ProofGenerationKey.nsk` now has type `ProofAuthorizingKey`. + +### Removed +- `sapling_crypto::keys`: + - `ViewingKey` (use `FullViewingKey` instead). + ## [0.1.0] - 2024-01-26 The crate has been completely rewritten. See [`zcash/librustzcash`] for the history of this rewrite. diff --git a/src/circuit.rs b/src/circuit.rs index 611dc88..e164509 100644 --- a/src/circuit.rs +++ b/src/circuit.rs @@ -25,6 +25,9 @@ use self::constants::{ #[cfg(test)] use group::ff::PrimeFieldBits; +#[cfg(test)] +use crate::keys::ProofAuthorizingKey; + mod constants; mod ecc; mod pedersen_hash; @@ -190,7 +193,9 @@ impl Circuit for Spend { // Witness nsk as bits let nsk = boolean::field_into_boolean_vec_le( cs.namespace(|| "nsk"), - self.proof_generation_key.as_ref().map(|k| k.nsk), + self.proof_generation_key + .as_ref() + .map(|k| k.nsk.to_scalar()), )?; // NB: We don't ensure that the bit representation of nsk @@ -658,7 +663,7 @@ fn test_input_circuit_with_bls12_381() { let proof_generation_key = ProofGenerationKey { ak: SpendValidatingKey::fake_random(&mut rng), - nsk: jubjub::Fr::random(&mut rng), + nsk: ProofAuthorizingKey::random(&mut rng), }; let viewing_key = proof_generation_key.to_viewing_key(); @@ -833,7 +838,7 @@ fn test_input_circuit_with_bls12_381_external_test_vectors() { let proof_generation_key = ProofGenerationKey { ak: SpendValidatingKey::fake_random(&mut rng), - nsk: jubjub::Fr::random(&mut rng), + nsk: ProofAuthorizingKey::random(&mut rng), }; let viewing_key = proof_generation_key.to_viewing_key(); @@ -984,7 +989,7 @@ fn test_output_circuit_with_bls12_381() { randomness: jubjub::Fr::random(&mut rng), }; - let nsk = jubjub::Fr::random(&mut rng); + let nsk = ProofAuthorizingKey::random(&mut rng); let ak = SpendValidatingKey::fake_random(&mut rng); let proof_generation_key = ProofGenerationKey { ak, nsk }; diff --git a/src/keys.rs b/src/keys.rs index 06348c7..8a5a1e5 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -9,7 +9,7 @@ use std::io::{self, Read, Write}; use super::{ address::PaymentAddress, - constants::{self, PROOF_GENERATION_KEY_GENERATOR}, + constants::PROOF_GENERATION_KEY_GENERATOR, note_encryption::KDF_SAPLING_PERSONALIZATION, spec::{ crh_ivk, diversify_hash, ka_sapling_agree, ka_sapling_agree_prepared, @@ -203,6 +203,78 @@ impl SpendValidatingKey { } } +/// A proof authorizing key, used to create [Spend] proofs. +/// +/// $\mathsf{nsk}$ as defined in [Zcash Protocol Spec § 4.2.2: Sapling Key Components][saplingkeycomponents]. +/// +/// [Spend]: crate::circuit::Spend +/// [saplingkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents +#[derive(Clone)] +pub struct ProofAuthorizingKey(jubjub::Scalar); + +impl PartialEq for ProofAuthorizingKey { + fn eq(&self, other: &Self) -> bool { + self.0.ct_eq(&other.0).into() + } +} + +impl ProofAuthorizingKey { + /// Constructs a `ProofAuthorizingKey` from a raw scalar. + pub(crate) fn from_scalar(nsk: jubjub::Scalar) -> Self { + Self(nsk) + } + + /// For circuit tests only. + #[cfg(test)] + pub(crate) fn random(rng: R) -> Self { + Self::from_scalar(jubjub::Scalar::random(rng)) + } + + /// Derives a `ProofAuthorizingKey` from a spending key. + fn from_spending_key(sk: &[u8]) -> Self { + Self::from_scalar(jubjub::Scalar::from_bytes_wide( + &PrfExpand::SAPLING_NSK.with(sk), + )) + } + + /// Parses a `ProofAuthorizingKey` from its encoded form. + pub(crate) fn from_bytes(bytes: &[u8]) -> Option { + <[u8; 32]>::try_from(bytes) + .ok() + .and_then(|b| jubjub::Scalar::from_repr(b).into()) + .map(Self::from_scalar) + } + + /// Converts this proof authorizing key to its serialized form. + pub(crate) fn to_bytes(&self) -> [u8; 32] { + self.0.to_repr() + } + + /// Converts this spend authorizing key to a raw scalar. + /// + /// Only used for ZIP 32 child derivation and proof creation. + pub(crate) fn to_scalar(&self) -> jubjub::Scalar { + self.0 + } +} + +/// A key used to derive [`Nullifier`]s from [`Note`]s. +/// +/// $\mathsf{nk}$ as defined in [Zcash Protocol Spec § 4.2.2: Sapling Key Components][saplingkeycomponents]. +/// +/// [`Nullifier`]: crate::note::nullifier::Nullifier +/// [`Note`]: crate::note::Note +/// [saplingkeycomponents]: https://zips.z.cash/protocol/protocol.pdf#saplingkeycomponents +/// A key used to derive the nullifier for a Sapling note. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct NullifierDerivingKey(pub jubjub::SubgroupPoint); + +impl From<&ProofAuthorizingKey> for NullifierDerivingKey { + fn from(nsk: &ProofAuthorizingKey) -> Self { + NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * nsk.0) + } +} + /// An outgoing viewing key #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct OutgoingViewingKey(pub [u8; 32]); @@ -211,7 +283,7 @@ pub struct OutgoingViewingKey(pub [u8; 32]); #[derive(Clone)] pub struct ExpandedSpendingKey { pub ask: SpendAuthorizingKey, - pub nsk: jubjub::Fr, + pub nsk: ProofAuthorizingKey, pub ovk: OutgoingViewingKey, } @@ -232,7 +304,7 @@ impl ExpandedSpendingKey { pub fn from_spending_key(sk: &[u8]) -> Self { let ask = SpendAuthorizingKey::from_spending_key(sk).expect("negligible chance of ask == 0"); - let nsk = jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_NSK.with(sk)); + let nsk = ProofAuthorizingKey::from_spending_key(sk); let mut ovk = OutgoingViewingKey([0u8; 32]); ovk.0 .copy_from_slice(&PrfExpand::SAPLING_OVK.with(sk)[..32]); @@ -242,7 +314,7 @@ impl ExpandedSpendingKey { pub fn proof_generation_key(&self) -> ProofGenerationKey { ProofGenerationKey { ak: (&self.ask).into(), - nsk: self.nsk, + nsk: self.nsk.clone(), } } @@ -258,8 +330,7 @@ impl ExpandedSpendingKey { } let ask = SpendAuthorizingKey::from_bytes(&b[0..32]).ok_or(DecodingError::InvalidAsk)?; - let nsk = Option::from(jubjub::Fr::from_repr(b[32..64].try_into().unwrap())) - .ok_or(DecodingError::InvalidNsk)?; + let nsk = ProofAuthorizingKey::from_bytes(&b[32..64]).ok_or(DecodingError::InvalidNsk)?; let ovk = OutgoingViewingKey(b[64..96].try_into().unwrap()); Ok(ExpandedSpendingKey { ask, nsk, ovk }) @@ -291,7 +362,7 @@ impl ExpandedSpendingKey { pub fn to_bytes(&self) -> [u8; 96] { let mut result = [0u8; 96]; result[0..32].copy_from_slice(&self.ask.to_bytes()); - result[32..64].copy_from_slice(&self.nsk.to_repr()); + result[32..64].copy_from_slice(&self.nsk.to_bytes()); result[64..96].copy_from_slice(&self.ovk.0); result } @@ -300,7 +371,7 @@ impl ExpandedSpendingKey { #[derive(Clone)] pub struct ProofGenerationKey { pub ak: SpendValidatingKey, - pub nsk: jubjub::Fr, + pub nsk: ProofAuthorizingKey, } impl fmt::Debug for ProofGenerationKey { @@ -315,65 +386,43 @@ impl ProofGenerationKey { pub fn to_viewing_key(&self) -> ViewingKey { ViewingKey { ak: self.ak.clone(), - nk: NullifierDerivingKey(constants::PROOF_GENERATION_KEY_GENERATOR * self.nsk), + nk: (&self.nsk).into(), } } } -/// A key used to derive the nullifier for a Sapling note. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct NullifierDerivingKey(pub jubjub::SubgroupPoint); - -#[derive(Debug, Clone)] -pub struct ViewingKey { - pub ak: SpendValidatingKey, - pub nk: NullifierDerivingKey, -} - -impl ViewingKey { - pub fn rk(&self, ar: jubjub::Fr) -> redjubjub::VerificationKey { - self.ak.randomize(&ar) - } - - pub fn ivk(&self) -> SaplingIvk { - SaplingIvk(crh_ivk(self.ak.to_bytes(), self.nk.0.to_bytes())) - } - - pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { - self.ivk().to_payment_address(diversifier) - } -} - -/// A Sapling key that provides the capability to view incoming and outgoing transactions. -#[derive(Debug)] +/// A key that provides the capability to view incoming and outgoing transactions. +/// +/// Modern Zcash wallets use multiple viewing keys scoped to external and internal +/// operations. You should consider using [`DiversifiableFullViewingKey`] instead, which +/// handles these details and ensures that you have a full and consistent view of wallet +/// activity. +/// +/// Defined in [Zcash Protocol Spec § 3.1: Payment Addresses and Keys][addressesandkeys]. +/// +/// [`DiversifiableFullViewingKey`]: crate::zip32::DiversifiableFullViewingKey +/// [addressesandkeys]: https://zips.z.cash/protocol/protocol.pdf#addressesandkeys +// TODO: Rename to `ScopedFullViewingKey` or `ScopedViewingKey` for API clarity. +#[derive(Clone, Debug, PartialEq, Eq)] pub struct FullViewingKey { - pub vk: ViewingKey, - pub ovk: OutgoingViewingKey, -} - -impl Clone for FullViewingKey { - fn clone(&self) -> Self { - FullViewingKey { - vk: ViewingKey { - ak: self.vk.ak.clone(), - nk: self.vk.nk, - }, - ovk: self.ovk, - } - } + ak: SpendValidatingKey, + nk: NullifierDerivingKey, + ovk: OutgoingViewingKey, } impl FullViewingKey { pub fn from_expanded_spending_key(expsk: &ExpandedSpendingKey) -> Self { FullViewingKey { - vk: ViewingKey { - ak: (&expsk.ask).into(), - nk: NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * expsk.nsk), - }, + ak: (&expsk.ask).into(), + nk: (&expsk.nsk).into(), ovk: expsk.ovk, } } + /// Parses a full viewing key from its "raw" encoding as specified in + /// [Zcash Protocol Spec § 5.6.3.3: Sapling Full Viewing Keys][saplingfullviewingkeyencoding]. + /// + /// [saplingfullviewingkeyencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingfullviewingkeyencoding pub fn read(mut reader: R) -> io::Result { let ak = { let mut buf = [0u8; 32]; @@ -404,25 +453,47 @@ impl FullViewingKey { reader.read_exact(&mut ovk)?; Ok(FullViewingKey { - vk: ViewingKey { ak, nk }, + ak, + nk, ovk: OutgoingViewingKey(ovk), }) } + /// Serializes the full viewing key as specified in + /// [Zcash Protocol Spec § 5.6.3.3: Sapling Full Viewing Keys][saplingfullviewingkeyencoding]. + /// + /// [saplingfullviewingkeyencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingfullviewingkeyencoding pub fn write(&self, mut writer: W) -> io::Result<()> { - writer.write_all(&self.vk.ak.to_bytes())?; - writer.write_all(&self.vk.nk.0.to_bytes())?; + writer.write_all(&self.ak.to_bytes())?; + writer.write_all(&self.nk.0.to_bytes())?; writer.write_all(&self.ovk.0)?; Ok(()) } + /// Serializes the full viewing key as specified in + /// [Zcash Protocol Spec § 5.6.3.3: Sapling Full Viewing Keys][saplingfullviewingkeyencoding]. + /// + /// [saplingfullviewingkeyencoding]: https://zips.z.cash/protocol/protocol.pdf#saplingfullviewingkeyencoding pub fn to_bytes(&self) -> [u8; 96] { let mut result = [0u8; 96]; self.write(&mut result[..]) .expect("should be able to serialize a FullViewingKey"); result } + + pub fn rk(&self, ar: jubjub::Fr) -> redjubjub::VerificationKey { + self.ak.randomize(&ar) + } + + /// Derives an `IncomingViewingKey` for this full viewing key. + pub fn ivk(&self) -> SaplingIvk { + SaplingIvk(crh_ivk(self.ak.to_bytes(), self.nk.0.to_bytes())) + } + + pub fn to_payment_address(&self, diversifier: Diversifier) -> Option { + self.ivk().to_payment_address(diversifier) + } } #[derive(Debug, Clone)] @@ -683,7 +754,7 @@ pub mod testing { prop_compose! { pub fn arb_incoming_viewing_key()(fvk in arb_full_viewing_key()) -> SaplingIvk { - fvk.vk.ivk() + fvk.ivk() } } } diff --git a/src/zip32.rs b/src/zip32.rs index b43be3e..5ff125d 100644 --- a/src/zip32.rs +++ b/src/zip32.rs @@ -14,7 +14,8 @@ use zip32::{ChainCode, ChildIndex, DiversifierIndex, Scope}; use std::io::{self, Read, Write}; use std::ops::AddAssign; -use super::{Diversifier, NullifierDerivingKey, PaymentAddress, ViewingKey}; +use super::{Diversifier, NullifierDerivingKey, PaymentAddress}; +use crate::keys::ProofAuthorizingKey; use crate::{ constants::PROOF_GENERATION_KEY_GENERATOR, keys::{ @@ -36,7 +37,7 @@ pub fn sapling_address( j: DiversifierIndex, ) -> Option { dk.diversifier(j) - .and_then(|d_j| fvk.vk.to_payment_address(d_j)) + .and_then(|d_j| fvk.to_payment_address(d_j)) } /// Search the diversifier space starting at diversifier index `j` for @@ -49,7 +50,7 @@ pub fn sapling_find_address( j: DiversifierIndex, ) -> Option<(DiversifierIndex, PaymentAddress)> { let (j, d_j) = dk.find_diversifier(j)?; - fvk.vk.to_payment_address(d_j).map(|addr| (j, addr)) + fvk.to_payment_address(d_j).map(|addr| (j, addr)) } /// Returns the payment address corresponding to the smallest valid diversifier @@ -92,16 +93,14 @@ pub fn sapling_derive_internal_fvk( jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_ZIP32_INTERNAL_NSK.with(i.as_bytes())); let r = PrfExpand::SAPLING_ZIP32_INTERNAL_DK_OVK.with(i.as_bytes()); // PROOF_GENERATION_KEY_GENERATOR = \mathcal{H}^Sapling - let nk_internal = NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * i_nsk + fvk.vk.nk.0); + let nk_internal = NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * i_nsk + fvk.nk.0); let dk_internal = DiversifierKey(r[..32].try_into().unwrap()); let ovk_internal = OutgoingViewingKey(r[32..].try_into().unwrap()); ( FullViewingKey { - vk: ViewingKey { - ak: fvk.vk.ak.clone(), - nk: nk_internal, - }, + ak: fvk.ak.clone(), + nk: nk_internal, ovk: ovk_internal, }, dk_internal, @@ -441,12 +440,12 @@ impl ExtendedSpendingKey { let mut nsk = jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_ZIP32_CHILD_I_NSK.with(i_l)); ask.add_assign(self.expsk.ask.to_scalar()); - nsk.add_assign(&self.expsk.nsk); + nsk.add_assign(self.expsk.nsk.to_scalar()); let ovk = derive_child_ovk(&self.expsk.ovk, i_l); ExpandedSpendingKey { ask: SpendAuthorizingKey::from_scalar(ask) .expect("negligible chance of ask == 0"), - nsk, + nsk: ProofAuthorizingKey::from_scalar(nsk), ovk, } }, @@ -478,7 +477,7 @@ impl ExtendedSpendingKey { let i_nsk = jubjub::Fr::from_bytes_wide(&PrfExpand::SAPLING_ZIP32_INTERNAL_NSK.with(i.as_bytes())); let r = PrfExpand::SAPLING_ZIP32_INTERNAL_DK_OVK.with(i.as_bytes()); - let nsk_internal = i_nsk + self.expsk.nsk; + let nsk_internal = ProofAuthorizingKey::from_scalar(i_nsk + self.expsk.nsk.to_scalar()); let dk_internal = DiversifierKey(r[..32].try_into().unwrap()); let ovk_internal = OutgoingViewingKey(r[32..].try_into().unwrap()); @@ -533,9 +532,7 @@ impl std::cmp::PartialEq for ExtendedFullViewingKey { && self.parent_fvk_tag == rhs.parent_fvk_tag && self.child_index == rhs.child_index && self.chain_code == rhs.chain_code - && self.fvk.vk.ak == rhs.fvk.vk.ak - && self.fvk.vk.nk == rhs.fvk.vk.nk - && self.fvk.ovk == rhs.fvk.ovk + && self.fvk == rhs.fvk && self.dk == rhs.dk } } @@ -708,16 +705,16 @@ impl DiversifiableFullViewingKey { /// This API is provided so that nullifiers for change notes can be correctly computed. pub fn to_nk(&self, scope: Scope) -> NullifierDerivingKey { match scope { - Scope::External => self.fvk.vk.nk, - Scope::Internal => self.derive_internal().fvk.vk.nk, + Scope::External => self.fvk.nk, + Scope::Internal => self.derive_internal().fvk.nk, } } /// Derives an incoming viewing key corresponding to this full viewing key. pub fn to_ivk(&self, scope: Scope) -> SaplingIvk { match scope { - Scope::External => self.fvk.vk.ivk(), - Scope::Internal => self.derive_internal().fvk.vk.ivk(), + Scope::External => self.fvk.ivk(), + Scope::Internal => self.derive_internal().fvk.ivk(), } } @@ -816,7 +813,7 @@ mod tests { use super::*; use super::{DiversifiableFullViewingKey, ExtendedSpendingKey}; - use ff::PrimeField; + use group::GroupEncoding; #[test] @@ -936,9 +933,7 @@ mod tests { // Check value -> bytes -> parsed round trip. let dfvk_bytes = dfvk.to_bytes(); let dfvk_parsed = DiversifiableFullViewingKey::from_bytes(&dfvk_bytes).unwrap(); - assert_eq!(dfvk_parsed.fvk.vk.ak, dfvk.fvk.vk.ak); - assert_eq!(dfvk_parsed.fvk.vk.nk, dfvk.fvk.vk.nk); - assert_eq!(dfvk_parsed.fvk.ovk, dfvk.fvk.ovk); + assert_eq!(dfvk_parsed.fvk, dfvk.fvk); assert_eq!(dfvk_parsed.dk, dfvk.dk); // Check bytes -> parsed -> bytes round trip. @@ -1626,7 +1621,7 @@ mod tests { for (xsk, tv) in xsks.iter().zip(test_vectors.iter()) { assert_eq!(xsk.expsk.ask.to_bytes(), tv.ask.unwrap()); - assert_eq!(xsk.expsk.nsk.to_repr().as_ref(), tv.nsk.unwrap()); + assert_eq!(xsk.expsk.nsk.to_bytes(), tv.nsk.unwrap()); assert_eq!(xsk.expsk.ovk.0, tv.ovk); assert_eq!(xsk.dk.0, tv.dk); @@ -1638,10 +1633,7 @@ mod tests { let internal_xsk = xsk.derive_internal(); assert_eq!(internal_xsk.expsk.ask.to_bytes(), tv.ask.unwrap()); - assert_eq!( - internal_xsk.expsk.nsk.to_repr().as_ref(), - tv.internal_nsk.unwrap() - ); + assert_eq!(internal_xsk.expsk.nsk.to_bytes(), tv.internal_nsk.unwrap()); assert_eq!(internal_xsk.expsk.ovk.0, tv.internal_ovk); assert_eq!(internal_xsk.dk.0, tv.internal_dk); @@ -1653,14 +1645,14 @@ mod tests { } for (xfvk, tv) in xfvks.iter().zip(test_vectors.iter()) { - assert_eq!(xfvk.fvk.vk.ak.to_bytes(), tv.ak); - assert_eq!(xfvk.fvk.vk.nk.0.to_bytes(), tv.nk); + assert_eq!(xfvk.fvk.ak.to_bytes(), tv.ak); + assert_eq!(xfvk.fvk.nk.0.to_bytes(), tv.nk); assert_eq!(xfvk.fvk.ovk.0, tv.ovk); assert_eq!(xfvk.dk.0, tv.dk); assert_eq!(xfvk.chain_code.as_bytes(), &tv.c); - assert_eq!(xfvk.fvk.vk.ivk().to_repr().as_ref(), tv.ivk); + assert_eq!(xfvk.fvk.ivk().to_repr().as_ref(), tv.ivk); let mut ser = vec![]; xfvk.write(&mut ser).unwrap(); @@ -1697,17 +1689,14 @@ mod tests { } let internal_xfvk = xfvk.derive_internal(); - assert_eq!(internal_xfvk.fvk.vk.ak.to_bytes(), tv.ak); - assert_eq!(internal_xfvk.fvk.vk.nk.0.to_bytes(), tv.internal_nk); + assert_eq!(internal_xfvk.fvk.ak.to_bytes(), tv.ak); + assert_eq!(internal_xfvk.fvk.nk.0.to_bytes(), tv.internal_nk); assert_eq!(internal_xfvk.fvk.ovk.0, tv.internal_ovk); assert_eq!(internal_xfvk.dk.0, tv.internal_dk); assert_eq!(internal_xfvk.chain_code.as_bytes(), &tv.c); - assert_eq!( - internal_xfvk.fvk.vk.ivk().to_repr().as_ref(), - tv.internal_ivk - ); + assert_eq!(internal_xfvk.fvk.ivk().to_repr().as_ref(), tv.internal_ivk); let mut ser = vec![]; internal_xfvk.write(&mut ser).unwrap();