mirror of https://github.com/zcash/orchard.git
commit
1182d8d5a7
|
@ -44,6 +44,10 @@ rev = "d04b532368d05b505e622f8cac4c0693574fbd93"
|
||||||
git = "https://github.com/str4d/redjubjub.git"
|
git = "https://github.com/str4d/redjubjub.git"
|
||||||
rev = "d5d8c5f3bb704bad8ae88fe4a29ae1f744774cb2"
|
rev = "d5d8c5f3bb704bad8ae88fe4a29ae1f744774cb2"
|
||||||
|
|
||||||
|
[dependencies.zcash_note_encryption]
|
||||||
|
git = "https://github.com/zcash/librustzcash.git"
|
||||||
|
rev = "cc533a9da4f6a7209a7be05f82b12a03969152c9"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
||||||
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
|
/// let sk = SpendingKey::from_bytes([7; 32]).unwrap();
|
||||||
/// let address = FullViewingKey::from(&sk).default_address();
|
/// let address = FullViewingKey::from(&sk).default_address();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Address {
|
pub struct Address {
|
||||||
d: Diversifier,
|
d: Diversifier,
|
||||||
pk_d: DiversifiedTransmissionKey,
|
pk_d: DiversifiedTransmissionKey,
|
||||||
|
@ -28,6 +28,10 @@ impl Address {
|
||||||
Address { d, pk_d }
|
Address { d, pk_d }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn diversifer(&self) -> Diversifier {
|
||||||
|
self.d
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn g_d(&self) -> NonIdentityPallasPoint {
|
pub(crate) fn g_d(&self) -> NonIdentityPallasPoint {
|
||||||
diversify_hash(self.d.as_array())
|
diversify_hash(self.d.as_array())
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey,
|
FullViewingKey, OutgoingViewingKey, SpendAuthorizingKey, SpendValidatingKey, SpendingKey,
|
||||||
},
|
},
|
||||||
note::{Note, TransmittedNoteCiphertext},
|
note::{Note, TransmittedNoteCiphertext},
|
||||||
|
note_encryption::OrchardNoteEncryption,
|
||||||
primitives::redpallas::{self, Binding, SpendAuth},
|
primitives::redpallas::{self, Binding, SpendAuth},
|
||||||
tree::{Anchor, MerklePath},
|
tree::{Anchor, MerklePath},
|
||||||
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
||||||
|
@ -79,7 +80,7 @@ struct RecipientInfo {
|
||||||
ovk: Option<OutgoingViewingKey>,
|
ovk: Option<OutgoingViewingKey>,
|
||||||
recipient: Address,
|
recipient: Address,
|
||||||
value: NoteValue,
|
value: NoteValue,
|
||||||
memo: Option<()>,
|
memo: Option<[u8; 512]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecipientInfo {
|
impl RecipientInfo {
|
||||||
|
@ -135,21 +136,32 @@ impl ActionInfo {
|
||||||
let alpha = pallas::Scalar::random(&mut rng);
|
let alpha = pallas::Scalar::random(&mut rng);
|
||||||
let rk = ak.randomize(&alpha);
|
let rk = ak.randomize(&alpha);
|
||||||
|
|
||||||
let note = Note::new(self.output.recipient, self.output.value, nf_old, rng);
|
let note = Note::new(self.output.recipient, self.output.value, nf_old, &mut rng);
|
||||||
let cm_new = note.commitment();
|
let cm_new = note.commitment();
|
||||||
|
let cmx = cm_new.into();
|
||||||
|
|
||||||
|
let encryptor = OrchardNoteEncryption::new(
|
||||||
|
self.output.ovk,
|
||||||
|
note,
|
||||||
|
self.output.recipient,
|
||||||
|
self.output.memo.unwrap_or_else(|| {
|
||||||
|
let mut memo = [0; 512];
|
||||||
|
memo[0] = 0xf6;
|
||||||
|
memo
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Note encryption
|
|
||||||
let encrypted_note = TransmittedNoteCiphertext {
|
let encrypted_note = TransmittedNoteCiphertext {
|
||||||
epk_bytes: [0u8; 32],
|
epk_bytes: encryptor.epk().to_bytes().0,
|
||||||
enc_ciphertext: [0u8; 580],
|
enc_ciphertext: encryptor.encrypt_note_plaintext(),
|
||||||
out_ciphertext: [0u8; 80],
|
out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng),
|
||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
Action::from_parts(
|
Action::from_parts(
|
||||||
nf_old,
|
nf_old,
|
||||||
rk,
|
rk,
|
||||||
cm_new.into(),
|
cmx,
|
||||||
encrypted_note,
|
encrypted_note,
|
||||||
cv_net,
|
cv_net,
|
||||||
SigningMetadata {
|
SigningMetadata {
|
||||||
|
@ -220,7 +232,7 @@ impl Builder {
|
||||||
ovk: Option<OutgoingViewingKey>,
|
ovk: Option<OutgoingViewingKey>,
|
||||||
recipient: Address,
|
recipient: Address,
|
||||||
value: NoteValue,
|
value: NoteValue,
|
||||||
memo: Option<()>,
|
memo: Option<[u8; 512]>,
|
||||||
) -> Result<(), &'static str> {
|
) -> Result<(), &'static str> {
|
||||||
if !self.flags.outputs_enabled() {
|
if !self.flags.outputs_enabled() {
|
||||||
return Err("Outputs are not enabled for this builder");
|
return Err("Outputs are not enabled for this builder");
|
||||||
|
|
192
src/keys.rs
192
src/keys.rs
|
@ -4,12 +4,15 @@ use std::convert::TryInto;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
|
||||||
use aes::Aes256;
|
use aes::Aes256;
|
||||||
|
use blake2b_simd::{Hash as Blake2bHash, Params};
|
||||||
use fpe::ff1::{BinaryNumeralString, FF1};
|
use fpe::ff1::{BinaryNumeralString, FF1};
|
||||||
use group::GroupEncoding;
|
use group::GroupEncoding;
|
||||||
use halo2::arithmetic::FieldExt;
|
use halo2::arithmetic::FieldExt;
|
||||||
use pasta_curves::pallas;
|
use pasta_curves::pallas;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
use subtle::CtOption;
|
use subtle::CtOption;
|
||||||
|
use zcash_note_encryption::EphemeralKeyBytes;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
address::Address,
|
address::Address,
|
||||||
|
@ -20,6 +23,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const KDF_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_OrchardKDF";
|
||||||
|
|
||||||
/// A spending key, from which all key material is derived.
|
/// A spending key, from which all key material is derived.
|
||||||
///
|
///
|
||||||
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
||||||
|
@ -286,10 +291,14 @@ impl DiversifierKey {
|
||||||
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
||||||
///
|
///
|
||||||
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub struct Diversifier([u8; 11]);
|
pub struct Diversifier([u8; 11]);
|
||||||
|
|
||||||
impl Diversifier {
|
impl Diversifier {
|
||||||
|
pub(crate) fn from_bytes(d: [u8; 11]) -> Self {
|
||||||
|
Diversifier(d)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the byte array corresponding to this diversifier.
|
/// Returns the byte array corresponding to this diversifier.
|
||||||
pub fn as_array(&self) -> &[u8; 11] {
|
pub fn as_array(&self) -> &[u8; 11] {
|
||||||
&self.0
|
&self.0
|
||||||
|
@ -334,7 +343,7 @@ impl KeyAgreementPrivateKey {
|
||||||
|
|
||||||
/// Returns the payment address for this key corresponding to the given diversifier.
|
/// Returns the payment address for this key corresponding to the given diversifier.
|
||||||
fn address(&self, d: Diversifier) -> Address {
|
fn address(&self, d: Diversifier) -> Address {
|
||||||
let pk_d = DiversifiedTransmissionKey::derive(self, &d);
|
let pk_d = DiversifiedTransmissionKey::derive_inner(self, &d);
|
||||||
Address::from_parts(d, pk_d)
|
Address::from_parts(d, pk_d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,6 +376,16 @@ impl From<&FullViewingKey> for IncomingViewingKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IncomingViewingKey {
|
impl IncomingViewingKey {
|
||||||
|
/// Parses an Orchard incoming viewing key from its raw encoding.
|
||||||
|
pub fn from_bytes(bytes: &[u8; 64]) -> CtOption<Self> {
|
||||||
|
NonZeroPallasBase::from_bytes(bytes[32..].try_into().unwrap()).map(|ivk| {
|
||||||
|
IncomingViewingKey {
|
||||||
|
dk: DiversifierKey(bytes[..32].try_into().unwrap()),
|
||||||
|
ivk: KeyAgreementPrivateKey(ivk.into()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the default payment address for this key.
|
/// Returns the default payment address for this key.
|
||||||
pub fn default_address(&self) -> Address {
|
pub fn default_address(&self) -> Address {
|
||||||
self.address(self.dk.default_diversifier())
|
self.address(self.dk.default_diversifier())
|
||||||
|
@ -401,35 +420,145 @@ impl From<&FullViewingKey> for OutgoingViewingKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 32]> for OutgoingViewingKey {
|
||||||
|
fn from(ovk: [u8; 32]) -> Self {
|
||||||
|
OutgoingViewingKey(ovk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8; 32]> for OutgoingViewingKey {
|
||||||
|
fn as_ref(&self) -> &[u8; 32] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The diversified transmission key for a given payment address.
|
/// The diversified transmission key for a given payment address.
|
||||||
///
|
///
|
||||||
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
||||||
///
|
///
|
||||||
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct DiversifiedTransmissionKey(NonIdentityPallasPoint);
|
pub struct DiversifiedTransmissionKey(NonIdentityPallasPoint);
|
||||||
|
|
||||||
impl DiversifiedTransmissionKey {
|
impl DiversifiedTransmissionKey {
|
||||||
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
/// Defined in [Zcash Protocol Spec § 4.2.3: Orchard Key Components][orchardkeycomponents].
|
||||||
///
|
///
|
||||||
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
/// [orchardkeycomponents]: https://zips.z.cash/protocol/nu5.pdf#orchardkeycomponents
|
||||||
fn derive(ivk: &KeyAgreementPrivateKey, d: &Diversifier) -> Self {
|
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());
|
let g_d = diversify_hash(&d.as_array());
|
||||||
DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d))
|
DiversifiedTransmissionKey(ka_orchard(&ivk.0, &g_d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// $abst_P(bytes)$
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
NonIdentityPallasPoint::from_bytes(bytes).map(DiversifiedTransmissionKey)
|
||||||
|
}
|
||||||
|
|
||||||
/// $repr_P(self)$
|
/// $repr_P(self)$
|
||||||
pub(crate) fn to_bytes(self) -> [u8; 32] {
|
pub(crate) fn to_bytes(self) -> [u8; 32] {
|
||||||
self.0.to_bytes()
|
self.0.to_bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An ephemeral secret key used to encrypt an output note on-chain.
|
||||||
|
///
|
||||||
|
/// `esk` is "ephemeral" in the sense that each secret key is only used once. In
|
||||||
|
/// practice, `esk` is derived deterministically from the note that it is encrypting.
|
||||||
|
///
|
||||||
|
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{Private} := \mathbb{F}^{\ast}_{r_P}$
|
||||||
|
///
|
||||||
|
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
|
||||||
|
///
|
||||||
|
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EphemeralSecretKey(pub(crate) NonZeroPallasScalar);
|
||||||
|
|
||||||
|
impl ConstantTimeEq for EphemeralSecretKey {
|
||||||
|
fn ct_eq(&self, other: &Self) -> subtle::Choice {
|
||||||
|
self.0.ct_eq(&other.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EphemeralSecretKey {
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
NonZeroPallasScalar::from_bytes(bytes).map(EphemeralSecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn derive_public(&self, g_d: NonIdentityPallasPoint) -> EphemeralPublicKey {
|
||||||
|
EphemeralPublicKey(ka_orchard(&self.0, &g_d))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn agree(&self, pk_d: &DiversifiedTransmissionKey) -> SharedSecret {
|
||||||
|
SharedSecret(ka_orchard(&self.0, &pk_d.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ephemeral public key used to encrypt an output note on-chain.
|
||||||
|
///
|
||||||
|
/// `epk` is "ephemeral" in the sense that each public key is only used once. In practice,
|
||||||
|
/// `epk` is derived deterministically from the note that it is encrypting.
|
||||||
|
///
|
||||||
|
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{Public} := \mathbb{P}^{\ast}$
|
||||||
|
///
|
||||||
|
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
|
||||||
|
///
|
||||||
|
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EphemeralPublicKey(NonIdentityPallasPoint);
|
||||||
|
|
||||||
|
impl EphemeralPublicKey {
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
NonIdentityPallasPoint::from_bytes(bytes).map(EphemeralPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_bytes(&self) -> EphemeralKeyBytes {
|
||||||
|
EphemeralKeyBytes(self.0.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn agree(&self, ivk: &IncomingViewingKey) -> SharedSecret {
|
||||||
|
SharedSecret(ka_orchard(&ivk.ivk.0, &self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// $\mathsf{KA}^\mathsf{Orchard}.\mathsf{SharedSecret} := \mathbb{P}^{\ast}$
|
||||||
|
///
|
||||||
|
/// Defined in [section 5.4.5.5: Orchard Key Agreement][concreteorchardkeyagreement].
|
||||||
|
///
|
||||||
|
/// [concreteorchardkeyagreement]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkeyagreement
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SharedSecret(NonIdentityPallasPoint);
|
||||||
|
|
||||||
|
impl SharedSecret {
|
||||||
|
/// For checking test vectors only.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn to_bytes(&self) -> [u8; 32] {
|
||||||
|
self.0.to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Defined in [Zcash Protocol Spec § 5.4.5.6: Orchard Key Agreement][concreteorchardkdf].
|
||||||
|
///
|
||||||
|
/// [concreteorchardkdf]: https://zips.z.cash/protocol/nu5.pdf#concreteorchardkdf
|
||||||
|
pub(crate) fn kdf_orchard(self, ephemeral_key: &EphemeralKeyBytes) -> Blake2bHash {
|
||||||
|
Params::new()
|
||||||
|
.hash_length(32)
|
||||||
|
.personal(KDF_ORCHARD_PERSONALIZATION)
|
||||||
|
.to_state()
|
||||||
|
.update(&self.0.to_bytes())
|
||||||
|
.update(&ephemeral_key.0)
|
||||||
|
.finalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Generators for property testing.
|
/// Generators for property testing.
|
||||||
#[cfg(any(test, feature = "test-dependencies"))]
|
#[cfg(any(test, feature = "test-dependencies"))]
|
||||||
pub mod testing {
|
pub mod testing {
|
||||||
use proptest::prelude::*;
|
use proptest::prelude::*;
|
||||||
|
|
||||||
use super::SpendingKey;
|
use super::{EphemeralSecretKey, SpendingKey};
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
/// Generate a uniformly distributed fake note commitment value.
|
/// Generate a uniformly distributed fake note commitment value.
|
||||||
|
@ -444,19 +573,64 @@ pub mod testing {
|
||||||
key.unwrap()
|
key.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prop_compose! {
|
||||||
|
/// Generate a uniformly distributed fake note commitment value.
|
||||||
|
pub fn arb_esk()(
|
||||||
|
esk in prop::array::uniform32(prop::num::u8::ANY)
|
||||||
|
.prop_map(|b| EphemeralSecretKey::from_bytes(&b))
|
||||||
|
.prop_filter(
|
||||||
|
"Values must correspond to valid Orchard ephemeral secret keys.",
|
||||||
|
|opt| bool::from(opt.is_some())
|
||||||
|
)
|
||||||
|
) -> EphemeralSecretKey {
|
||||||
|
esk.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use ff::PrimeField;
|
use ff::PrimeField;
|
||||||
|
use proptest::prelude::*;
|
||||||
|
|
||||||
use super::*;
|
use super::{
|
||||||
|
testing::{arb_esk, arb_spending_key},
|
||||||
|
*,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
note::{ExtractedNoteCommitment, Nullifier},
|
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
|
||||||
value::NoteValue,
|
value::NoteValue,
|
||||||
Note,
|
Note,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsers_reject_invalid() {
|
||||||
|
assert!(bool::from(
|
||||||
|
EphemeralSecretKey::from_bytes(&[0xff; 32]).is_none()
|
||||||
|
));
|
||||||
|
assert!(bool::from(
|
||||||
|
EphemeralPublicKey::from_bytes(&[0xff; 32]).is_none()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn key_agreement(
|
||||||
|
sk in arb_spending_key(),
|
||||||
|
esk in arb_esk(),
|
||||||
|
) {
|
||||||
|
let ivk = IncomingViewingKey::from(&(&sk).into());
|
||||||
|
let addr = ivk.default_address();
|
||||||
|
|
||||||
|
let epk = esk.derive_public(addr.g_d());
|
||||||
|
|
||||||
|
assert!(bool::from(
|
||||||
|
esk.agree(addr.pk_d()).0.ct_eq(&epk.agree(&ivk).0)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_vectors() {
|
fn test_vectors() {
|
||||||
for tv in crate::test_vectors::keys::test_vectors() {
|
for tv in crate::test_vectors::keys::test_vectors() {
|
||||||
|
@ -492,7 +666,7 @@ mod tests {
|
||||||
addr,
|
addr,
|
||||||
NoteValue::from_raw(tv.note_v),
|
NoteValue::from_raw(tv.note_v),
|
||||||
rho,
|
rho,
|
||||||
tv.note_rseed.into(),
|
RandomSeed::from_bytes(tv.note_rseed, &rho).unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let cmx: ExtractedNoteCommitment = note.commitment().into();
|
let cmx: ExtractedNoteCommitment = note.commitment().into();
|
||||||
|
|
|
@ -23,6 +23,7 @@ mod circuit;
|
||||||
mod constants;
|
mod constants;
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
pub mod note;
|
pub mod note;
|
||||||
|
mod note_encryption;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
mod spec;
|
mod spec;
|
||||||
mod tree;
|
mod tree;
|
||||||
|
|
71
src/note.rs
71
src/note.rs
|
@ -5,8 +5,8 @@ use rand::RngCore;
|
||||||
use subtle::CtOption;
|
use subtle::CtOption;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
keys::{FullViewingKey, SpendingKey},
|
keys::{EphemeralSecretKey, FullViewingKey, SpendingKey},
|
||||||
spec::{to_base, to_scalar, PrfExpand},
|
spec::{to_base, to_scalar, NonZeroPallasScalar, PrfExpand},
|
||||||
value::NoteValue,
|
value::NoteValue,
|
||||||
Address,
|
Address,
|
||||||
};
|
};
|
||||||
|
@ -21,17 +21,26 @@ pub use self::nullifier::Nullifier;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct RandomSeed([u8; 32]);
|
pub(crate) struct RandomSeed([u8; 32]);
|
||||||
|
|
||||||
impl From<[u8; 32]> for RandomSeed {
|
|
||||||
fn from(rseed: [u8; 32]) -> Self {
|
|
||||||
RandomSeed(rseed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RandomSeed {
|
impl RandomSeed {
|
||||||
pub(crate) fn random(rng: &mut impl RngCore) -> Self {
|
pub(crate) fn random(rng: &mut impl RngCore, rho: &Nullifier) -> Self {
|
||||||
|
loop {
|
||||||
let mut bytes = [0; 32];
|
let mut bytes = [0; 32];
|
||||||
rng.fill_bytes(&mut bytes);
|
rng.fill_bytes(&mut bytes);
|
||||||
RandomSeed(bytes)
|
let rseed = RandomSeed::from_bytes(bytes, rho);
|
||||||
|
if rseed.is_some().into() {
|
||||||
|
break rseed.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_bytes(rseed: [u8; 32], rho: &Nullifier) -> CtOption<Self> {
|
||||||
|
let rseed = RandomSeed(rseed);
|
||||||
|
let esk = rseed.esk_inner(rho);
|
||||||
|
CtOption::new(rseed, esk.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_bytes(&self) -> &[u8; 32] {
|
||||||
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
||||||
|
@ -44,8 +53,18 @@ impl RandomSeed {
|
||||||
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
||||||
///
|
///
|
||||||
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
|
/// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend
|
||||||
fn esk(&self, rho: &Nullifier) -> pallas::Scalar {
|
fn esk_inner(&self, rho: &Nullifier) -> CtOption<NonZeroPallasScalar> {
|
||||||
to_scalar(PrfExpand::Esk.with_ad(&self.0, &rho.to_bytes()[..]))
|
NonZeroPallasScalar::from_scalar(to_scalar(
|
||||||
|
PrfExpand::Esk.with_ad(&self.0, &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, rho: &Nullifier) -> NonZeroPallasScalar {
|
||||||
|
// We can't construct a RandomSeed for which this unwrap fails.
|
||||||
|
self.esk_inner(rho).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
/// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend].
|
||||||
|
@ -76,8 +95,17 @@ pub struct Note {
|
||||||
rseed: RandomSeed,
|
rseed: RandomSeed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Note {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
// Notes are canonically defined by their commitments.
|
||||||
|
ExtractedNoteCommitment::from(self.commitment())
|
||||||
|
.eq(&ExtractedNoteCommitment::from(other.commitment()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for Note {}
|
||||||
|
|
||||||
impl Note {
|
impl Note {
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn from_parts(
|
pub(crate) fn from_parts(
|
||||||
recipient: Address,
|
recipient: Address,
|
||||||
value: NoteValue,
|
value: NoteValue,
|
||||||
|
@ -108,7 +136,7 @@ impl Note {
|
||||||
recipient,
|
recipient,
|
||||||
value,
|
value,
|
||||||
rho,
|
rho,
|
||||||
rseed: RandomSeed::random(&mut rng),
|
rseed: RandomSeed::random(&mut rng, &rho),
|
||||||
};
|
};
|
||||||
if note.commitment_inner().is_some().into() {
|
if note.commitment_inner().is_some().into() {
|
||||||
break note;
|
break note;
|
||||||
|
@ -139,11 +167,26 @@ impl Note {
|
||||||
(sk, fvk, note)
|
(sk, fvk, note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the recipient of this note.
|
||||||
|
pub fn recipient(&self) -> Address {
|
||||||
|
self.recipient
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the value of this note.
|
/// Returns the value of this note.
|
||||||
pub fn value(&self) -> NoteValue {
|
pub fn value(&self) -> NoteValue {
|
||||||
self.value
|
self.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives the ephemeral secret key for this note.
|
||||||
|
pub(crate) fn rseed(&self) -> &RandomSeed {
|
||||||
|
&self.rseed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives the ephemeral secret key for this note.
|
||||||
|
pub(crate) fn esk(&self) -> EphemeralSecretKey {
|
||||||
|
EphemeralSecretKey(self.rseed.esk(&self.rho))
|
||||||
|
}
|
||||||
|
|
||||||
/// Derives the commitment to this note.
|
/// Derives the commitment to this note.
|
||||||
///
|
///
|
||||||
/// Defined in [Zcash Protocol Spec § 3.2: Notes][notes].
|
/// Defined in [Zcash Protocol Spec § 3.2: Notes][notes].
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::iter;
|
||||||
use bitvec::{array::BitArray, order::Lsb0};
|
use bitvec::{array::BitArray, order::Lsb0};
|
||||||
use ff::PrimeFieldBits;
|
use ff::PrimeFieldBits;
|
||||||
use pasta_curves::{arithmetic::FieldExt, pallas};
|
use pasta_curves::{arithmetic::FieldExt, pallas};
|
||||||
use subtle::CtOption;
|
use subtle::{ConstantTimeEq, CtOption};
|
||||||
|
|
||||||
use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla, spec::extract_p, value::NoteValue};
|
use crate::{constants::L_ORCHARD_BASE, primitives::sinsemilla, spec::extract_p, value::NoteValue};
|
||||||
|
|
||||||
|
@ -71,3 +71,23 @@ impl std::ops::Deref for ExtractedNoteCommitment {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&ExtractedNoteCommitment> for [u8; 32] {
|
||||||
|
fn from(cmx: &ExtractedNoteCommitment) -> Self {
|
||||||
|
cmx.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 {}
|
||||||
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
//! In-band secret distribution for Orchard bundles.
|
||||||
|
|
||||||
|
use std::convert::TryInto;
|
||||||
|
|
||||||
|
use blake2b_simd::{Hash, Params};
|
||||||
|
use halo2::arithmetic::FieldExt;
|
||||||
|
use zcash_note_encryption::{
|
||||||
|
Domain, EphemeralKeyBytes, NotePlaintextBytes, NoteValidity, OutPlaintextBytes,
|
||||||
|
OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bundle::Action,
|
||||||
|
keys::{
|
||||||
|
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
|
||||||
|
IncomingViewingKey, OutgoingViewingKey, SharedSecret,
|
||||||
|
},
|
||||||
|
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
|
||||||
|
spec::diversify_hash,
|
||||||
|
value::{NoteValue, ValueCommitment},
|
||||||
|
Address, Note,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
||||||
|
|
||||||
|
/// 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_ock_orchard(
|
||||||
|
ovk: &OutgoingViewingKey,
|
||||||
|
cv: &ValueCommitment,
|
||||||
|
cmx_bytes: &[u8; 32],
|
||||||
|
ephemeral_key: &EphemeralKeyBytes,
|
||||||
|
) -> OutgoingCipherKey {
|
||||||
|
OutgoingCipherKey(
|
||||||
|
Params::new()
|
||||||
|
.hash_length(32)
|
||||||
|
.personal(PRF_OCK_ORCHARD_PERSONALIZATION)
|
||||||
|
.to_state()
|
||||||
|
.update(ovk.as_ref())
|
||||||
|
.update(&cv.to_bytes())
|
||||||
|
.update(cmx_bytes)
|
||||||
|
.update(ephemeral_key.as_ref())
|
||||||
|
.finalize()
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orchard_parse_note_plaintext_without_memo<F>(
|
||||||
|
domain: &OrchardDomain,
|
||||||
|
plaintext: &[u8],
|
||||||
|
get_validated_pk_d: F,
|
||||||
|
) -> Option<(Note, Address)>
|
||||||
|
where
|
||||||
|
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
||||||
|
{
|
||||||
|
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
|
||||||
|
|
||||||
|
// Check note plaintext version
|
||||||
|
if plaintext[0] != 0x02 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The unwraps below are guaranteed to succeed by the assertion above
|
||||||
|
let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
|
||||||
|
let value = NoteValue::from_bytes(plaintext[12..20].try_into().unwrap());
|
||||||
|
let rseed = Option::from(RandomSeed::from_bytes(
|
||||||
|
plaintext[20..COMPACT_NOTE_SIZE].try_into().unwrap(),
|
||||||
|
&domain.rho,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let pk_d = get_validated_pk_d(&diversifier)?;
|
||||||
|
|
||||||
|
let recipient = Address::from_parts(diversifier, pk_d);
|
||||||
|
let note = Note::from_parts(recipient, value, domain.rho, rseed);
|
||||||
|
Some((note, recipient))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Orchard-specific note encryption logic.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct OrchardDomain {
|
||||||
|
rho: Nullifier,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Domain for OrchardDomain {
|
||||||
|
type EphemeralSecretKey = EphemeralSecretKey;
|
||||||
|
type EphemeralPublicKey = EphemeralPublicKey;
|
||||||
|
type SharedSecret = SharedSecret;
|
||||||
|
type SymmetricKey = Hash;
|
||||||
|
type Note = Note;
|
||||||
|
type Recipient = Address;
|
||||||
|
type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
|
||||||
|
type IncomingViewingKey = IncomingViewingKey;
|
||||||
|
type OutgoingViewingKey = OutgoingViewingKey;
|
||||||
|
type ValueCommitment = ValueCommitment;
|
||||||
|
type ExtractedCommitment = ExtractedNoteCommitment;
|
||||||
|
type ExtractedCommitmentBytes = [u8; 32];
|
||||||
|
type Memo = [u8; 512]; // TODO use a more interesting type
|
||||||
|
|
||||||
|
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
|
||||||
|
Some(note.esk())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_pk_d(note: &Self::Note) -> Self::DiversifiedTransmissionKey {
|
||||||
|
*note.recipient().pk_d()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ka_derive_public(
|
||||||
|
note: &Self::Note,
|
||||||
|
esk: &Self::EphemeralSecretKey,
|
||||||
|
) -> Self::EphemeralPublicKey {
|
||||||
|
esk.derive_public(note.recipient().g_d())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ka_agree_enc(
|
||||||
|
esk: &Self::EphemeralSecretKey,
|
||||||
|
pk_d: &Self::DiversifiedTransmissionKey,
|
||||||
|
) -> Self::SharedSecret {
|
||||||
|
esk.agree(pk_d)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ka_agree_dec(
|
||||||
|
ivk: &Self::IncomingViewingKey,
|
||||||
|
epk: &Self::EphemeralPublicKey,
|
||||||
|
) -> Self::SharedSecret {
|
||||||
|
epk.agree(ivk)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey {
|
||||||
|
secret.kdf_orchard(&ephemeral_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn note_plaintext_bytes(
|
||||||
|
note: &Self::Note,
|
||||||
|
_: &Self::Recipient,
|
||||||
|
memo: &Self::Memo,
|
||||||
|
) -> NotePlaintextBytes {
|
||||||
|
let mut np = [0; NOTE_PLAINTEXT_SIZE];
|
||||||
|
np[0] = 0x02;
|
||||||
|
np[1..12].copy_from_slice(note.recipient().diversifer().as_array());
|
||||||
|
np[12..20].copy_from_slice(¬e.value().to_bytes());
|
||||||
|
np[20..52].copy_from_slice(note.rseed().to_bytes());
|
||||||
|
np[52..].copy_from_slice(memo);
|
||||||
|
NotePlaintextBytes(np)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_ock(
|
||||||
|
ovk: &Self::OutgoingViewingKey,
|
||||||
|
cv: &Self::ValueCommitment,
|
||||||
|
cmstar_bytes: &Self::ExtractedCommitmentBytes,
|
||||||
|
ephemeral_key: &EphemeralKeyBytes,
|
||||||
|
) -> OutgoingCipherKey {
|
||||||
|
prf_ock_orchard(ovk, cv, cmstar_bytes, ephemeral_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outgoing_plaintext_bytes(
|
||||||
|
note: &Self::Note,
|
||||||
|
esk: &Self::EphemeralSecretKey,
|
||||||
|
) -> OutPlaintextBytes {
|
||||||
|
let mut op = [0; OUT_PLAINTEXT_SIZE];
|
||||||
|
op[..32].copy_from_slice(¬e.recipient().pk_d().to_bytes());
|
||||||
|
op[32..].copy_from_slice(&esk.0.to_bytes());
|
||||||
|
OutPlaintextBytes(op)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn epk_bytes(epk: &Self::EphemeralPublicKey) -> EphemeralKeyBytes {
|
||||||
|
epk.to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn epk(ephemeral_key: &EphemeralKeyBytes) -> Option<Self::EphemeralPublicKey> {
|
||||||
|
EphemeralPublicKey::from_bytes(&ephemeral_key.0).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_epk_bytes<F: Fn(&Self::EphemeralSecretKey) -> NoteValidity>(
|
||||||
|
note: &Self::Note,
|
||||||
|
check: F,
|
||||||
|
) -> NoteValidity {
|
||||||
|
check(¬e.esk())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmstar(note: &Self::Note) -> Self::ExtractedCommitment {
|
||||||
|
note.commitment().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_note_plaintext_without_memo_ivk(
|
||||||
|
&self,
|
||||||
|
ivk: &Self::IncomingViewingKey,
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> Option<(Self::Note, Self::Recipient)> {
|
||||||
|
orchard_parse_note_plaintext_without_memo(&self, plaintext, |diversifier| {
|
||||||
|
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_note_plaintext_without_memo_ovk(
|
||||||
|
&self,
|
||||||
|
pk_d: &Self::DiversifiedTransmissionKey,
|
||||||
|
esk: &Self::EphemeralSecretKey,
|
||||||
|
ephemeral_key: &EphemeralKeyBytes,
|
||||||
|
plaintext: &[u8],
|
||||||
|
) -> Option<(Self::Note, Self::Recipient)> {
|
||||||
|
orchard_parse_note_plaintext_without_memo(&self, plaintext, |diversifier| {
|
||||||
|
if esk
|
||||||
|
.derive_public(diversify_hash(diversifier.as_array()))
|
||||||
|
.to_bytes()
|
||||||
|
.0
|
||||||
|
== ephemeral_key.0
|
||||||
|
{
|
||||||
|
Some(*pk_d)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_memo(&self, plaintext: &[u8]) -> Self::Memo {
|
||||||
|
plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
|
||||||
|
.try_into()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_pk_d(
|
||||||
|
out_plaintext: &[u8; OUT_PLAINTEXT_SIZE],
|
||||||
|
) -> Option<Self::DiversifiedTransmissionKey> {
|
||||||
|
DiversifiedTransmissionKey::from_bytes(out_plaintext[0..32].try_into().unwrap()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_esk(out_plaintext: &[u8; OUT_PLAINTEXT_SIZE]) -> Option<Self::EphemeralSecretKey> {
|
||||||
|
EphemeralSecretKey::from_bytes(out_plaintext[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
|
||||||
|
|
||||||
|
impl<T> ShieldedOutput<OrchardDomain> for Action<T> {
|
||||||
|
fn ephemeral_key(&self) -> EphemeralKeyBytes {
|
||||||
|
EphemeralKeyBytes(self.encrypted_note().epk_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmstar_bytes(&self) -> [u8; 32] {
|
||||||
|
self.cmx().to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_ciphertext(&self) -> &[u8] {
|
||||||
|
&self.encrypted_note().enc_ciphertext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CompactAction {
|
||||||
|
ephemeral_key: EphemeralKeyBytes,
|
||||||
|
cmx: ExtractedNoteCommitment,
|
||||||
|
enc_ciphertext: [u8; 52],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<&Action<T>> for CompactAction {
|
||||||
|
fn from(action: &Action<T>) -> Self {
|
||||||
|
CompactAction {
|
||||||
|
ephemeral_key: action.ephemeral_key(),
|
||||||
|
cmx: action.cmx().clone(),
|
||||||
|
enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
|
||||||
|
.try_into()
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShieldedOutput<OrchardDomain> for CompactAction {
|
||||||
|
fn ephemeral_key(&self) -> EphemeralKeyBytes {
|
||||||
|
EphemeralKeyBytes(self.ephemeral_key.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmstar_bytes(&self) -> [u8; 32] {
|
||||||
|
self.cmx.to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_ciphertext(&self) -> &[u8] {
|
||||||
|
&self.enc_ciphertext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use zcash_note_encryption::{
|
||||||
|
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
|
||||||
|
EphemeralKeyBytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
|
||||||
|
use crate::{
|
||||||
|
bundle::Action,
|
||||||
|
keys::{
|
||||||
|
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
|
||||||
|
OutgoingViewingKey,
|
||||||
|
},
|
||||||
|
note::{ExtractedNoteCommitment, Nullifier, RandomSeed, TransmittedNoteCiphertext},
|
||||||
|
primitives::redpallas,
|
||||||
|
value::{NoteValue, ValueCommitment},
|
||||||
|
Address, Note,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_vectors() {
|
||||||
|
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
|
||||||
|
|
||||||
|
for tv in test_vectors {
|
||||||
|
//
|
||||||
|
// Load the test vector components
|
||||||
|
//
|
||||||
|
|
||||||
|
// Recipient key material
|
||||||
|
let ivk = 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();
|
||||||
|
|
||||||
|
// Received Action
|
||||||
|
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
|
||||||
|
let rho = Nullifier::from_bytes(&tv.rho).unwrap();
|
||||||
|
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
|
||||||
|
|
||||||
|
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
|
||||||
|
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
|
||||||
|
|
||||||
|
// Details about the expected note
|
||||||
|
let value = NoteValue::from_raw(tv.v);
|
||||||
|
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test the individual components
|
||||||
|
//
|
||||||
|
|
||||||
|
let shared_secret = esk.agree(&pk_d);
|
||||||
|
assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
|
||||||
|
|
||||||
|
let k_enc = shared_secret.kdf_orchard(&ephemeral_key);
|
||||||
|
assert_eq!(k_enc.as_bytes(), tv.k_enc);
|
||||||
|
|
||||||
|
let ock = prf_ock_orchard(&ovk, &cv_net, &cmx.to_bytes(), &ephemeral_key);
|
||||||
|
assert_eq!(ock.as_ref(), tv.ock);
|
||||||
|
|
||||||
|
let recipient = Address::from_parts(d, pk_d);
|
||||||
|
let note = Note::from_parts(recipient, value, rho, rseed);
|
||||||
|
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||||
|
|
||||||
|
let action = Action::from_parts(
|
||||||
|
// rho is the nullifier in the receiving Action.
|
||||||
|
rho,
|
||||||
|
// We don't need a valid rk for this test.
|
||||||
|
redpallas::VerificationKey::dummy(),
|
||||||
|
cmx.clone(),
|
||||||
|
TransmittedNoteCiphertext {
|
||||||
|
epk_bytes: ephemeral_key.0,
|
||||||
|
enc_ciphertext: tv.c_enc,
|
||||||
|
out_ciphertext: tv.c_out,
|
||||||
|
},
|
||||||
|
cv_net.clone(),
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test decryption
|
||||||
|
// (Tested first because it only requires immutable references.)
|
||||||
|
//
|
||||||
|
|
||||||
|
let domain = OrchardDomain { rho };
|
||||||
|
|
||||||
|
match try_note_decryption(&domain, &ivk, &action) {
|
||||||
|
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
|
||||||
|
assert_eq!(decrypted_note, note);
|
||||||
|
assert_eq!(decrypted_to, recipient);
|
||||||
|
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
|
||||||
|
}
|
||||||
|
None => panic!("Note decryption failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match try_compact_note_decryption(&domain, &ivk, &CompactAction::from(&action)) {
|
||||||
|
Some((decrypted_note, decrypted_to)) => {
|
||||||
|
assert_eq!(decrypted_note, note);
|
||||||
|
assert_eq!(decrypted_to, recipient);
|
||||||
|
}
|
||||||
|
None => panic!("Compact note decryption failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
|
||||||
|
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
|
||||||
|
assert_eq!(decrypted_note, note);
|
||||||
|
assert_eq!(decrypted_to, recipient);
|
||||||
|
assert_eq!(&decrypted_memo[..], &tv.memo[..]);
|
||||||
|
}
|
||||||
|
None => panic!("Output recovery failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Test encryption
|
||||||
|
//
|
||||||
|
|
||||||
|
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, recipient, tv.memo);
|
||||||
|
|
||||||
|
assert_eq!(ne.encrypt_note_plaintext().as_ref(), &tv.c_enc[..]);
|
||||||
|
assert_eq!(
|
||||||
|
&ne.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut OsRng)[..],
|
||||||
|
&tv.c_out[..]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,9 @@ use std::convert::{TryFrom, TryInto};
|
||||||
use pasta_curves::pallas;
|
use pasta_curves::pallas;
|
||||||
use rand::{CryptoRng, RngCore};
|
use rand::{CryptoRng, RngCore};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
/// A RedPallas signature type.
|
/// A RedPallas signature type.
|
||||||
pub trait SigType: reddsa::SigType + private::Sealed {}
|
pub trait SigType: reddsa::SigType + private::Sealed {}
|
||||||
|
|
||||||
|
@ -93,6 +96,12 @@ impl<T: SigType> PartialEq for VerificationKey<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VerificationKey<SpendAuth> {
|
impl VerificationKey<SpendAuth> {
|
||||||
|
/// Used in the note encryption tests.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn dummy() -> Self {
|
||||||
|
VerificationKey((&reddsa::SigningKey::new(OsRng)).into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Randomizes this verification key with the given `randomizer`.
|
/// Randomizes this verification key with the given `randomizer`.
|
||||||
///
|
///
|
||||||
/// Randomization is only supported for `SpendAuth` keys.
|
/// Randomization is only supported for `SpendAuth` keys.
|
||||||
|
|
67
src/spec.rs
67
src/spec.rs
|
@ -4,10 +4,11 @@ use std::iter;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use ff::{Field, PrimeField, PrimeFieldBits};
|
use ff::{Field, PrimeField, PrimeFieldBits};
|
||||||
|
use group::GroupEncoding;
|
||||||
use group::{Curve, Group};
|
use group::{Curve, Group};
|
||||||
use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt};
|
use halo2::arithmetic::{CurveAffine, CurveExt, FieldExt};
|
||||||
use pasta_curves::pallas;
|
use pasta_curves::pallas;
|
||||||
use subtle::CtOption;
|
use subtle::{ConditionallySelectable, CtOption};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::L_ORCHARD_BASE,
|
constants::L_ORCHARD_BASE,
|
||||||
|
@ -18,9 +19,28 @@ mod prf_expand;
|
||||||
pub(crate) use prf_expand::PrfExpand;
|
pub(crate) use prf_expand::PrfExpand;
|
||||||
|
|
||||||
/// A Pallas point that is guaranteed to not be the identity.
|
/// A Pallas point that is guaranteed to not be the identity.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct NonIdentityPallasPoint(pallas::Point);
|
pub(crate) struct NonIdentityPallasPoint(pallas::Point);
|
||||||
|
|
||||||
|
impl Default for NonIdentityPallasPoint {
|
||||||
|
fn default() -> Self {
|
||||||
|
NonIdentityPallasPoint(pallas::Point::generator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConditionallySelectable for NonIdentityPallasPoint {
|
||||||
|
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
|
||||||
|
NonIdentityPallasPoint(pallas::Point::conditional_select(&a.0, &b.0, choice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NonIdentityPallasPoint {
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
pallas::Point::from_bytes(bytes)
|
||||||
|
.and_then(|p| CtOption::new(NonIdentityPallasPoint(p), !p.is_identity()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for NonIdentityPallasPoint {
|
impl Deref for NonIdentityPallasPoint {
|
||||||
type Target = pallas::Point;
|
type Target = pallas::Point;
|
||||||
|
|
||||||
|
@ -30,9 +50,30 @@ impl Deref for NonIdentityPallasPoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An integer in [1..q_P].
|
/// An integer in [1..q_P].
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct NonZeroPallasBase(pallas::Base);
|
pub(crate) struct NonZeroPallasBase(pallas::Base);
|
||||||
|
|
||||||
|
impl Default for NonZeroPallasBase {
|
||||||
|
fn default() -> Self {
|
||||||
|
NonZeroPallasBase(pallas::Base::one())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConditionallySelectable for NonZeroPallasBase {
|
||||||
|
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
|
||||||
|
NonZeroPallasBase(pallas::Base::conditional_select(&a.0, &b.0, choice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NonZeroPallasBase {
|
impl NonZeroPallasBase {
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
pallas::Base::from_bytes(bytes).and_then(NonZeroPallasBase::from_base)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_base(b: pallas::Base) -> CtOption<Self> {
|
||||||
|
CtOption::new(NonZeroPallasBase(b), !b.ct_is_zero())
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs a wrapper for a base field element that is guaranteed to be non-zero.
|
/// Constructs a wrapper for a base field element that is guaranteed to be non-zero.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
@ -45,16 +86,36 @@ impl NonZeroPallasBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An integer in [1..r_P].
|
/// An integer in [1..r_P].
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct NonZeroPallasScalar(pallas::Scalar);
|
pub(crate) struct NonZeroPallasScalar(pallas::Scalar);
|
||||||
|
|
||||||
|
impl Default for NonZeroPallasScalar {
|
||||||
|
fn default() -> Self {
|
||||||
|
NonZeroPallasScalar(pallas::Scalar::one())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<NonZeroPallasBase> for NonZeroPallasScalar {
|
impl From<NonZeroPallasBase> for NonZeroPallasScalar {
|
||||||
fn from(s: NonZeroPallasBase) -> Self {
|
fn from(s: NonZeroPallasBase) -> Self {
|
||||||
NonZeroPallasScalar::guaranteed(mod_r_p(s.0))
|
NonZeroPallasScalar::guaranteed(mod_r_p(s.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConditionallySelectable for NonZeroPallasScalar {
|
||||||
|
fn conditional_select(a: &Self, b: &Self, choice: subtle::Choice) -> Self {
|
||||||
|
NonZeroPallasScalar(pallas::Scalar::conditional_select(&a.0, &b.0, choice))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl NonZeroPallasScalar {
|
impl NonZeroPallasScalar {
|
||||||
|
pub(crate) fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
|
||||||
|
pallas::Scalar::from_bytes(bytes).and_then(NonZeroPallasScalar::from_scalar)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_scalar(s: pallas::Scalar) -> CtOption<Self> {
|
||||||
|
CtOption::new(NonZeroPallasScalar(s), !s.ct_is_zero())
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs a wrapper for a scalar field element that is guaranteed to be non-zero.
|
/// Constructs a wrapper for a scalar field element that is guaranteed to be non-zero.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
pub(crate) mod commitment_tree;
|
pub(crate) mod commitment_tree;
|
||||||
pub(crate) mod keys;
|
pub(crate) mod keys;
|
||||||
|
pub(crate) mod note_encryption;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -74,6 +74,14 @@ impl NoteValue {
|
||||||
NoteValue(value)
|
NoteValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self {
|
||||||
|
NoteValue(u64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_bytes(self) -> [u8; 8] {
|
||||||
|
self.0.to_le_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn to_le_bits(self) -> BitArray<Lsb0, [u8; 8]> {
|
pub(crate) fn to_le_bits(self) -> BitArray<Lsb0, [u8; 8]> {
|
||||||
BitArray::<Lsb0, _>::new(self.0.to_le_bytes())
|
BitArray::<Lsb0, _>::new(self.0.to_le_bytes())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue