diff --git a/zcash_primitives/src/sapling/note.rs b/zcash_primitives/src/sapling/note.rs index fa652c4aa..34237387a 100644 --- a/zcash_primitives/src/sapling/note.rs +++ b/zcash_primitives/src/sapling/note.rs @@ -1,15 +1,14 @@ -use byteorder::{LittleEndian, WriteBytesExt}; use group::{ ff::{Field, PrimeField}, - Curve, GroupEncoding, + GroupEncoding, }; use rand_core::{CryptoRng, RngCore}; -use super::{ - pedersen_hash::{pedersen_hash, Personalization}, - Node, Nullifier, NullifierDerivingKey, -}; -use crate::{constants, keys::prf_expand}; +use super::{value::NoteValue, Node, Nullifier, NullifierDerivingKey}; +use crate::keys::prf_expand; + +mod commitment; +pub use self::commitment::{ExtractedNoteCommitment, NoteCommitment}; pub(super) mod nullifier; @@ -24,6 +23,20 @@ pub enum Rseed { AfterZip212([u8; 32]), } +impl Rseed { + /// Defined in [Zcash Protocol Spec § 4.7.2: Sending Notes (Sapling)][saplingsend]. + /// + /// [saplingsend]: https://zips.z.cash/protocol/protocol.pdf#saplingsend + pub(crate) fn rcm(&self) -> commitment::NoteCommitTrapdoor { + commitment::NoteCommitTrapdoor(match self { + Rseed::BeforeZip212(rcm) => *rcm, + Rseed::AfterZip212(rseed) => { + jubjub::Fr::from_bytes_wide(prf_expand(rseed, &[0x04]).as_array()) + } + }) + } +} + #[derive(Clone, Debug)] pub struct Note { /// The value of the note @@ -53,31 +66,13 @@ impl Note { } /// Computes the note commitment, returning the full point. - fn cm_full_point(&self) -> jubjub::SubgroupPoint { - // Calculate the note contents, as bytes - let mut note_contents = vec![]; - - // Writing the value in little endian - note_contents.write_u64::(self.value).unwrap(); - - // Write g_d - note_contents.extend_from_slice(&self.g_d.to_bytes()); - - // Write pk_d - note_contents.extend_from_slice(&self.pk_d.to_bytes()); - - assert_eq!(note_contents.len(), 32 + 32 + 8); - - // Compute the Pedersen hash of the note contents - let hash_of_contents = pedersen_hash( - Personalization::NoteCommitment, - note_contents - .into_iter() - .flat_map(|byte| (0..8).map(move |i| ((byte >> i) & 1) == 1)), - ); - - // Compute final commitment - (constants::NOTE_COMMITMENT_RANDOMNESS_GENERATOR * self.rcm()) + hash_of_contents + fn cm_full_point(&self) -> NoteCommitment { + NoteCommitment::derive( + self.g_d.to_bytes(), + self.pk_d.to_bytes(), + NoteValue::from_raw(self.value), + self.rseed.rcm(), + ) } /// Computes the nullifier given the nullifier deriving key and @@ -88,20 +83,12 @@ impl Note { /// Computes the note commitment pub fn cmu(&self) -> bls12_381::Scalar { - // The commitment is in the prime order subgroup, so mapping the - // commitment to the u-coordinate is an injective encoding. - jubjub::ExtendedPoint::from(self.cm_full_point()) - .to_affine() - .get_u() + // TODO: Expose typed representation. + ExtractedNoteCommitment::from(self.cm_full_point()).inner() } pub fn rcm(&self) -> jubjub::Fr { - match self.rseed { - Rseed::BeforeZip212(rcm) => rcm, - Rseed::AfterZip212(rseed) => { - jubjub::Fr::from_bytes_wide(prf_expand(&rseed, &[0x04]).as_array()) - } - } + self.rseed.rcm().0 } pub fn generate_or_derive_esk(&self, rng: &mut R) -> jubjub::Fr { diff --git a/zcash_primitives/src/sapling/note/commitment.rs b/zcash_primitives/src/sapling/note/commitment.rs new file mode 100644 index 000000000..bde236ef8 --- /dev/null +++ b/zcash_primitives/src/sapling/note/commitment.rs @@ -0,0 +1,101 @@ +use core::iter; + +use bitvec::{array::BitArray, order::Lsb0}; +use group::ff::PrimeField; +use subtle::{ConstantTimeEq, CtOption}; + +use crate::sapling::{ + pedersen_hash::Personalization, + spec::{extract_p, windowed_pedersen_commit}, + value::NoteValue, +}; + +/// The trapdoor for a Sapling note commitment. +#[derive(Clone, Debug)] +pub(crate) struct NoteCommitTrapdoor(pub(super) jubjub::Fr); + +/// A commitment to a note. +#[derive(Clone, Debug)] +pub struct NoteCommitment(jubjub::SubgroupPoint); + +impl NoteCommitment { + pub(crate) fn inner(&self) -> jubjub::SubgroupPoint { + self.0 + } +} + +impl NoteCommitment { + /// $NoteCommit^Sapling$. + /// + /// Defined in [Zcash Protocol Spec § 5.4.8.2: Windowed Pedersen commitments][concretewindowedcommit]. + /// + /// [concretewindowedcommit]: https://zips.z.cash/protocol/protocol.pdf#concretewindowedcommit + pub(super) fn derive( + g_d: [u8; 32], + pk_d: [u8; 32], + v: NoteValue, + rcm: NoteCommitTrapdoor, + ) -> Self { + NoteCommitment(windowed_pedersen_commit( + Personalization::NoteCommitment, + iter::empty() + .chain(v.to_le_bits().iter().by_vals()) + .chain(BitArray::<_, Lsb0>::new(g_d).iter().by_vals()) + .chain(BitArray::<_, Lsb0>::new(pk_d).iter().by_vals()), + rcm.0, + )) + } +} + +/// The u-coordinate of the commitment to a note. +#[derive(Copy, Clone, Debug)] +pub struct ExtractedNoteCommitment(pub(super) bls12_381::Scalar); + +impl ExtractedNoteCommitment { + /// Deserialize the extracted note commitment from a byte array. + /// + /// This method enforces the [consensus rule][cmucanon] that the byte representation + /// of cmu MUST be canonical. + /// + /// [cmucanon]: https://zips.z.cash/protocol/protocol.pdf#outputencodingandconsensus + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + bls12_381::Scalar::from_repr(*bytes).map(ExtractedNoteCommitment) + } + + /// Serialize the value commitment to its canonical byte representation. + pub fn to_bytes(self) -> [u8; 32] { + self.0.to_repr() + } +} + +impl From for ExtractedNoteCommitment { + fn from(cm: NoteCommitment) -> Self { + ExtractedNoteCommitment(extract_p(&cm.0)) + } +} + +impl ExtractedNoteCommitment { + pub(crate) fn inner(&self) -> bls12_381::Scalar { + self.0 + } +} + +impl From<&ExtractedNoteCommitment> for [u8; 32] { + fn from(cmu: &ExtractedNoteCommitment) -> Self { + cmu.to_bytes() + } +} + +impl ConstantTimeEq for ExtractedNoteCommitment { + fn ct_eq(&self, other: &Self) -> subtle::Choice { + self.0.ct_eq(&other.0) + } +} + +impl PartialEq for ExtractedNoteCommitment { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl Eq for ExtractedNoteCommitment {} diff --git a/zcash_primitives/src/sapling/note/nullifier.rs b/zcash_primitives/src/sapling/note/nullifier.rs index bf84dd74b..86f7bf946 100644 --- a/zcash_primitives/src/sapling/note/nullifier.rs +++ b/zcash_primitives/src/sapling/note/nullifier.rs @@ -2,6 +2,7 @@ use std::array::TryFromSliceError; use subtle::{Choice, ConstantTimeEq}; +use super::NoteCommitment; use crate::sapling::{ keys::NullifierDerivingKey, spec::{mixing_pedersen_hash, prf_nf}, @@ -25,12 +26,8 @@ impl Nullifier { /// Defined in [Zcash Protocol Spec § 4.16: Note Commitments and Nullifiers][commitmentsandnullifiers]. /// /// [commitmentsandnullifiers]: https://zips.z.cash/protocol/protocol.pdf#commitmentsandnullifiers - pub(super) fn derive( - nk: &NullifierDerivingKey, - cm: jubjub::SubgroupPoint, - position: u64, - ) -> Self { - let rho = mixing_pedersen_hash(cm, position); + pub(super) fn derive(nk: &NullifierDerivingKey, cm: NoteCommitment, position: u64) -> Self { + let rho = mixing_pedersen_hash(cm.inner(), position); Nullifier(prf_nf(&nk.0, &rho)) } } diff --git a/zcash_primitives/src/sapling/spec.rs b/zcash_primitives/src/sapling/spec.rs index f3686a99a..32d1d09cc 100644 --- a/zcash_primitives/src/sapling/spec.rs +++ b/zcash_primitives/src/sapling/spec.rs @@ -1,9 +1,12 @@ //! Helper functions defined in the Zcash Protocol Specification. use blake2s_simd::Params as Blake2sParams; -use group::GroupEncoding; +use group::{Curve, GroupEncoding}; -use crate::constants::{NULLIFIER_POSITION_GENERATOR, PRF_NF_PERSONALIZATION}; +use super::pedersen_hash::{pedersen_hash, Personalization}; +use crate::constants::{ + NOTE_COMMITMENT_RANDOMNESS_GENERATOR, NULLIFIER_POSITION_GENERATOR, PRF_NF_PERSONALIZATION, +}; /// $MixingPedersenHash$. /// @@ -34,3 +37,32 @@ pub(crate) fn prf_nf(nk: &jubjub::SubgroupPoint, rho: &jubjub::SubgroupPoint) -> .try_into() .expect("output length is correct") } + +/// $WindowedPedersenCommit_r(s)$ +/// +/// Defined in [Zcash Protocol Spec § 5.4.8.2: Windowed Pedersen commitments][concretewindowedcommit]. +/// +/// [concretewindowedcommit]: https://zips.z.cash/protocol/protocol.pdf#concretewindowedcommit +pub(crate) fn windowed_pedersen_commit( + personalization: Personalization, + s: I, + r: jubjub::Scalar, +) -> jubjub::SubgroupPoint +where + I: IntoIterator, +{ + pedersen_hash(personalization, s) + (NOTE_COMMITMENT_RANDOMNESS_GENERATOR * r) +} + +/// Coordinate extractor for Jubjub. +/// +/// Defined in [Zcash Protocol Spec § 5.4.9.4: Coordinate Extractor for Jubjub][concreteextractorjubjub]. +/// +/// [concreteextractorjubjub]: https://zips.z.cash/protocol/protocol.pdf#concreteextractorjubjub +pub(crate) fn extract_p(point: &jubjub::SubgroupPoint) -> bls12_381::Scalar { + // The commitment is in the prime order subgroup, so mapping the + // commitment to the u-coordinate is an injective encoding. + Into::<&jubjub::ExtendedPoint>::into(point) + .to_affine() + .get_u() +} diff --git a/zcash_primitives/src/sapling/value.rs b/zcash_primitives/src/sapling/value.rs index e8c130a16..b504fb0b7 100644 --- a/zcash_primitives/src/sapling/value.rs +++ b/zcash_primitives/src/sapling/value.rs @@ -37,6 +37,7 @@ //! [`SaplingBuilder::add_output`]: crate::transaction::components::sapling::builder::SaplingBuilder::add_output //! [Rust documentation]: https://doc.rust-lang.org/stable/std/primitive.i64.html +use bitvec::{array::BitArray, order::Lsb0}; use ff::Field; use group::GroupEncoding; use rand::RngCore; @@ -67,6 +68,10 @@ impl NoteValue { pub fn from_raw(value: u64) -> Self { NoteValue(value) } + + pub(crate) fn to_le_bits(self) -> BitArray<[u8; 8], Lsb0> { + BitArray::<_, Lsb0>::new(self.0.to_le_bytes()) + } } /// The blinding factor for a [`ValueCommitment`].