Merge pull request #80 from zcash/spec-updates

Update implementation to match protocol spec version 2021.2.0
This commit is contained in:
str4d 2021-05-21 21:24:39 +01:00 committed by GitHub
commit 97710e04d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 219 additions and 77 deletions

View File

@ -23,6 +23,15 @@ $$\mathsf{cm} = \mathit{Commit}^{\mathsf{cm}}_{\mathsf{rcm}}(\text{rest of note}
This is the same split (and rationale) as in Sapling, but using the more PLONK-efficient
Sinsemilla instead of Bowe--Hopwood Pedersen hashes.
Note that we also deviate from Sapling by using $\mathit{ShortCommit}$ to deriving $\mathsf{ivk}$
instead of a full PRF. This removes an unnecessary (large) PRF primitive from the circuit,
at the cost of requiring $\mathsf{rivk}$ to be part of the full viewing key.
Note that for $\mathsf{ivk}$, we also deviate from Sapling in two ways:
- We use $\mathit{ShortCommit}$ to derive $\mathsf{ivk}$ instead of a full PRF. This removes an
unnecessary (large) PRF primitive from the circuit, at the cost of requiring $\mathsf{rivk}$ to be
part of the full viewing key.
- We define $\mathsf{ivk}$ as an integer in $[1, q_P)$; that is, we exclude $\mathsf{ivk} = 0$. For
Sapling, we relied on BLAKE2s to make $\mathsf{ivk} = 0$ infeasible to produce, but it was still
technically possible. For Orchard, we get this by construction:
- $0$ is not a valid x-coordinate for any Pallas point.
- $\mathsf{SinsemillaShortCommit}$ internally maps points to field elements by replacing the identity (which
has no affine coordinates) with $0$. But $\mathsf{SinsemillaCommit}$ is defined using incomplete addition, and
thus will never produce the identity.

View File

@ -1,8 +1,6 @@
use pasta_curves::pallas;
use crate::{
keys::{DiversifiedTransmissionKey, Diversifier},
spec::diversify_hash,
spec::{diversify_hash, NonIdentityPallasPoint},
};
/// A shielded payment address.
@ -15,7 +13,7 @@ use crate::{
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
/// let address = FullViewingKey::from(&sk).default_address();
/// ```
#[derive(Debug, Clone)]
#[derive(Clone, Copy, Debug)]
pub struct Address {
d: Diversifier,
pk_d: DiversifiedTransmissionKey,
@ -30,7 +28,7 @@ impl Address {
Address { d, pk_d }
}
pub(crate) fn g_d(&self) -> pallas::Point {
pub(crate) fn g_d(&self) -> NonIdentityPallasPoint {
diversify_hash(self.d.as_array())
}

View File

@ -16,7 +16,7 @@ use crate::{
primitives::redpallas::{self, SpendAuth},
spec::{
commit_ivk, diversify_hash, extract_p, ka_orchard, prf_expand, prf_expand_vec, prf_nf,
to_base, to_scalar,
to_base, to_scalar, NonIdentityPallasPoint, NonZeroPallasBase, NonZeroPallasScalar,
},
};
@ -57,7 +57,7 @@ impl SpendingKey {
// needed. Also, `from` would panic on ask = 0.
let ask = SpendAuthorizingKey::derive_inner(&sk);
// If ivk = ⊥, discard this key.
let ivk = IncomingViewingKey::derive_inner(&(&sk).into());
let ivk = KeyAgreementPrivateKey::derive_inner(&(&sk).into());
CtOption::new(sk, !(ask.ct_is_zero() | ivk.is_none()))
}
}
@ -218,17 +218,18 @@ impl FullViewingKey {
/// Returns the default payment address for this key.
pub fn default_address(&self) -> Address {
self.address(DiversifierKey::from(self).default_diversifier())
IncomingViewingKey::from(self).default_address()
}
/// Returns the payment address for this key at the given index.
pub fn address_at(&self, j: impl Into<DiversifierIndex>) -> Address {
self.address(DiversifierKey::from(self).get(j))
IncomingViewingKey::from(self).address_at(j)
}
/// Returns the payment address for this key corresponding to the given diversifier.
pub fn address(&self, d: Diversifier) -> Address {
IncomingViewingKey::from(self).address(d)
// Shortcut: we don't need to derive DiversifierKey.
KeyAgreementPrivateKey::from(self).address(d)
}
}
@ -287,7 +288,7 @@ impl DiversifierKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug, Clone)]
#[derive(Clone, Copy, Debug)]
pub struct Diversifier([u8; 11]);
impl Diversifier {
@ -297,6 +298,49 @@ impl Diversifier {
}
}
/// The private key $\mathsf{ivk}$ used in $KA^{Orchard}$, for decrypting incoming notes.
///
/// In Sapling this is what was encoded as an incoming viewing key. For Orchard, we store
/// both this and [`DiversifierKey`] inside [`IncomingViewingKey`] for usability (to
/// enable deriving the default address for an incoming viewing key), while this separate
/// type represents $\mathsf{ivk}$.
///
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
///
/// # Implementation notes
///
/// We store $\mathsf{ivk}$ in memory as a scalar instead of a base, so that we aren't
/// incurring an expensive serialize-and-parse step every time we use it (e.g. for trial
/// decryption of notes). When we actually want to serialize ivk, we're guaranteed to get
/// a valid base field element encoding, because we always construct ivk from an integer
/// in the correct range.
#[derive(Debug)]
struct KeyAgreementPrivateKey(NonZeroPallasScalar);
impl From<&FullViewingKey> for KeyAgreementPrivateKey {
fn from(fvk: &FullViewingKey) -> Self {
// KeyAgreementPrivateKey cannot be constructed such that this unwrap would fail.
let ivk = KeyAgreementPrivateKey::derive_inner(fvk).unwrap();
KeyAgreementPrivateKey(ivk.into())
}
}
impl KeyAgreementPrivateKey {
/// Derives ivk from fvk. Internal use only, does not enforce all constraints.
fn derive_inner(fvk: &FullViewingKey) -> CtOption<NonZeroPallasBase> {
let ak = extract_p(&pallas::Point::from_bytes(&(&fvk.ak.0).into()).unwrap());
commit_ivk(&ak, &fvk.nk.0, &fvk.rivk.0)
}
/// Returns the payment address for this key corresponding to the given diversifier.
fn address(&self, d: Diversifier) -> Address {
let pk_d = DiversifiedTransmissionKey::derive(self, &d);
Address::from_parts(d, pk_d)
}
}
/// 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.
///
@ -306,31 +350,38 @@ impl Diversifier {
/// This key is not suitable for use on its own in a wallet, as it cannot maintain
/// accurate balance. You should use a [`FullViewingKey`] instead.
///
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
/// Defined in [Zcash Protocol Spec § 5.6.4.3: Orchard Raw Incoming Viewing Keys][orchardinviewingkeyencoding].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
/// [orchardinviewingkeyencoding]: https://zips.z.cash/protocol/nu5.pdf#orchardinviewingkeyencoding
#[derive(Debug)]
pub struct IncomingViewingKey(pallas::Scalar);
pub struct IncomingViewingKey {
dk: DiversifierKey,
ivk: KeyAgreementPrivateKey,
}
impl From<&FullViewingKey> for IncomingViewingKey {
fn from(fvk: &FullViewingKey) -> Self {
let ivk = IncomingViewingKey::derive_inner(fvk);
// IncomingViewingKey cannot be constructed such that this unwrap would fail.
IncomingViewingKey(ivk.unwrap())
IncomingViewingKey {
dk: fvk.into(),
ivk: fvk.into(),
}
}
}
impl IncomingViewingKey {
/// Derives ask from sk. Internal use only, does not enforce all constraints.
fn derive_inner(fvk: &FullViewingKey) -> CtOption<pallas::Scalar> {
let ak = extract_p(&pallas::Point::from_bytes(&(&fvk.ak.0).into()).unwrap());
commit_ivk(&ak, &fvk.nk.0, &fvk.rivk.0)
/// Returns the default payment address for this key.
pub fn default_address(&self) -> Address {
self.address(self.dk.default_diversifier())
}
/// Returns the payment address for this key at the given index.
pub fn address_at(&self, j: impl Into<DiversifierIndex>) -> Address {
self.address(self.dk.get(j))
}
/// Returns the payment address for this key corresponding to the given diversifier.
pub fn address(&self, d: Diversifier) -> Address {
let pk_d = DiversifiedTransmissionKey::derive(self, &d);
Address::from_parts(d, pk_d)
self.ivk.address(d)
}
}
@ -357,14 +408,14 @@ impl From<&FullViewingKey> for OutgoingViewingKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
#[derive(Debug, Clone)]
pub(crate) struct DiversifiedTransmissionKey(pallas::Point);
#[derive(Clone, Copy, Debug)]
pub(crate) struct DiversifiedTransmissionKey(NonIdentityPallasPoint);
impl DiversifiedTransmissionKey {
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
///
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
fn derive(ivk: &IncomingViewingKey, d: &Diversifier) -> Self {
fn derive(ivk: &KeyAgreementPrivateKey, d: &Diversifier) -> Self {
let g_d = diversify_hash(&d.as_array());
DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d))
}

View File

@ -2,10 +2,11 @@
use group::GroupEncoding;
use pasta_curves::pallas;
use rand::RngCore;
use subtle::CtOption;
use crate::{
keys::{FullViewingKey, SpendingKey},
spec::{prf_expand, to_base, to_scalar},
spec::{prf_expand_vec, to_base, to_scalar},
value::NoteValue,
Address,
};
@ -30,15 +31,25 @@ impl RandomSeed {
/// 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 psi(&self, rho: &Nullifier) -> pallas::Base {
to_base(prf_expand_vec(&self.0, &[&[0x09], &rho.to_bytes()[..]]))
}
/// 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]))
fn esk(&self, rho: &Nullifier) -> pallas::Scalar {
to_scalar(prf_expand_vec(&self.0, &[&[0x04], &rho.to_bytes()[..]]))
}
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
///
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
fn rcm(&self, rho: &Nullifier) -> commitment::NoteCommitTrapdoor {
commitment::NoteCommitTrapdoor(to_scalar(prf_expand_vec(
&self.0,
&[&[0x05], &rho.to_bytes()[..]],
)))
}
}
@ -72,11 +83,16 @@ impl Note {
rho: Nullifier,
mut rng: impl RngCore,
) -> Self {
Note {
recipient,
value,
rho,
rseed: RandomSeed::random(&mut rng),
loop {
let note = Note {
recipient,
value,
rho,
rseed: RandomSeed::random(&mut rng),
};
if note.commitment_inner().is_some().into() {
break note;
}
}
}
@ -93,12 +109,12 @@ impl Note {
let fvk: FullViewingKey = (&sk).into();
let recipient = fvk.default_address();
let note = Note {
let note = Note::new(
recipient,
value: NoteValue::zero(),
rho: rho.unwrap_or_else(|| Nullifier::dummy(rng)),
rseed: RandomSeed::random(rng),
};
NoteValue::zero(),
rho.unwrap_or_else(|| Nullifier::dummy(rng)),
rng,
);
(sk, fvk, note)
}
@ -114,23 +130,40 @@ impl Note {
///
/// [notes]: https://zips.z.cash/protocol/nu5.pdf#notes
pub fn commitment(&self) -> NoteCommitment {
// `Note` will always have a note commitment by construction.
self.commitment_inner().unwrap()
}
/// Derives the commitment to this note.
///
/// This is the internal fallible API, used to check at construction time that the
/// note has a commitment. Once you have a [`Note`] object, use `note.commitment()`
/// instead.
///
/// Defined in [Zcash Protocol Spec § 3.2: Notes][notes].
///
/// [notes]: https://zips.z.cash/protocol/nu5.pdf#notes
fn commitment_inner(&self) -> CtOption<NoteCommitment> {
let g_d = self.recipient.g_d();
// `Note` will always have a note commitment by construction.
NoteCommitment::derive(
g_d.to_bytes(),
self.recipient.pk_d().to_bytes(),
self.value,
self.rho.0,
self.rseed.psi(),
(&self.rseed).into(),
self.rseed.psi(&self.rho),
self.rseed.rcm(&self.rho),
)
.unwrap()
}
/// Derives the nullifier for this note.
pub fn nullifier(&self, fvk: &FullViewingKey) -> Nullifier {
Nullifier::derive(fvk.nk(), self.rho.0, self.rseed.psi(), self.commitment())
Nullifier::derive(
fvk.nk(),
self.rho.0,
self.rseed.psi(&self.rho),
self.commitment(),
)
}
}

View File

@ -5,22 +5,9 @@ use ff::PrimeField;
use pasta_curves::{arithmetic::FieldExt, pallas};
use subtle::CtOption;
use crate::{
constants::L_ORCHARD_BASE,
primitives::sinsemilla,
spec::{extract_p, prf_expand, to_scalar},
value::NoteValue,
};
use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla, spec::extract_p, 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])))
}
}
pub(super) struct NoteCommitTrapdoor(pub(super) pallas::Scalar);
/// A commitment to a note.
#[derive(Debug)]

