diff --git a/src/builder.rs b/src/builder.rs index c1a45b76..87d60d29 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -23,6 +23,7 @@ use crate::{ tree::{Anchor, MerklePath}, value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum}, }; +use crate::note::AssetType; const MIN_ACTIONS: usize = 2; @@ -150,8 +151,9 @@ impl ActionInfo { let ak: SpendValidatingKey = self.spend.fvk.clone().into(); let alpha = pallas::Scalar::random(&mut rng); let rk = ak.randomize(&alpha); + let asset_type = self.spend.note.asset_type(); - let note = Note::new(self.output.recipient, self.output.value, nf_old, &mut rng); + let note = Note::new(self.output.recipient, self.output.value, nf_old, &mut rng, asset_type); let cm_new = note.commitment(); let cmx = cm_new.into(); diff --git a/src/constants/fixed_bases.rs b/src/constants/fixed_bases.rs index af11e335..81a8534f 100644 --- a/src/constants/fixed_bases.rs +++ b/src/constants/fixed_bases.rs @@ -30,6 +30,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"; diff --git a/src/keys.rs b/src/keys.rs index 0504dfa4..b1a5d4d5 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -967,6 +967,7 @@ mod tests { value::NoteValue, Note, }; + use crate::note::AssetType; #[test] fn spend_validating_key_from_bytes() { @@ -1049,6 +1050,7 @@ mod tests { NoteValue::from_raw(tv.note_v), rho, RandomSeed::from_bytes(tv.note_rseed, &rho).unwrap(), + AssetType::ZEC, ); let cmx: ExtractedNoteCommitment = note.commitment().into(); diff --git a/src/note.rs b/src/note.rs index 6bd3f778..48dd65d7 100644 --- a/src/note.rs +++ b/src/note.rs @@ -79,6 +79,28 @@ impl RandomSeed { } } +/// The ID of ZEC or a ZSA asset. +#[derive(Debug, Copy, Clone)] +pub enum AssetType { + /// Represents the native asset of the protocol, a.k.a. ZEC. + ZEC, + /// Represents a user-defined asset. + // TODO: check the uniqueness of the encoding. + Asset(ZSAType), +} + +impl AssetType { + /// Parse the encoding of a ZSA asset type. + pub fn from_bytes(bytes: &[u8; 32]) -> CtOption { + pallas::Affine::from_bytes(bytes) + .map(|t| AssetType::Asset(ZSAType(t))) + } +} + +/// The ID of a ZSA asset. This type cannot represent native ZEC. +#[derive(Debug, Copy, Clone)] +pub struct ZSAType(pub(crate) pallas::Affine); + /// A discrete amount of funds received by an address. #[derive(Debug, Copy, Clone)] pub struct Note { @@ -95,6 +117,9 @@ pub struct Note { rho: Nullifier, /// The seed randomness for various note components. rseed: RandomSeed, + // TODO: merge with the value field to make it impossible to ignore? + // TODO: use a constant-time structure (like CtOption)? + asset_type: AssetType, } impl PartialEq for Note { @@ -113,12 +138,14 @@ impl Note { value: NoteValue, rho: Nullifier, rseed: RandomSeed, + asset_type: AssetType, ) -> Self { Note { recipient, value, rho, rseed, + asset_type, } } @@ -132,6 +159,7 @@ impl Note { value: NoteValue, rho: Nullifier, mut rng: impl RngCore, + asset_type: AssetType, ) -> Self { loop { let note = Note { @@ -139,6 +167,7 @@ impl Note { value, rho, rseed: RandomSeed::random(&mut rng, &rho), + asset_type, }; if note.commitment_inner().is_some().into() { break note; @@ -158,12 +187,14 @@ impl Note { let sk = SpendingKey::random(rng); let fvk: FullViewingKey = (&sk).into(); let recipient = fvk.address_at(0u32, Scope::External); + let asset_type = AssetType::ZEC; let note = Note::new( recipient, NoteValue::zero(), rho.unwrap_or_else(|| Nullifier::dummy(rng)), rng, + asset_type, ); (sk, fvk, note) @@ -179,6 +210,11 @@ impl Note { self.value } + /// Returns the asset type of this note. + pub fn asset_type(&self) -> AssetType { + self.asset_type + } + /// Returns the rseed value of this note. pub(crate) fn rseed(&self) -> &RandomSeed { &self.rseed @@ -223,6 +259,7 @@ impl Note { self.rho.0, self.rseed.psi(&self.rho), self.rseed.rcm(&self.rho), + self.asset_type, ) } @@ -269,7 +306,7 @@ pub mod testing { address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue, }; - use super::{Note, RandomSeed}; + use super::{AssetType, Note, RandomSeed}; prop_compose! { /// Generate an arbitrary random seed @@ -290,6 +327,7 @@ pub mod testing { value, rho, rseed, + asset_type: AssetType::ZEC, } } } diff --git a/src/note/commitment.rs b/src/note/commitment.rs index 9de51d28..ac3229bf 100644 --- a/src/note/commitment.rs +++ b/src/note/commitment.rs @@ -11,6 +11,9 @@ use crate::{ spec::extract_p, value::NoteValue, }; +use crate::note::AssetType; +use group::GroupEncoding; +use crate::constants::fixed_bases::NOTE_ZSA_COMMITMENT_PERSONALIZATION; #[derive(Clone, Debug)] pub(crate) struct NoteCommitTrapdoor(pub(super) pallas::Scalar); @@ -44,16 +47,57 @@ impl NoteCommitment { rho: pallas::Base, psi: pallas::Base, rcm: NoteCommitTrapdoor, + asset_type: AssetType, ) -> CtOption { - let domain = sinsemilla::CommitDomain::new(NOTE_COMMITMENT_PERSONALIZATION); + 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 match constant-time. + match asset_type { + // Commit to ZEC notes as per the Orchard protocol. + AssetType::ZEC => + Self::commit( + NOTE_COMMITMENT_PERSONALIZATION, + zec_note_bits, + rcm, + ), + + // Commit to non-ZEC notes as per the ZSA protocol. + AssetType::Asset(zsa_type) => { + // Append the asset type to the Orchard note encoding. + let encoded_type = BitArray::<_, Lsb0>::new(zsa_type.0.to_bytes()); + let zsa_note_bits = zec_note_bits + .chain(encoded_type.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, + rcm: NoteCommitTrapdoor, + ) -> CtOption { + let domain = sinsemilla::CommitDomain::new(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)), + bits, &rcm.0, ) .map(NoteCommitment) diff --git a/src/note_encryption.rs b/src/note_encryption.rs index 7aed1831..7b6539ca 100644 --- a/src/note_encryption.rs +++ b/src/note_encryption.rs @@ -4,11 +4,7 @@ use core::fmt; use blake2b_simd::{Hash, Params}; use group::ff::PrimeField; -use zcash_note_encryption::{ - BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes, - OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, - OUT_PLAINTEXT_SIZE, -}; +use zcash_note_encryption::{BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, COMPACT_ZSA_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_PLAINTEXT_SIZE, MEMO_SIZE}; use crate::{ action::Action, @@ -21,6 +17,9 @@ use crate::{ value::{NoteValue, ValueCommitment}, Address, Note, }; +use crate::note::AssetType; +use group::GroupEncoding; +use subtle::CtOption; const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock"; @@ -57,12 +56,11 @@ fn orchard_parse_note_plaintext_without_memo( where F: FnOnce(&Diversifier) -> Option, { - assert!(plaintext.len() >= COMPACT_NOTE_SIZE); + assert!(plaintext.len() >= COMPACT_NOTE_SIZE); // TODO: don’t panic, return None. // Check note plaintext version - if plaintext[0] != 0x02 { - return None; - } + // and parse the asset type accordingly. + let asset_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()); @@ -75,10 +73,23 @@ where 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); + + let note = Note::from_parts(recipient, value, domain.rho, rseed, asset_type); Some((note, recipient)) } +fn parse_version_and_asset_type(plaintext: &[u8]) -> Option { + // TODO: make this constant-time? + match plaintext[0] { + 0x02 => Some(AssetType::ZEC), + 0x03 if plaintext.len() >= COMPACT_ZSA_NOTE_SIZE => { + let bytes = &plaintext[COMPACT_NOTE_SIZE..COMPACT_ZSA_NOTE_SIZE].try_into().unwrap(); + AssetType::from_bytes(bytes).into() + } + _ => None, + } +} + /// Orchard-specific note encryption logic. #[derive(Debug)] pub struct OrchardDomain { @@ -107,7 +118,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 { Some(note.esk()) @@ -148,11 +159,24 @@ impl Domain for OrchardDomain { memo: &Self::Memo, ) -> NotePlaintextBytes { let mut np = [0; NOTE_PLAINTEXT_SIZE]; - np[0] = 0x02; + np[0] = match note.asset_type() { + AssetType::ZEC => 0x02, + AssetType::Asset(_) => 0x03, + }; np[1..12].copy_from_slice(note.recipient().diversifier().as_array()); np[12..20].copy_from_slice(¬e.value().to_bytes()); np[20..52].copy_from_slice(note.rseed().as_bytes()); - np[52..].copy_from_slice(memo); + match note.asset_type() { + AssetType::ZEC => { + np[52..].copy_from_slice(memo); + }, + AssetType::Asset(zsa_type) => { + np[52..84].copy_from_slice(&zsa_type.0.to_bytes()); + let short_memo = &memo[0..memo.len()-32]; + np[84..].copy_from_slice(short_memo); + // TODO: handle full-size memo or make short_memo explicit. + } + }; NotePlaintextBytes(np) } @@ -327,6 +351,7 @@ mod tests { value::{NoteValue, ValueCommitment}, Address, Note, }; + use crate::note::AssetType; #[test] fn test_vectors() { @@ -369,7 +394,8 @@ mod tests { assert_eq!(ock.as_ref(), tv.ock); let recipient = Address::from_parts(d, pk_d); - let note = Note::from_parts(recipient, value, rho, rseed); + let asset_type = AssetType::ZEC; // TODO: from data. + let note = Note::from_parts(recipient, value, rho, rseed, asset_type); assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx); let action = Action::from_parts(