mirror of https://github.com/zcash/orchard.git
ZSA note encryption in Orchard crate (#3)
* Circleci project setup (#1) * Added .circleci/config.yml * Added NoteType to Notes * reformated file * updated `derive` for NoteType * added note_type to value commit derivation * rustfmt * updated ci config * updated ci config * updated ci config * updated derive for note_type * added test for arb note_type * added test for `native` note type * zsa-note-encryption: introduce AssetType and encode and decode it in note plaintexts * zsa-note-encryption: extend the size of compact notes to include asset_type * fixed clippy warrnings * rustfmt * zsa-note-encryption: document parsing requirement * zsa-note-encryption: revert support of ZSA compact action * zsa_value: add NoteType method is_native * zsa-note-encryption: remove dependency on changes in the other crate * zsa-note-encryption: extract memo of ZSA notes * zsa-note-encryption: tests (zcash_test_vectors 77c73492) * zsa-note-encryption: simplify roundtrip test * zsa-note-encryption: more test vectors (zcash_test_vectors c10da464) * Circleci project setup (#1) * Added .circleci/config.yml * issuer keys implementation (#5) Implements the issuer keys as IssuerAuthorizingKey -> isk IssuerVerifyingKey -> ik Test vectors generated with zcash_test_vectors repo * Added NoteType to Notes (#2) * Added NoteType to Notes * Added NoteType to value commitment derivation * zsa-note-encryption: use both native and ZSA in proptests * zsa-note-encryption: test vector commit 51398c93 * zsa-note-encryption: fix after merge Co-authored-by: Paul <3682187+PaulLaux@users.noreply.github.com> Co-authored-by: Paul <lauxpaul@protonmail.com> Co-authored-by: Aurélien Nicolas <info@nau.re> Co-authored-by: Daniel Benarroch <danielbenarroch92@gmail.com>
This commit is contained in:
parent
5ae51075db
commit
ac71bc7528
|
@ -2,3 +2,4 @@
|
|||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
.vscode
|
||||
.idea
|
||||
|
|
|
@ -34,6 +34,9 @@ pub const VALUE_COMMITMENT_R_BYTES: [u8; 1] = *b"r";
|
|||
/// SWU hash-to-curve personalization for the note commitment generator
|
||||
pub const NOTE_COMMITMENT_PERSONALIZATION: &str = "z.cash:Orchard-NoteCommit";
|
||||
|
||||
/// SWU hash-to-curve personalization for the ZSA note commitment generator
|
||||
pub const NOTE_ZSA_COMMITMENT_PERSONALIZATION: &str = "z.cash:ZSA-NoteCommit";
|
||||
|
||||
/// SWU hash-to-curve personalization for the IVK commitment generator
|
||||
pub const COMMIT_IVK_PERSONALIZATION: &str = "z.cash:Orchard-CommitIvk";
|
||||
|
||||
|
|
|
@ -235,6 +235,7 @@ impl Note {
|
|||
g_d.to_bytes(),
|
||||
self.recipient.pk_d().to_bytes(),
|
||||
self.value,
|
||||
self.note_type,
|
||||
self.rho.0,
|
||||
self.rseed.psi(&self.rho),
|
||||
self.rseed.rcm(&self.rho),
|
||||
|
|
|
@ -7,7 +7,11 @@ use pasta_curves::pallas;
|
|||
use subtle::{ConstantTimeEq, CtOption};
|
||||
|
||||
use crate::{
|
||||
constants::{fixed_bases::NOTE_COMMITMENT_PERSONALIZATION, L_ORCHARD_BASE},
|
||||
constants::{
|
||||
fixed_bases::{NOTE_COMMITMENT_PERSONALIZATION, NOTE_ZSA_COMMITMENT_PERSONALIZATION},
|
||||
L_ORCHARD_BASE,
|
||||
},
|
||||
note::note_type::NoteType,
|
||||
spec::extract_p,
|
||||
value::NoteValue,
|
||||
};
|
||||
|
@ -41,22 +45,46 @@ impl NoteCommitment {
|
|||
g_d: [u8; 32],
|
||||
pk_d: [u8; 32],
|
||||
v: NoteValue,
|
||||
note_type: NoteType,
|
||||
rho: pallas::Base,
|
||||
psi: pallas::Base,
|
||||
rcm: NoteCommitTrapdoor,
|
||||
) -> CtOption<Self> {
|
||||
let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION);
|
||||
domain
|
||||
.commit(
|
||||
iter::empty()
|
||||
.chain(BitArray::<_, Lsb0>::new(g_d).iter().by_vals())
|
||||
.chain(BitArray::<_, Lsb0>::new(pk_d).iter().by_vals())
|
||||
.chain(v.to_le_bits().iter().by_vals())
|
||||
.chain(rho.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
|
||||
.chain(psi.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
|
||||
&rcm.0,
|
||||
)
|
||||
.map(NoteCommitment)
|
||||
let g_d_bits = BitArray::<_, Lsb0>::new(g_d);
|
||||
let pk_d_bits = BitArray::<_, Lsb0>::new(pk_d);
|
||||
let v_bits = v.to_le_bits();
|
||||
let rho_bits = rho.to_le_bits();
|
||||
let psi_bits = psi.to_le_bits();
|
||||
|
||||
let zec_note_bits = iter::empty()
|
||||
.chain(g_d_bits.iter().by_vals())
|
||||
.chain(pk_d_bits.iter().by_vals())
|
||||
.chain(v_bits.iter().by_vals())
|
||||
.chain(rho_bits.iter().by_vals().take(L_ORCHARD_BASE))
|
||||
.chain(psi_bits.iter().by_vals().take(L_ORCHARD_BASE));
|
||||
|
||||
// TODO: make this constant-time.
|
||||
if note_type.is_native().into() {
|
||||
// Commit to ZEC notes as per the Orchard protocol.
|
||||
Self::commit(NOTE_COMMITMENT_PERSONALIZATION, zec_note_bits, rcm)
|
||||
} else {
|
||||
// Commit to non-ZEC notes as per the ZSA protocol.
|
||||
// Append the note type to the Orchard note encoding.
|
||||
let type_bits = BitArray::<_, Lsb0>::new(note_type.to_bytes());
|
||||
let zsa_note_bits = zec_note_bits.chain(type_bits.iter().by_vals());
|
||||
|
||||
// Commit in a different domain than Orchard notes.
|
||||
Self::commit(NOTE_ZSA_COMMITMENT_PERSONALIZATION, zsa_note_bits, rcm)
|
||||
}
|
||||
}
|
||||
|
||||
fn commit(
|
||||
personalization: &str,
|
||||
bits: impl Iterator<Item = bool>,
|
||||
rcm: NoteCommitTrapdoor,
|
||||
) -> CtOption<Self> {
|
||||
let domain = sinsemilla::CommitDomain::new(personalization);
|
||||
domain.commit(bits, &rcm.0).map(NoteCommitment)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use group::GroupEncoding;
|
||||
use halo2_proofs::arithmetic::CurveExt;
|
||||
use pasta_curves::pallas;
|
||||
use subtle::CtOption;
|
||||
use subtle::{Choice, ConstantTimeEq, CtOption};
|
||||
|
||||
use crate::constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_V_BYTES};
|
||||
use crate::keys::IssuerValidatingKey;
|
||||
|
@ -15,7 +15,7 @@ const MAX_ASSET_DESCRIPTION_SIZE: usize = 512;
|
|||
// the hasher used to derive the assetID
|
||||
#[allow(non_snake_case)]
|
||||
fn assetID_hasher(msg: Vec<u8>) -> pallas::Point {
|
||||
// TODO(zsa) replace personalization, will require circuit change?
|
||||
// TODO(zsa) replace personalization
|
||||
pallas::Point::hash_to_curve(VALUE_COMMITMENT_PERSONALIZATION)(&msg)
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,16 @@ impl NoteType {
|
|||
pub fn native() -> Self {
|
||||
NoteType(assetID_hasher(VALUE_COMMITMENT_V_BYTES.to_vec()))
|
||||
}
|
||||
|
||||
/// The base point used in value commitments.
|
||||
pub fn cv_base(&self) -> pallas::Point {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Whether this note represents a native or ZSA asset.
|
||||
pub fn is_native(&self) -> Choice {
|
||||
self.0.ct_eq(&Self::native().0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generators for property testing.
|
||||
|
@ -58,20 +68,25 @@ impl NoteType {
|
|||
pub mod testing {
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::NoteType;
|
||||
|
||||
use crate::keys::{testing::arb_spending_key, IssuerAuthorizingKey, IssuerValidatingKey};
|
||||
|
||||
use super::NoteType;
|
||||
|
||||
prop_compose! {
|
||||
/// Generate a uniformly distributed note type
|
||||
pub fn arb_note_type()(
|
||||
is_native in prop::bool::ANY,
|
||||
sk in arb_spending_key(),
|
||||
bytes32a in prop::array::uniform32(prop::num::u8::ANY),
|
||||
bytes32b in prop::array::uniform32(prop::num::u8::ANY),
|
||||
) -> NoteType {
|
||||
let bytes64 = [bytes32a, bytes32b].concat();
|
||||
let isk = IssuerAuthorizingKey::from(&sk);
|
||||
NoteType::derive(&IssuerValidatingKey::from(&isk), bytes64)
|
||||
if is_native {
|
||||
NoteType::native()
|
||||
} else {
|
||||
let bytes64 = [bytes32a, bytes32b].concat();
|
||||
let isk = IssuerAuthorizingKey::from(&sk);
|
||||
NoteType::derive(&IssuerValidatingKey::from(&isk), bytes64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
//! In-band secret distribution for Orchard bundles.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use blake2b_simd::{Hash, Params};
|
||||
use core::fmt;
|
||||
use group::ff::PrimeField;
|
||||
use zcash_note_encryption::{
|
||||
BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
|
||||
|
@ -25,6 +24,15 @@ use crate::{
|
|||
|
||||
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
||||
|
||||
/// The size of the encoding of a ZSA asset type.
|
||||
const ZSA_TYPE_SIZE: usize = 32;
|
||||
/// The size of the ZSA variant of COMPACT_NOTE_SIZE.
|
||||
const COMPACT_ZSA_NOTE_SIZE: usize = COMPACT_NOTE_SIZE + ZSA_TYPE_SIZE;
|
||||
/// The size of the memo.
|
||||
const MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_NOTE_SIZE;
|
||||
/// The size of the ZSA variant of the memo.
|
||||
const ZSA_MEMO_SIZE: usize = NOTE_PLAINTEXT_SIZE - COMPACT_ZSA_NOTE_SIZE;
|
||||
|
||||
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
|
||||
///
|
||||
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
|
||||
|
@ -50,6 +58,8 @@ pub(crate) fn prf_ock_orchard(
|
|||
)
|
||||
}
|
||||
|
||||
/// Domain-specific requirements:
|
||||
/// - If the note version is 3, the `plaintext` must contain a valid encoding of a ZSA asset type.
|
||||
fn orchard_parse_note_plaintext_without_memo<F>(
|
||||
domain: &OrchardDomain,
|
||||
plaintext: &[u8],
|
||||
|
@ -61,9 +71,8 @@ where
|
|||
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
|
||||
|
||||
// Check note plaintext version
|
||||
if plaintext[0] != 0x02 {
|
||||
return None;
|
||||
}
|
||||
// and parse the asset type accordingly.
|
||||
let note_type = parse_version_and_asset_type(plaintext)?;
|
||||
|
||||
// The unwraps below are guaranteed to succeed by the assertion above
|
||||
let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
|
||||
|
@ -76,11 +85,25 @@ where
|
|||
let pk_d = get_validated_pk_d(&diversifier)?;
|
||||
|
||||
let recipient = Address::from_parts(diversifier, pk_d);
|
||||
// TODO: add note_type
|
||||
let note = Note::from_parts(recipient, value, NoteType::native(), domain.rho, rseed);
|
||||
|
||||
let note = Note::from_parts(recipient, value, note_type, domain.rho, rseed);
|
||||
Some((note, recipient))
|
||||
}
|
||||
|
||||
fn parse_version_and_asset_type(plaintext: &[u8]) -> Option<NoteType> {
|
||||
// TODO: make this constant-time?
|
||||
match plaintext[0] {
|
||||
0x02 => Some(NoteType::native()),
|
||||
0x03 if plaintext.len() >= COMPACT_ZSA_NOTE_SIZE => {
|
||||
let bytes = &plaintext[COMPACT_NOTE_SIZE..COMPACT_ZSA_NOTE_SIZE]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
NoteType::from_bytes(bytes).into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Orchard-specific note encryption logic.
|
||||
#[derive(Debug)]
|
||||
pub struct OrchardDomain {
|
||||
|
@ -114,7 +137,7 @@ impl Domain for OrchardDomain {
|
|||
type ValueCommitment = ValueCommitment;
|
||||
type ExtractedCommitment = ExtractedNoteCommitment;
|
||||
type ExtractedCommitmentBytes = [u8; 32];
|
||||
type Memo = [u8; 512]; // TODO use a more interesting type
|
||||
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
|
||||
|
||||
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
|
||||
Some(note.esk())
|
||||
|
@ -154,13 +177,23 @@ impl Domain for OrchardDomain {
|
|||
_: &Self::Recipient,
|
||||
memo: &Self::Memo,
|
||||
) -> NotePlaintextBytes {
|
||||
let is_native: bool = note.note_type().is_native().into();
|
||||
|
||||
let mut np = [0; NOTE_PLAINTEXT_SIZE];
|
||||
np[0] = 0x02;
|
||||
np[0] = if is_native { 0x02 } else { 0x03 };
|
||||
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
|
||||
np[12..20].copy_from_slice(¬e.value().to_bytes());
|
||||
// todo: add note_type
|
||||
np[20..52].copy_from_slice(note.rseed().as_bytes());
|
||||
np[52..].copy_from_slice(memo);
|
||||
if is_native {
|
||||
np[52..].copy_from_slice(memo);
|
||||
} else {
|
||||
let zsa_type = note.note_type().to_bytes();
|
||||
np[52..84].copy_from_slice(&zsa_type);
|
||||
let short_memo = &memo[0..memo.len() - ZSA_TYPE_SIZE];
|
||||
np[84..].copy_from_slice(short_memo);
|
||||
// TODO: handle full-size memo or make short_memo explicit.
|
||||
};
|
||||
NotePlaintextBytes(np)
|
||||
}
|
||||
|
||||
|
@ -227,9 +260,20 @@ impl Domain for OrchardDomain {
|
|||
}
|
||||
|
||||
fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
|
||||
plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
|
||||
.try_into()
|
||||
.unwrap()
|
||||
let mut memo = [0; MEMO_SIZE];
|
||||
match get_note_version(plaintext) {
|
||||
0x02 => {
|
||||
let full_memo = &plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
|
||||
memo.copy_from_slice(full_memo);
|
||||
}
|
||||
0x03 => {
|
||||
// ZSA note plaintext have a shorter memo.
|
||||
let short_memo = &plaintext.0[COMPACT_ZSA_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
|
||||
memo[..ZSA_MEMO_SIZE].copy_from_slice(short_memo);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
memo
|
||||
}
|
||||
|
||||
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
|
||||
|
@ -257,6 +301,10 @@ impl BatchDomain for OrchardDomain {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_note_version(plaintext: &NotePlaintextBytes) -> u8 {
|
||||
plaintext.0[0]
|
||||
}
|
||||
|
||||
/// Implementation of in-band secret distribution for Orchard bundles.
|
||||
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
|
||||
|
||||
|
@ -279,7 +327,7 @@ pub struct CompactAction {
|
|||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: [u8; 52],
|
||||
enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
|
||||
}
|
||||
|
||||
impl fmt::Debug for CompactAction {
|
||||
|
@ -294,7 +342,7 @@ impl<T> From<&Action<T>> for CompactAction {
|
|||
nullifier: *action.nullifier(),
|
||||
cmx: *action.cmx(),
|
||||
ephemeral_key: action.ephemeral_key(),
|
||||
enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
|
||||
enc_ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
}
|
||||
|
@ -339,13 +387,13 @@ impl CompactAction {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
use rand::rngs::OsRng;
|
||||
use zcash_note_encryption::{
|
||||
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
|
||||
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
|
||||
EphemeralKeyBytes,
|
||||
};
|
||||
|
||||
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
|
||||
use crate::note::NoteType;
|
||||
use crate::{
|
||||
action::Action,
|
||||
|
@ -353,12 +401,55 @@ mod tests {
|
|||
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
|
||||
OutgoingViewingKey,
|
||||
},
|
||||
note::{ExtractedNoteCommitment, Nullifier, RandomSeed, TransmittedNoteCiphertext},
|
||||
note::{
|
||||
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
|
||||
TransmittedNoteCiphertext,
|
||||
},
|
||||
primitives::redpallas,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
use super::{get_note_version, orchard_parse_note_plaintext_without_memo};
|
||||
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_encoding_roundtrip(
|
||||
note in arb_note(NoteValue::from_raw(10)),
|
||||
) {
|
||||
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
|
||||
|
||||
// Encode.
|
||||
let plaintext = OrchardDomain::note_plaintext_bytes(¬e, ¬e.recipient(), memo);
|
||||
|
||||
// Decode.
|
||||
let domain = OrchardDomain { rho: note.rho() };
|
||||
let parsed_version = get_note_version(&plaintext);
|
||||
let parsed_memo = domain.extract_memo(&plaintext);
|
||||
|
||||
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &plaintext.0,
|
||||
|diversifier| {
|
||||
assert_eq!(diversifier, ¬e.recipient().diversifier());
|
||||
Some(*note.recipient().pk_d())
|
||||
}
|
||||
).expect("Plaintext parsing failed");
|
||||
|
||||
// Check.
|
||||
assert_eq!(parsed_note, note);
|
||||
assert_eq!(parsed_recipient, note.recipient());
|
||||
if parsed_note.note_type().is_native().into() {
|
||||
assert_eq!(parsed_version, 0x02);
|
||||
assert_eq!(&parsed_memo, memo);
|
||||
} else {
|
||||
assert_eq!(parsed_version, 0x03);
|
||||
let mut short_memo = *memo;
|
||||
short_memo[512 - 32..].copy_from_slice(&[0; 32]);
|
||||
assert_eq!(parsed_memo, short_memo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors() {
|
||||
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
|
||||
|
@ -400,7 +491,13 @@ mod tests {
|
|||
assert_eq!(ock.as_ref(), tv.ock);
|
||||
|
||||
let recipient = Address::from_parts(d, pk_d);
|
||||
let note = Note::from_parts(recipient, value, NoteType::native(), rho, rseed);
|
||||
|
||||
let note_type = match tv.note_type {
|
||||
None => NoteType::native(),
|
||||
Some(type_bytes) => NoteType::from_bytes(&type_bytes).unwrap(),
|
||||
};
|
||||
|
||||
let note = Note::from_parts(recipient, value, note_type, rho, rseed);
|
||||
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||
|
||||
let action = Action::from_parts(
|
||||
|
@ -439,7 +536,8 @@ mod tests {
|
|||
assert_eq!(decrypted_note, note);
|
||||
assert_eq!(decrypted_to, recipient);
|
||||
}
|
||||
None => panic!("Compact note decryption failed"),
|
||||
None => assert!(tv.note_type.is_some(), "Compact note decryption failed"),
|
||||
// Ignore that ZSA notes are not detected in compact decryption.
|
||||
}
|
||||
|
||||
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -302,7 +302,7 @@ impl ValueCommitment {
|
|||
pallas::Scalar::from(abs_value)
|
||||
};
|
||||
|
||||
let V_zsa = note_type.0;
|
||||
let V_zsa = note_type.cv_base();
|
||||
|
||||
ValueCommitment(V_zsa * value + R * rcv.0)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue