diff --git a/src/address.rs b/src/address.rs index 6dbed73b..96328985 100644 --- a/src/address.rs +++ b/src/address.rs @@ -1,4 +1,9 @@ -use crate::keys::{DiversifiedTransmissionKey, Diversifier}; +use pasta_curves::pallas; + +use crate::{ + keys::{DiversifiedTransmissionKey, Diversifier}, + spec::diversify_hash, +}; /// A shielded payment address. /// @@ -18,6 +23,18 @@ pub struct Address { impl Address { pub(crate) fn from_parts(d: Diversifier, pk_d: DiversifiedTransmissionKey) -> Self { + // We assume here that pk_d is correctly-derived from d. We ensure this for + // internal APIs. For parsing from raw byte encodings, we assume that users aren't + // modifying internals of encoded address formats. If they do, that can result in + // lost funds, but we can't defend against that from here. Address { d, pk_d } } + + pub(crate) fn g_d(&self) -> pallas::Point { + diversify_hash(self.d.as_array()) + } + + pub(crate) fn pk_d(&self) -> &DiversifiedTransmissionKey { + &self.pk_d + } } diff --git a/src/keys.rs b/src/keys.rs index afb0e3aa..6ccbb8ae 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -14,8 +14,8 @@ use crate::{ address::Address, primitives::redpallas::{self, SpendAuth}, spec::{ - commit_ivk, diversify_hash, extract_p, ka_orchard, prf_expand, prf_expand_vec, to_base, - to_scalar, + commit_ivk, diversify_hash, extract_p, ka_orchard, prf_expand, prf_expand_vec, prf_nf, + to_base, to_scalar, }, }; @@ -102,6 +102,12 @@ impl From<&SpendingKey> for NullifierDerivingKey { } } +impl NullifierDerivingKey { + pub(crate) fn prf_nf(&self, rho: pallas::Base) -> pallas::Base { + prf_nf(self.0, rho) + } +} + /// The randomness for $\mathsf{Commit}^\mathsf{ivk}$. /// /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. @@ -142,6 +148,10 @@ impl From<&SpendingKey> for FullViewingKey { } impl FullViewingKey { + pub(crate) fn nk(&self) -> &NullifierDerivingKey { + &self.nk + } + /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents @@ -229,6 +239,13 @@ impl DiversifierKey { #[derive(Debug)] pub struct Diversifier([u8; 11]); +impl Diversifier { + /// Returns the byte array corresponding to this diversifier. + pub fn as_array(&self) -> &[u8; 11] { + &self.0 + } +} + /// A key that provides the capability to detect and decrypt incoming notes from the block /// chain, without being able to spend the notes or detect when they are spent. /// @@ -290,7 +307,12 @@ impl DiversifiedTransmissionKey { /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents fn derive(ivk: &IncomingViewingKey, d: &Diversifier) -> Self { - let g_d = diversify_hash(&d.0); + let g_d = diversify_hash(&d.as_array()); DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d)) } + + /// $repr_P(self)$ + pub(crate) fn to_bytes(&self) -> [u8; 32] { + self.0.to_bytes() + } } diff --git a/src/note.rs b/src/note.rs index dbfa575c..e0728f01 100644 --- a/src/note.rs +++ b/src/note.rs @@ -1,20 +1,36 @@ -use crate::{keys::FullViewingKey, value::NoteValue, Address}; +use group::GroupEncoding; +use pasta_curves::pallas; + +use crate::{ + keys::FullViewingKey, + spec::{prf_expand, to_base, to_scalar}, + value::NoteValue, + Address, +}; + +mod commitment; +pub use self::commitment::NoteCommitment; + +mod nullifier; +pub use self::nullifier::Nullifier; /// The ZIP 212 seed randomness for a note. #[derive(Debug)] struct RandomSeed([u8; 32]); impl RandomSeed { - fn psi(&self) -> () { - todo!() + /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. + /// + /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend + fn psi(&self) -> pallas::Base { + to_base(prf_expand(&self.0, &[0x09])) } - fn rcm(&self) -> () { - todo!() - } - - fn esk(&self) -> () { - todo!() + /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. + /// + /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend + fn esk(&self) -> pallas::Scalar { + to_scalar(prf_expand(&self.0, &[0x04])) } } @@ -38,24 +54,29 @@ pub struct Note { impl Note { /// Derives the commitment to this note. + /// + /// Defined in [Zcash Protocol Spec § 3.2: Notes][notes]. + /// + /// [notes]: https://zips.z.cash/protocol/nu5.pdf#notes pub fn commitment(&self) -> NoteCommitment { - todo!() + let g_d = self.recipient.g_d(); + + NoteCommitment::derive( + g_d.to_bytes(), + self.recipient.pk_d().to_bytes(), + self.value, + self.rho.0, + self.rseed.psi(), + (&self.rseed).into(), + ) } /// Derives the nullifier for this note. - pub fn nullifier(&self, _: &FullViewingKey) -> Nullifier { - todo!() + pub fn nullifier(&self, fvk: &FullViewingKey) -> Nullifier { + Nullifier::derive(fvk.nk(), self.rho.0, self.rseed.psi(), self.commitment()) } } /// An encrypted note. #[derive(Debug)] pub struct EncryptedNote; - -/// A commitment to a note. -#[derive(Debug)] -pub struct NoteCommitment; - -/// A unique nullifier for a note. -#[derive(Debug)] -pub struct Nullifier; diff --git a/src/note/commitment.rs b/src/note/commitment.rs new file mode 100644 index 00000000..81dd7654 --- /dev/null +++ b/src/note/commitment.rs @@ -0,0 +1,55 @@ +use std::iter; + +use bitvec::{array::BitArray, order::Lsb0}; +use ff::PrimeField; +use pasta_curves::pallas; + +use crate::{ + constants::L_ORCHARD_BASE, + primitives::sinsemilla, + spec::{prf_expand, to_scalar}, + value::NoteValue, +}; + +use super::RandomSeed; + +pub(super) struct NoteCommitTrapdoor(pallas::Scalar); + +impl From<&RandomSeed> for NoteCommitTrapdoor { + fn from(rseed: &RandomSeed) -> Self { + NoteCommitTrapdoor(to_scalar(prf_expand(&rseed.0, &[0x05]))) + } +} + +/// A commitment to a note. +#[derive(Debug)] +pub struct NoteCommitment(pub(super) pallas::Point); + +impl NoteCommitment { + /// $NoteCommit^Orchard$. + /// + /// Defined in [Zcash Protocol Spec § 5.4.8.4: Sinsemilla commitments][concretesinsemillacommit]. + /// + /// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit + pub(super) fn derive( + g_d: [u8; 32], + pk_d: [u8; 32], + v: NoteValue, + rho: pallas::Base, + psi: pallas::Base, + rcm: NoteCommitTrapdoor, + ) -> Self { + let domain = sinsemilla::CommitDomain::new("z.cash:Orchard-NoteCommit"); + NoteCommitment( + domain.commit( + iter::empty() + .chain(BitArray::::new(g_d).iter().by_val()) + .chain(BitArray::::new(pk_d).iter().by_val()) + .chain(v.to_le_bits().iter().by_val()) + .chain(rho.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)) + .chain(psi.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)), + &rcm.0, + ), + ) + } +} diff --git a/src/note/nullifier.rs b/src/note/nullifier.rs new file mode 100644 index 00000000..93e8f661 --- /dev/null +++ b/src/note/nullifier.rs @@ -0,0 +1,30 @@ +use halo2::arithmetic::CurveExt; +use pasta_curves::pallas; + +use super::NoteCommitment; +use crate::{ + keys::NullifierDerivingKey, + spec::{extract_p, mod_r_p}, +}; + +/// A unique nullifier for a note. +#[derive(Debug)] +pub struct Nullifier(pub(super) pallas::Base); + +impl Nullifier { + /// $DeriveNullifier$. + /// + /// Defined in [Zcash Protocol Spec § 4.16: Note Commitments and Nullifiers][commitmentsandnullifiers]. + /// + /// [commitmentsandnullifiers]: https://zips.z.cash/protocol/nu5.pdf#commitmentsandnullifiers + pub(super) fn derive( + nk: &NullifierDerivingKey, + rho: pallas::Base, + psi: pallas::Base, + cm: NoteCommitment, + ) -> Self { + let k = pallas::Point::hash_to_curve("z.cash:Orchard")(b"K"); + + Nullifier(extract_p(&(k * mod_r_p(nk.prf_nf(rho) + psi) + cm.0))) + } +} diff --git a/src/spec.rs b/src/spec.rs index 1c7e2203..f7172513 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -8,7 +8,10 @@ use group::{Curve, Group}; use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt}; use pasta_curves::pallas; -use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla}; +use crate::{ + constants::L_ORCHARD_BASE, + primitives::{poseidon, sinsemilla}, +}; const PRF_EXPAND_PERSONALIZATION: &[u8; 16] = b"Zcash_ExpandSeed"; @@ -30,6 +33,14 @@ pub(crate) fn to_scalar(x: [u8; 64]) -> pallas::Scalar { pallas::Scalar::from_bytes_wide(&x) } +/// Converts from pallas::Base to pallas::Scalar (aka $x \pmod{r_\mathbb{P}}$). +/// +/// This requires no modular reduction because Pallas' base field is smaller than its +/// scalar field. +pub(crate) fn mod_r_p(x: pallas::Base) -> pallas::Scalar { + pallas::Scalar::from_repr(x.to_repr()).unwrap() +} + /// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents]. /// /// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents @@ -41,16 +52,15 @@ pub(crate) fn commit_ivk( // We rely on the API contract that to_le_bits() returns at least PrimeField::NUM_BITS // bits, which is equal to L_ORCHARD_BASE. let domain = sinsemilla::CommitDomain::new(&"z.cash:Orchard-CommitIvk"); - let ivk = domain.short_commit( - iter::empty() - .chain(ak.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)) - .chain(nk.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)), - rivk, - ); - - // Convert from pallas::Base to pallas::Scalar. This requires no modular reduction - // because Pallas' base field is smaller than its scalar field. - pallas::Scalar::from_repr(ivk.to_repr()).unwrap() + // TODO: handle the (negligible probability of) failure of SinsemillaShortCommit. + mod_r_p( + domain.short_commit( + iter::empty() + .chain(ak.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)) + .chain(nk.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)), + rivk, + ), + ) } /// Defined in [Zcash Protocol Spec § 5.4.1.6: DiversifyHash^Sapling and DiversifyHash^Orchard Hash Functions][concretediversifyhash]. @@ -88,6 +98,16 @@ pub(crate) fn prf_expand_vec(sk: &[u8], ts: &[&[u8]]) -> [u8; 64] { *h.finalize().as_array() } +/// $PRF^\mathsf{nfOrchard}(nk, \rho) := Poseidon(nk, \rho)$ +/// +/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs]. +/// +/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs +pub(crate) fn prf_nf(nk: pallas::Base, rho: pallas::Base) -> pallas::Base { + poseidon::Hash::init(poseidon::OrchardNullifier, poseidon::ConstantLength(2)) + .hash(iter::empty().chain(Some(nk)).chain(Some(rho))) +} + /// Defined in [Zcash Protocol Spec § 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement]. /// /// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement diff --git a/src/value.rs b/src/value.rs index 6cf96cdc..4a476cfc 100644 --- a/src/value.rs +++ b/src/value.rs @@ -13,10 +13,18 @@ //! [`Action`]: crate::bundle::Action //! [`Bundle`]: crate::bundle::Bundle +use bitvec::{array::BitArray, order::Lsb0}; + /// The value of an individual Orchard note. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub struct NoteValue(u64); +impl NoteValue { + pub(crate) fn to_le_bits(&self) -> BitArray { + BitArray::::new(self.0.to_le_bytes()) + } +} + /// A sum of Orchard note values. #[derive(Debug)] pub struct ValueSum(i64);