mirror of https://github.com/zcash/orchard.git
zsa-note-encryption: introduce AssetType and encode and decode it in note plaintexts
This commit is contained in:
parent
0d7ecad8e9
commit
96442412af
|
@ -23,6 +23,7 @@ use crate::{
|
||||||
tree::{Anchor, MerklePath},
|
tree::{Anchor, MerklePath},
|
||||||
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
||||||
};
|
};
|
||||||
|
use crate::note::AssetType;
|
||||||
|
|
||||||
const MIN_ACTIONS: usize = 2;
|
const MIN_ACTIONS: usize = 2;
|
||||||
|
|
||||||
|
@ -150,8 +151,9 @@ impl ActionInfo {
|
||||||
let ak: SpendValidatingKey = self.spend.fvk.clone().into();
|
let ak: SpendValidatingKey = self.spend.fvk.clone().into();
|
||||||
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 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 cm_new = note.commitment();
|
||||||
let cmx = cm_new.into();
|
let cmx = cm_new.into();
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,9 @@ pub const VALUE_COMMITMENT_R_BYTES: [u8; 1] = *b"r";
|
||||||
/// SWU hash-to-curve personalization for the note commitment generator
|
/// SWU hash-to-curve personalization for the note commitment generator
|
||||||
pub const NOTE_COMMITMENT_PERSONALIZATION: &str = "z.cash:Orchard-NoteCommit";
|
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
|
/// SWU hash-to-curve personalization for the IVK commitment generator
|
||||||
pub const COMMIT_IVK_PERSONALIZATION: &str = "z.cash:Orchard-CommitIvk";
|
pub const COMMIT_IVK_PERSONALIZATION: &str = "z.cash:Orchard-CommitIvk";
|
||||||
|
|
||||||
|
|
|
@ -967,6 +967,7 @@ mod tests {
|
||||||
value::NoteValue,
|
value::NoteValue,
|
||||||
Note,
|
Note,
|
||||||
};
|
};
|
||||||
|
use crate::note::AssetType;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn spend_validating_key_from_bytes() {
|
fn spend_validating_key_from_bytes() {
|
||||||
|
@ -1049,6 +1050,7 @@ mod tests {
|
||||||
NoteValue::from_raw(tv.note_v),
|
NoteValue::from_raw(tv.note_v),
|
||||||
rho,
|
rho,
|
||||||
RandomSeed::from_bytes(tv.note_rseed, &rho).unwrap(),
|
RandomSeed::from_bytes(tv.note_rseed, &rho).unwrap(),
|
||||||
|
AssetType::ZEC,
|
||||||
);
|
);
|
||||||
|
|
||||||
let cmx: ExtractedNoteCommitment = note.commitment().into();
|
let cmx: ExtractedNoteCommitment = note.commitment().into();
|
||||||
|
|
40
src/note.rs
40
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<Self> {
|
||||||
|
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.
|
/// A discrete amount of funds received by an address.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct Note {
|
pub struct Note {
|
||||||
|
@ -95,6 +117,9 @@ pub struct Note {
|
||||||
rho: Nullifier,
|
rho: Nullifier,
|
||||||
/// The seed randomness for various note components.
|
/// The seed randomness for various note components.
|
||||||
rseed: RandomSeed,
|
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 {
|
impl PartialEq for Note {
|
||||||
|
@ -113,12 +138,14 @@ impl Note {
|
||||||
value: NoteValue,
|
value: NoteValue,
|
||||||
rho: Nullifier,
|
rho: Nullifier,
|
||||||
rseed: RandomSeed,
|
rseed: RandomSeed,
|
||||||
|
asset_type: AssetType,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Note {
|
Note {
|
||||||
recipient,
|
recipient,
|
||||||
value,
|
value,
|
||||||
rho,
|
rho,
|
||||||
rseed,
|
rseed,
|
||||||
|
asset_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +159,7 @@ impl Note {
|
||||||
value: NoteValue,
|
value: NoteValue,
|
||||||
rho: Nullifier,
|
rho: Nullifier,
|
||||||
mut rng: impl RngCore,
|
mut rng: impl RngCore,
|
||||||
|
asset_type: AssetType,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
loop {
|
loop {
|
||||||
let note = Note {
|
let note = Note {
|
||||||
|
@ -139,6 +167,7 @@ impl Note {
|
||||||
value,
|
value,
|
||||||
rho,
|
rho,
|
||||||
rseed: RandomSeed::random(&mut rng, &rho),
|
rseed: RandomSeed::random(&mut rng, &rho),
|
||||||
|
asset_type,
|
||||||
};
|
};
|
||||||
if note.commitment_inner().is_some().into() {
|
if note.commitment_inner().is_some().into() {
|
||||||
break note;
|
break note;
|
||||||
|
@ -158,12 +187,14 @@ impl Note {
|
||||||
let sk = SpendingKey::random(rng);
|
let sk = SpendingKey::random(rng);
|
||||||
let fvk: FullViewingKey = (&sk).into();
|
let fvk: FullViewingKey = (&sk).into();
|
||||||
let recipient = fvk.address_at(0u32, Scope::External);
|
let recipient = fvk.address_at(0u32, Scope::External);
|
||||||
|
let asset_type = AssetType::ZEC;
|
||||||
|
|
||||||
let note = Note::new(
|
let note = Note::new(
|
||||||
recipient,
|
recipient,
|
||||||
NoteValue::zero(),
|
NoteValue::zero(),
|
||||||
rho.unwrap_or_else(|| Nullifier::dummy(rng)),
|
rho.unwrap_or_else(|| Nullifier::dummy(rng)),
|
||||||
rng,
|
rng,
|
||||||
|
asset_type,
|
||||||
);
|
);
|
||||||
|
|
||||||
(sk, fvk, note)
|
(sk, fvk, note)
|
||||||
|
@ -179,6 +210,11 @@ impl Note {
|
||||||
self.value
|
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.
|
/// Returns the rseed value of this note.
|
||||||
pub(crate) fn rseed(&self) -> &RandomSeed {
|
pub(crate) fn rseed(&self) -> &RandomSeed {
|
||||||
&self.rseed
|
&self.rseed
|
||||||
|
@ -223,6 +259,7 @@ impl Note {
|
||||||
self.rho.0,
|
self.rho.0,
|
||||||
self.rseed.psi(&self.rho),
|
self.rseed.psi(&self.rho),
|
||||||
self.rseed.rcm(&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,
|
address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Note, RandomSeed};
|
use super::{AssetType, Note, RandomSeed};
|
||||||
|
|
||||||
prop_compose! {
|
prop_compose! {
|
||||||
/// Generate an arbitrary random seed
|
/// Generate an arbitrary random seed
|
||||||
|
@ -290,6 +327,7 @@ pub mod testing {
|
||||||
value,
|
value,
|
||||||
rho,
|
rho,
|
||||||
rseed,
|
rseed,
|
||||||
|
asset_type: AssetType::ZEC,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ use crate::{
|
||||||
spec::extract_p,
|
spec::extract_p,
|
||||||
value::NoteValue,
|
value::NoteValue,
|
||||||
};
|
};
|
||||||
|
use crate::note::AssetType;
|
||||||
|
use group::GroupEncoding;
|
||||||
|
use crate::constants::fixed_bases::NOTE_ZSA_COMMITMENT_PERSONALIZATION;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct NoteCommitTrapdoor(pub(super) pallas::Scalar);
|
pub(crate) struct NoteCommitTrapdoor(pub(super) pallas::Scalar);
|
||||||
|
@ -44,16 +47,57 @@ impl NoteCommitment {
|
||||||
rho: pallas::Base,
|
rho: pallas::Base,
|
||||||
psi: pallas::Base,
|
psi: pallas::Base,
|
||||||
rcm: NoteCommitTrapdoor,
|
rcm: NoteCommitTrapdoor,
|
||||||
|
asset_type: AssetType,
|
||||||
) -> CtOption<Self> {
|
) -> CtOption<Self> {
|
||||||
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<Item = bool>,
|
||||||
|
rcm: NoteCommitTrapdoor,
|
||||||
|
) -> CtOption<Self> {
|
||||||
|
let domain = sinsemilla::CommitDomain::new(personalization);
|
||||||
domain
|
domain
|
||||||
.commit(
|
.commit(
|
||||||
iter::empty()
|
bits,
|
||||||
.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,
|
&rcm.0,
|
||||||
)
|
)
|
||||||
.map(NoteCommitment)
|
.map(NoteCommitment)
|
||||||
|
|
|
@ -4,11 +4,7 @@ use core::fmt;
|
||||||
|
|
||||||
use blake2b_simd::{Hash, Params};
|
use blake2b_simd::{Hash, Params};
|
||||||
use group::ff::PrimeField;
|
use group::ff::PrimeField;
|
||||||
use zcash_note_encryption::{
|
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};
|
||||||
BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
|
|
||||||
OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE,
|
|
||||||
OUT_PLAINTEXT_SIZE,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
action::Action,
|
action::Action,
|
||||||
|
@ -21,6 +17,9 @@ use crate::{
|
||||||
value::{NoteValue, ValueCommitment},
|
value::{NoteValue, ValueCommitment},
|
||||||
Address, Note,
|
Address, Note,
|
||||||
};
|
};
|
||||||
|
use crate::note::AssetType;
|
||||||
|
use group::GroupEncoding;
|
||||||
|
use subtle::CtOption;
|
||||||
|
|
||||||
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
||||||
|
|
||||||
|
@ -57,12 +56,11 @@ fn orchard_parse_note_plaintext_without_memo<F>(
|
||||||
where
|
where
|
||||||
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
||||||
{
|
{
|
||||||
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
|
assert!(plaintext.len() >= COMPACT_NOTE_SIZE); // TODO: don’t panic, return None.
|
||||||
|
|
||||||
// Check note plaintext version
|
// Check note plaintext version
|
||||||
if plaintext[0] != 0x02 {
|
// and parse the asset type accordingly.
|
||||||
return None;
|
let asset_type = parse_version_and_asset_type(plaintext)?;
|
||||||
}
|
|
||||||
|
|
||||||
// The unwraps below are guaranteed to succeed by the assertion above
|
// The unwraps below are guaranteed to succeed by the assertion above
|
||||||
let diversifier = Diversifier::from_bytes(plaintext[1..12].try_into().unwrap());
|
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 pk_d = get_validated_pk_d(&diversifier)?;
|
||||||
|
|
||||||
let recipient = Address::from_parts(diversifier, pk_d);
|
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))
|
Some((note, recipient))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_version_and_asset_type(plaintext: &[u8]) -> Option<AssetType> {
|
||||||
|
// 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.
|
/// Orchard-specific note encryption logic.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct OrchardDomain {
|
pub struct OrchardDomain {
|
||||||
|
@ -107,7 +118,7 @@ impl Domain for OrchardDomain {
|
||||||
type ValueCommitment = ValueCommitment;
|
type ValueCommitment = ValueCommitment;
|
||||||
type ExtractedCommitment = ExtractedNoteCommitment;
|
type ExtractedCommitment = ExtractedNoteCommitment;
|
||||||
type ExtractedCommitmentBytes = [u8; 32];
|
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> {
|
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
|
||||||
Some(note.esk())
|
Some(note.esk())
|
||||||
|
@ -148,11 +159,24 @@ impl Domain for OrchardDomain {
|
||||||
memo: &Self::Memo,
|
memo: &Self::Memo,
|
||||||
) -> NotePlaintextBytes {
|
) -> NotePlaintextBytes {
|
||||||
let mut np = [0; NOTE_PLAINTEXT_SIZE];
|
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[1..12].copy_from_slice(note.recipient().diversifier().as_array());
|
||||||
np[12..20].copy_from_slice(¬e.value().to_bytes());
|
np[12..20].copy_from_slice(¬e.value().to_bytes());
|
||||||
np[20..52].copy_from_slice(note.rseed().as_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)
|
NotePlaintextBytes(np)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,6 +351,7 @@ mod tests {
|
||||||
value::{NoteValue, ValueCommitment},
|
value::{NoteValue, ValueCommitment},
|
||||||
Address, Note,
|
Address, Note,
|
||||||
};
|
};
|
||||||
|
use crate::note::AssetType;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_vectors() {
|
fn test_vectors() {
|
||||||
|
@ -369,7 +394,8 @@ mod tests {
|
||||||
assert_eq!(ock.as_ref(), tv.ock);
|
assert_eq!(ock.as_ref(), tv.ock);
|
||||||
|
|
||||||
let recipient = Address::from_parts(d, pk_d);
|
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);
|
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||||
|
|
||||||
let action = Action::from_parts(
|
let action = Action::from_parts(
|
||||||
|
|
Loading…
Reference in New Issue