Store OutputDescription `ephemeral_key` as bytes

This removes an unnecessary `to_bytes` during trial decryption of notes,
and more closely matches the protocol spec. We retain the consensus rule
canonicity check on epk due to `SaplingVerificationContext::check_output`
taking a `jubjub::ExtendedPoint`, forcing `zcashd` to parse the bytes.
This commit is contained in:
Jack Grigg 2021-08-06 16:54:48 +01:00
parent 99d877e22d
commit 83c6a2d1ca
8 changed files with 51 additions and 44 deletions

View File

@ -1,7 +1,6 @@
//! Generated code for handling light client protobuf structs. //! Generated code for handling light client protobuf structs.
use ff::PrimeField; use ff::PrimeField;
use group::GroupEncoding;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use zcash_primitives::{ use zcash_primitives::{
@ -103,13 +102,8 @@ impl compact_formats::CompactOutput {
/// A convenience method that parses [`CompactOutput.epk`]. /// A convenience method that parses [`CompactOutput.epk`].
/// ///
/// [`CompactOutput.epk`]: #structfield.epk /// [`CompactOutput.epk`]: #structfield.epk
pub fn epk(&self) -> Result<jubjub::ExtendedPoint, ()> { pub fn ephemeral_key(&self) -> Result<[u8; 32], ()> {
let p = jubjub::ExtendedPoint::from_bytes(&self.epk[..].try_into().map_err(|_| ())?); self.epk[..].try_into().map_err(|_| ())
if p.is_some().into() {
Ok(p.unwrap())
} else {
Err(())
}
} }
} }
@ -117,7 +111,7 @@ impl<A: sapling::Authorization> From<OutputDescription<A>> for compact_formats::
fn from(out: OutputDescription<A>) -> compact_formats::CompactOutput { fn from(out: OutputDescription<A>) -> compact_formats::CompactOutput {
let mut result = compact_formats::CompactOutput::new(); let mut result = compact_formats::CompactOutput::new();
result.set_cmu(out.cmu.to_repr().to_vec()); result.set_cmu(out.cmu.to_repr().to_vec());
result.set_epk(out.ephemeral_key.to_bytes().to_vec()); result.set_epk(out.ephemeral_key.to_vec());
result.set_ciphertext(out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec()); result.set_ciphertext(out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec());
result result
} }
@ -129,7 +123,7 @@ impl TryFrom<compact_formats::CompactOutput> for CompactOutputDescription {
fn try_from(value: compact_formats::CompactOutput) -> Result<Self, Self::Error> { fn try_from(value: compact_formats::CompactOutput) -> Result<Self, Self::Error> {
Ok(CompactOutputDescription { Ok(CompactOutputDescription {
cmu: value.cmu()?, cmu: value.cmu()?,
epk: value.epk()?, ephemeral_key: value.ephemeral_key()?,
enc_ciphertext: value.ciphertext, enc_ciphertext: value.ciphertext,
}) })
} }

View File

@ -54,7 +54,7 @@ pub struct WalletShieldedSpend {
pub struct WalletShieldedOutput<N> { pub struct WalletShieldedOutput<N> {
pub index: usize, pub index: usize,
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub epk: jubjub::ExtendedPoint, pub ephemeral_key: [u8; 32],
pub account: AccountId, pub account: AccountId,
pub note: Note, pub note: Note,
pub to: PaymentAddress, pub to: PaymentAddress,

View File

@ -76,7 +76,7 @@ fn scan_output<P: consensus::Parameters, K: ScanningKey>(
return Some(WalletShieldedOutput { return Some(WalletShieldedOutput {
index, index,
cmu: output.cmu, cmu: output.cmu,
epk: output.epk, ephemeral_key: output.ephemeral_key,
account: **account, account: **account,
note, note,
to, to,

View File

@ -1,5 +1,6 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field; use ff::Field;
use group::GroupEncoding;
use rand_core::OsRng; use rand_core::OsRng;
use zcash_primitives::{ use zcash_primitives::{
consensus::{NetworkUpgrade::Canopy, Parameters, TestNetwork, TEST_NETWORK}, consensus::{NetworkUpgrade::Canopy, Parameters, TestNetwork, TEST_NETWORK},
@ -49,7 +50,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let ne = let ne =
sapling_note_encryption::<_, TestNetwork>(None, note, pa, MemoBytes::empty(), &mut rng); sapling_note_encryption::<_, TestNetwork>(None, note, pa, MemoBytes::empty(), &mut rng);
let ephemeral_key = *ne.epk(); let ephemeral_key = ne.epk().to_bytes();
let enc_ciphertext = ne.encrypt_note_plaintext(); let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng); let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng);

View File

@ -436,8 +436,8 @@ mod tests {
use std::convert::TryInto; use std::convert::TryInto;
use zcash_note_encryption::{ use zcash_note_encryption::{
NoteEncryption, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, EphemeralKeyBytes, NoteEncryption, OutgoingCipherKey, ENC_CIPHERTEXT_SIZE,
OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE,
}; };
use super::{ use super::{
@ -538,7 +538,7 @@ mod tests {
let output = OutputDescription { let output = OutputDescription {
cv, cv,
cmu, cmu,
ephemeral_key: epk, ephemeral_key: epk.to_bytes(),
enc_ciphertext: ne.encrypt_note_plaintext(), enc_ciphertext: ne.encrypt_note_plaintext(),
out_ciphertext: ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng), out_ciphertext: ne.encrypt_outgoing_plaintext(&cv, &cmu, &mut rng),
zkproof: [0u8; GROTH_PROOF_SIZE], zkproof: [0u8; GROTH_PROOF_SIZE],
@ -551,12 +551,17 @@ mod tests {
ovk: &OutgoingViewingKey, ovk: &OutgoingViewingKey,
cv: &jubjub::ExtendedPoint, cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar, cmu: &bls12_381::Scalar,
epk: &jubjub::ExtendedPoint, ephemeral_key: &[u8; 32],
enc_ciphertext: &mut [u8; ENC_CIPHERTEXT_SIZE], enc_ciphertext: &mut [u8; ENC_CIPHERTEXT_SIZE],
out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE], out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]), modify_plaintext: impl Fn(&mut [u8; NOTE_PLAINTEXT_SIZE]),
) { ) {
let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(epk)); let ock = prf_ock(
&ovk,
&cv,
&cmu.to_repr(),
&EphemeralKeyBytes(*ephemeral_key),
);
let mut op = [0; OUT_CIPHERTEXT_SIZE]; let mut op = [0; OUT_CIPHERTEXT_SIZE];
assert_eq!( assert_eq!(
@ -571,7 +576,7 @@ mod tests {
let esk = jubjub::Fr::from_repr(op[32..OUT_PLAINTEXT_SIZE].try_into().unwrap()).unwrap(); let esk = jubjub::Fr::from_repr(op[32..OUT_PLAINTEXT_SIZE].try_into().unwrap()).unwrap();
let shared_secret = sapling_ka_agree(&esk, &pk_d.into()); let shared_secret = sapling_ka_agree(&esk, &pk_d.into());
let key = kdf_sapling(shared_secret, &epk_bytes(&epk)); let key = kdf_sapling(shared_secret, &EphemeralKeyBytes(*ephemeral_key));
let mut plaintext = { let mut plaintext = {
let mut buf = [0; ENC_CIPHERTEXT_SIZE]; let mut buf = [0; ENC_CIPHERTEXT_SIZE];
@ -664,7 +669,7 @@ mod tests {
for &height in heights.iter() { for &height in heights.iter() {
let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng).to_bytes();
assert_eq!( assert_eq!(
try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output,), try_sapling_note_decryption(&TEST_NETWORK, height, &ivk, &output,),
@ -829,7 +834,7 @@ mod tests {
for &height in heights.iter() { for &height in heights.iter() {
let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng); let (_, _, ivk, mut output) = random_enc_ciphertext(height, &mut rng);
output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng).to_bytes();
assert_eq!( assert_eq!(
try_sapling_compact_note_decryption( try_sapling_compact_note_decryption(
@ -1061,7 +1066,7 @@ mod tests {
for &height in heights.iter() { for &height in heights.iter() {
let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng); let (ovk, ock, _, mut output) = random_enc_ciphertext(height, &mut rng);
output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng); output.ephemeral_key = jubjub::ExtendedPoint::random(&mut rng).to_bytes();
assert_eq!( assert_eq!(
try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,), try_sapling_output_recovery(&TEST_NETWORK, height, &ovk, &output,),
@ -1275,7 +1280,7 @@ mod tests {
let cv = read_point!(tv.cv); let cv = read_point!(tv.cv);
let cmu = read_bls12_381_scalar!(tv.cmu); let cmu = read_bls12_381_scalar!(tv.cmu);
let esk = read_jubjub_scalar!(tv.esk); let esk = read_jubjub_scalar!(tv.esk);
let epk = read_point!(tv.epk); let ephemeral_key = EphemeralKeyBytes(tv.epk);
// //
// Test the individual components // Test the individual components
@ -1284,11 +1289,11 @@ mod tests {
let shared_secret = sapling_ka_agree(&esk, &pk_d.into()); let shared_secret = sapling_ka_agree(&esk, &pk_d.into());
assert_eq!(shared_secret.to_bytes(), tv.shared_secret); assert_eq!(shared_secret.to_bytes(), tv.shared_secret);
let k_enc = kdf_sapling(shared_secret, &epk_bytes(&epk)); let k_enc = kdf_sapling(shared_secret, &ephemeral_key);
assert_eq!(k_enc.as_bytes(), tv.k_enc); assert_eq!(k_enc.as_bytes(), tv.k_enc);
let ovk = OutgoingViewingKey(tv.ovk); let ovk = OutgoingViewingKey(tv.ovk);
let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &epk_bytes(&epk)); let ock = prf_ock(&ovk, &cv, &cmu.to_repr(), &ephemeral_key);
assert_eq!(ock.as_ref(), tv.ock); assert_eq!(ock.as_ref(), tv.ock);
let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap(); let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap();
@ -1298,7 +1303,7 @@ mod tests {
let output = OutputDescription { let output = OutputDescription {
cv, cv,
cmu, cmu,
ephemeral_key: epk, ephemeral_key: ephemeral_key.0,
enc_ciphertext: tv.c_enc, enc_ciphertext: tv.c_enc,
out_ciphertext: tv.c_out, out_ciphertext: tv.c_out,
zkproof: [0u8; GROTH_PROOF_SIZE], zkproof: [0u8; GROTH_PROOF_SIZE],

View File

@ -213,7 +213,7 @@ impl SpendDescriptionV5 {
pub struct OutputDescription<Proof> { pub struct OutputDescription<Proof> {
pub cv: jubjub::ExtendedPoint, pub cv: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub ephemeral_key: jubjub::ExtendedPoint, pub ephemeral_key: [u8; 32],
pub enc_ciphertext: [u8; 580], pub enc_ciphertext: [u8; 580],
pub out_ciphertext: [u8; 80], pub out_ciphertext: [u8; 80],
pub zkproof: Proof, pub zkproof: Proof,
@ -221,7 +221,7 @@ pub struct OutputDescription<Proof> {
impl<P: consensus::Parameters, A> ShieldedOutput<SaplingDomain<P>> for OutputDescription<A> { impl<P: consensus::Parameters, A> ShieldedOutput<SaplingDomain<P>> for OutputDescription<A> {
fn ephemeral_key(&self) -> EphemeralKeyBytes { fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.ephemeral_key.to_bytes()) EphemeralKeyBytes(self.ephemeral_key)
} }
fn cmstar_bytes(&self) -> [u8; 32] { fn cmstar_bytes(&self) -> [u8; 32] {
@ -255,9 +255,10 @@ impl OutputDescription<GrothProofBytes> {
let cmu = read_base(&mut reader, "cmu")?; let cmu = read_base(&mut reader, "cmu")?;
// Consensus rules (§4.5): // Consensus rules (§4.5):
// - Canonical encoding is enforced here. // - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd
// - "Not small order" is enforced in SaplingVerificationContext::check_output() // - "Not small order" is enforced in SaplingVerificationContext::check_output()
let ephemeral_key = read_point(&mut reader, "ephemeral key")?; let mut ephemeral_key = [0u8; 32];
reader.read_exact(&mut ephemeral_key)?;
let mut enc_ciphertext = [0u8; 580]; let mut enc_ciphertext = [0u8; 580];
let mut out_ciphertext = [0u8; 80]; let mut out_ciphertext = [0u8; 80];
@ -279,7 +280,7 @@ impl OutputDescription<GrothProofBytes> {
pub fn write_v4<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write_v4<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.cv.to_bytes())?; writer.write_all(&self.cv.to_bytes())?;
writer.write_all(self.cmu.to_repr().as_ref())?; writer.write_all(self.cmu.to_repr().as_ref())?;
writer.write_all(&self.ephemeral_key.to_bytes())?; writer.write_all(&self.ephemeral_key)?;
writer.write_all(&self.enc_ciphertext)?; writer.write_all(&self.enc_ciphertext)?;
writer.write_all(&self.out_ciphertext)?; writer.write_all(&self.out_ciphertext)?;
writer.write_all(&self.zkproof) writer.write_all(&self.zkproof)
@ -288,7 +289,7 @@ impl OutputDescription<GrothProofBytes> {
pub fn write_v5_without_proof<W: Write>(&self, mut writer: W) -> io::Result<()> { pub fn write_v5_without_proof<W: Write>(&self, mut writer: W) -> io::Result<()> {
writer.write_all(&self.cv.to_bytes())?; writer.write_all(&self.cv.to_bytes())?;
writer.write_all(self.cmu.to_repr().as_ref())?; writer.write_all(self.cmu.to_repr().as_ref())?;
writer.write_all(&self.ephemeral_key.to_bytes())?; writer.write_all(&self.ephemeral_key)?;
writer.write_all(&self.enc_ciphertext)?; writer.write_all(&self.enc_ciphertext)?;
writer.write_all(&self.out_ciphertext) writer.write_all(&self.out_ciphertext)
} }
@ -298,7 +299,7 @@ impl OutputDescription<GrothProofBytes> {
pub struct OutputDescriptionV5 { pub struct OutputDescriptionV5 {
pub cv: jubjub::ExtendedPoint, pub cv: jubjub::ExtendedPoint,
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub ephemeral_key: jubjub::ExtendedPoint, pub ephemeral_key: [u8; 32],
pub enc_ciphertext: [u8; 580], pub enc_ciphertext: [u8; 580],
pub out_ciphertext: [u8; 80], pub out_ciphertext: [u8; 80],
} }
@ -307,7 +308,12 @@ impl OutputDescriptionV5 {
pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> { pub fn read<R: Read>(mut reader: &mut R) -> io::Result<Self> {
let cv = read_point(&mut reader, "cv")?; let cv = read_point(&mut reader, "cv")?;
let cmu = read_base(&mut reader, "cmu")?; let cmu = read_base(&mut reader, "cmu")?;
let ephemeral_key = read_point(&mut reader, "ephemeral key")?;
// Consensus rules (§4.5):
// - Canonical encoding is enforced in librustzcash_sapling_check_output by zcashd
// - "Not small order" is enforced in SaplingVerificationContext::check_output()
let mut ephemeral_key = [0u8; 32];
reader.read_exact(&mut ephemeral_key)?;
let mut enc_ciphertext = [0u8; 580]; let mut enc_ciphertext = [0u8; 580];
let mut out_ciphertext = [0u8; 80]; let mut out_ciphertext = [0u8; 80];
@ -339,7 +345,7 @@ impl OutputDescriptionV5 {
} }
pub struct CompactOutputDescription { pub struct CompactOutputDescription {
pub epk: jubjub::ExtendedPoint, pub ephemeral_key: [u8; 32],
pub cmu: bls12_381::Scalar, pub cmu: bls12_381::Scalar,
pub enc_ciphertext: Vec<u8>, pub enc_ciphertext: Vec<u8>,
} }
@ -347,7 +353,7 @@ pub struct CompactOutputDescription {
impl<A> From<OutputDescription<A>> for CompactOutputDescription { impl<A> From<OutputDescription<A>> for CompactOutputDescription {
fn from(out: OutputDescription<A>) -> CompactOutputDescription { fn from(out: OutputDescription<A>) -> CompactOutputDescription {
CompactOutputDescription { CompactOutputDescription {
epk: out.ephemeral_key, ephemeral_key: out.ephemeral_key,
cmu: out.cmu, cmu: out.cmu,
enc_ciphertext: out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec(), enc_ciphertext: out.enc_ciphertext[..COMPACT_NOTE_SIZE].to_vec(),
} }
@ -356,7 +362,7 @@ impl<A> From<OutputDescription<A>> for CompactOutputDescription {
impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>> for CompactOutputDescription { impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>> for CompactOutputDescription {
fn ephemeral_key(&self) -> EphemeralKeyBytes { fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.epk.to_bytes()) EphemeralKeyBytes(self.ephemeral_key)
} }
fn cmstar_bytes(&self) -> [u8; 32] { fn cmstar_bytes(&self) -> [u8; 32] {
@ -371,7 +377,7 @@ impl<P: consensus::Parameters> ShieldedOutput<SaplingDomain<P>> for CompactOutpu
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use ff::Field; use ff::Field;
use group::Group; use group::{Group, GroupEncoding};
use proptest::collection::vec; use proptest::collection::vec;
use proptest::prelude::*; use proptest::prelude::*;
use rand::{rngs::StdRng, SeedableRng}; use rand::{rngs::StdRng, SeedableRng};
@ -438,7 +444,7 @@ pub mod testing {
.prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)), .prop_map(|v| bls12_381::Scalar::from_bytes_wide(&v)),
enc_ciphertext in vec(any::<u8>(), 580) enc_ciphertext in vec(any::<u8>(), 580)
.prop_map(|v| <[u8;580]>::try_from(v.as_slice()).unwrap()), .prop_map(|v| <[u8;580]>::try_from(v.as_slice()).unwrap()),
ephemeral_key in arb_extended_point(), epk in arb_extended_point(),
out_ciphertext in vec(any::<u8>(), 80) out_ciphertext in vec(any::<u8>(), 80)
.prop_map(|v| <[u8;80]>::try_from(v.as_slice()).unwrap()), .prop_map(|v| <[u8;80]>::try_from(v.as_slice()).unwrap()),
zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE) zkproof in vec(any::<u8>(), GROTH_PROOF_SIZE)
@ -447,7 +453,7 @@ pub mod testing {
OutputDescription { OutputDescription {
cv, cv,
cmu, cmu,
ephemeral_key, ephemeral_key: epk.to_bytes(),
enc_ciphertext, enc_ciphertext,
out_ciphertext, out_ciphertext,
zkproof, zkproof,

View File

@ -4,6 +4,7 @@ use std::fmt;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use ff::Field; use ff::Field;
use group::GroupEncoding;
use rand::{seq::SliceRandom, RngCore}; use rand::{seq::SliceRandom, RngCore};
use crate::{ use crate::{
@ -136,12 +137,12 @@ impl SaplingOutput {
let enc_ciphertext = encryptor.encrypt_note_plaintext(); let enc_ciphertext = encryptor.encrypt_note_plaintext();
let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng); let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, rng);
let ephemeral_key = *encryptor.epk(); let epk = *encryptor.epk();
OutputDescription { OutputDescription {
cv, cv,
cmu, cmu,
ephemeral_key, ephemeral_key: epk.to_bytes(),
enc_ciphertext, enc_ciphertext,
out_ciphertext, out_ciphertext,
zkproof, zkproof,
@ -463,7 +464,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
OutputDescription { OutputDescription {
cv, cv,
cmu, cmu,
ephemeral_key: epk.into(), ephemeral_key: epk.to_bytes(),
enc_ciphertext, enc_ciphertext,
out_ciphertext, out_ciphertext,
zkproof, zkproof,

View File

@ -173,7 +173,7 @@ pub(crate) fn hash_sapling_outputs<A>(shielded_outputs: &[OutputDescription<A>])
let mut nh = hasher(ZCASH_SAPLING_OUTPUTS_NONCOMPACT_HASH_PERSONALIZATION); let mut nh = hasher(ZCASH_SAPLING_OUTPUTS_NONCOMPACT_HASH_PERSONALIZATION);
for s_out in shielded_outputs { for s_out in shielded_outputs {
ch.write_all(&s_out.cmu.to_repr().as_ref()).unwrap(); ch.write_all(&s_out.cmu.to_repr().as_ref()).unwrap();
ch.write_all(&s_out.ephemeral_key.to_bytes()).unwrap(); ch.write_all(&s_out.ephemeral_key).unwrap();
ch.write_all(&s_out.enc_ciphertext[..52]).unwrap(); ch.write_all(&s_out.enc_ciphertext[..52]).unwrap();
mh.write_all(&s_out.enc_ciphertext[52..564]).unwrap(); mh.write_all(&s_out.enc_ciphertext[52..564]).unwrap();