View File

@ -11,7 +11,7 @@ use crate::{
};
/// A unique nullifier for a note.
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub struct Nullifier(pub(crate) pallas::Base);
impl Nullifier {

View File

@ -1,9 +1,10 @@
//! Helper functions defined in the Zcash Protocol Specification.
use std::iter;
use std::ops::Deref;
use blake2b_simd::Params;
use ff::PrimeField;
use ff::{Field, PrimeField};
use group::{Curve, Group};
use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt};
use pasta_curves::pallas;
@ -16,6 +17,63 @@ use crate::{
const PRF_EXPAND_PERSONALIZATION: &[u8; 16] = b"Zcash_ExpandSeed";
/// A Pallas point that is guaranteed to not be the identity.
#[derive(Clone, Copy, Debug)]
pub(crate) struct NonIdentityPallasPoint(pallas::Point);
impl Deref for NonIdentityPallasPoint {
type Target = pallas::Point;
fn deref(&self) -> &pallas::Point {
&self.0
}
}
/// An integer in [1..q_P].
pub(crate) struct NonZeroPallasBase(pallas::Base);
impl NonZeroPallasBase {
/// Constructs a wrapper for a base field element that is guaranteed to be non-zero.
///
/// # Panics
///
/// Panics if `s.is_zero()`.
fn guaranteed(s: pallas::Base) -> Self {
assert!(!s.is_zero());
NonZeroPallasBase(s)
}
}
/// An integer in [1..r_P].
#[derive(Debug)]
pub(crate) struct NonZeroPallasScalar(pallas::Scalar);
impl From<NonZeroPallasBase> for NonZeroPallasScalar {
fn from(s: NonZeroPallasBase) -> Self {
NonZeroPallasScalar::guaranteed(mod_r_p(s.0))
}
}
impl NonZeroPallasScalar {
/// Constructs a wrapper for a scalar field element that is guaranteed to be non-zero.
///
/// # Panics
///
/// Panics if `s.is_zero()`.
fn guaranteed(s: pallas::Scalar) -> Self {
assert!(!s.is_zero());
NonZeroPallasScalar(s)
}
}
impl Deref for NonZeroPallasScalar {
type Target = pallas::Scalar;
fn deref(&self) -> &pallas::Scalar {
&self.0
}
}
/// $\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].
@ -49,7 +107,7 @@ pub(crate) fn commit_ivk(
ak: &pallas::Base,
nk: &pallas::Base,
rivk: &pallas::Scalar,
) -> CtOption<pallas::Scalar> {
) -> CtOption<NonZeroPallasBase> {
// 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");
@ -60,21 +118,24 @@ pub(crate) fn commit_ivk(
.chain(nk.to_le_bits().iter().by_val().take(L_ORCHARD_BASE)),
rivk,
)
.map(mod_r_p)
// Commit^ivk.Output is specified as [1..q_P] {⊥}. We get this from
// sinsemilla::CommitDomain::short_commit by construction:
// - 0 is not a valid x-coordinate for any Pallas point.
// - sinsemilla::CommitDomain::short_commit calls extract_p_bottom, which replaces
// the identity (which has no affine coordinates) with 0. but Sinsemilla is
// defined using incomplete addition, and thus will never produce the identity.
.map(NonZeroPallasBase::guaranteed)
}
/// Defined in [Zcash Protocol Spec § 5.4.1.6: DiversifyHash^Sapling and DiversifyHash^Orchard Hash Functions][concretediversifyhash].
///
/// [concretediversifyhash]: https://zips.z.cash/protocol/nu5.pdf#concretediversifyhash
pub(crate) fn diversify_hash(d: &[u8; 11]) -> pallas::Point {
pub(crate) fn diversify_hash(d: &[u8; 11]) -> NonIdentityPallasPoint {
let hasher = pallas::Point::hash_to_curve("z.cash:Orchard-gd");
let pk_d = hasher(d);
if pk_d.is_identity().into() {
// If the identity occurs, we replace it with a different fixed point.
hasher(&[])
} else {
pk_d
}
// If the identity occurs, we replace it with a different fixed point.
// TODO: Replace the unwrap_or_else with a cached fixed point.
NonIdentityPallasPoint(CtOption::new(pk_d, !pk_d.is_identity()).unwrap_or_else(|| hasher(&[])))
}
/// $PRF^\mathsf{expand}(sk, t) := BLAKE2b-512("Zcash_ExpandSeed", sk || t)$
@ -110,8 +171,11 @@ pub(crate) fn prf_nf(nk: pallas::Base, rho: pallas::Base) -> pallas::Base {
/// 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(sk: &pallas::Scalar, b: &pallas::Point) -> pallas::Point {
b * sk
pub(crate) fn ka_orchard(
sk: &NonZeroPallasScalar,
b: &NonIdentityPallasPoint,
) -> NonIdentityPallasPoint {
NonIdentityPallasPoint(b.deref() * sk.deref())
}
/// Coordinate extractor for Pallas.