use alloc::vec::Vec;
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 crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
OutgoingViewingKey, PreparedEphemeralPublicKey, PreparedIncomingViewingKey, SharedSecret,
},
note::{ExtractedNoteCommitment, NoteVersion, Nullifier, RandomSeed, Rho},
value::{NoteValue, ValueCommitment},
Address, Note,
};
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
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 parse_note_plaintext_without_memo<F>(
rho: Rho,
plaintext: &[u8],
note_version: NoteVersion,
get_pk_d: F,
) -> Option<(Note, Address)>
where
F: FnOnce(&Diversifier) -> DiversifiedTransmissionKey,
{
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
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(),
&rho,
))?;
let pk_d = get_pk_d(&diversifier);
let recipient = Address::from_parts(diversifier, pk_d);
let note = Option::from(Note::from_parts(recipient, value, rho, rseed, note_version))?;
Some((note, recipient))
}
mod sealed {
pub trait Sealed {}
}
trait DomainPolicy {
fn note_version(&self, plaintext: &[u8]) -> Option<NoteVersion>;
}
pub trait DomainVersion: sealed::Sealed + Default {
const NOTE_VERSION: NoteVersion;
}
impl<V: DomainVersion> DomainPolicy for V {
fn note_version(&self, plaintext: &[u8]) -> Option<NoteVersion> {
if plaintext.first().copied() == Some(V::NOTE_VERSION.lead_byte()) {
Some(V::NOTE_VERSION)
} else {
None
}
}
}
#[derive(Default, Debug)]
pub struct OrchardVersion;
impl sealed::Sealed for OrchardVersion {}
impl DomainVersion for OrchardVersion {
const NOTE_VERSION: NoteVersion = NoteVersion::V2;
}
#[derive(Default, Debug)]
pub struct IronwoodVersion;
impl sealed::Sealed for IronwoodVersion {}
impl DomainVersion for IronwoodVersion {
const NOTE_VERSION: NoteVersion = NoteVersion::V3;
}
#[derive(Debug)]
pub(crate) struct BundleDomainPolicy {
note_version: NoteVersion,
}
impl DomainPolicy for BundleDomainPolicy {
fn note_version(&self, plaintext: &[u8]) -> Option<NoteVersion> {
let note_version = NoteVersion::from_lead_byte(*plaintext.first()?)?;
if note_version == self.note_version {
Some(note_version)
} else {
None
}
}
}
#[derive(Debug)]
pub struct NoteEncryptionDomain<P> {
rho: Rho,
policy: P,
}
impl<P> memuse::DynamicUsage for NoteEncryptionDomain<P> {
fn dynamic_usage(&self) -> usize {
self.rho.dynamic_usage()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
self.rho.dynamic_usage_bounds()
}
}
impl<V: DomainVersion> NoteEncryptionDomain<V> {
pub(crate) fn from_rho(rho: Rho) -> Self {
Self {
rho,
policy: V::default(),
}
}
pub fn for_action<T>(act: &Action<T>) -> Self {
Self::from_rho(act.rho())
}
pub fn for_pczt_action(act: &crate::pczt::Action) -> Self {
Self::from_rho(Rho::from_nf_old(act.spend().nullifier))
}
pub fn for_compact_action(act: &CompactAction) -> Self {
Self::from_rho(act.rho())
}
}
pub type OrchardDomain = NoteEncryptionDomain<OrchardVersion>;
pub type IronwoodDomain = NoteEncryptionDomain<IronwoodVersion>;
pub(crate) type BundleDomain = NoteEncryptionDomain<BundleDomainPolicy>;
impl BundleDomain {
pub(crate) fn for_action<T>(act: &Action<T>, note_version: NoteVersion) -> Self {
Self {
rho: act.rho(),
policy: BundleDomainPolicy { note_version },
}
}
}
impl<P: DomainPolicy> Domain for NoteEncryptionDomain<P> {
type EphemeralSecretKey = EphemeralSecretKey;
type EphemeralPublicKey = EphemeralPublicKey;
type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
type SharedSecret = SharedSecret;
type SymmetricKey = Hash;
type Note = Note;
type Recipient = Address;
type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
type IncomingViewingKey = PreparedIncomingViewingKey;
type OutgoingViewingKey = OutgoingViewingKey;
type ValueCommitment = ValueCommitment;
type ExtractedCommitment = ExtractedNoteCommitment;
type ExtractedCommitmentBytes = [u8; 32];
type Memo = [u8; 512];
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 prepare_epk(epk: Self::EphemeralPublicKey) -> Self::PreparedEphemeralPublicKey {
PreparedEphemeralPublicKey::new(epk)
}
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::PreparedEphemeralPublicKey,
) -> 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, memo: &Self::Memo) -> NotePlaintextBytes {
let mut np = [0; NOTE_PLAINTEXT_SIZE];
np[0] = note.version().lead_byte();
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);
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_repr());
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 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)> {
let note_version = self.policy.note_version(plaintext)?;
parse_note_plaintext_without_memo(self.rho, plaintext, note_version, |diversifier| {
DiversifiedTransmissionKey::derive(ivk, diversifier)
})
}
fn parse_note_plaintext_without_memo_ovk(
&self,
pk_d: &Self::DiversifiedTransmissionKey,
plaintext: &NotePlaintextBytes,
) -> Option<(Self::Note, Self::Recipient)> {
let note_version = self.policy.note_version(&plaintext.0)?;
parse_note_plaintext_without_memo(self.rho, &plaintext.0, note_version, |_| *pk_d)
}
fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]
.try_into()
.unwrap()
}
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
DiversifiedTransmissionKey::from_bytes(out_plaintext.0[0..32].try_into().unwrap()).into()
}
fn extract_esk(out_plaintext: &OutPlaintextBytes) -> Option<Self::EphemeralSecretKey> {
EphemeralSecretKey::from_bytes(out_plaintext.0[32..OUT_PLAINTEXT_SIZE].try_into().unwrap())
.into()
}
}
impl<P: DomainPolicy> BatchDomain for NoteEncryptionDomain<P> {
fn batch_kdf<'a>(
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
) -> Vec<Option<Self::SymmetricKey>> {
batch_kdf(items)
}
fn batch_epk(
ephemeral_keys: impl Iterator<Item = EphemeralKeyBytes>,
) -> Vec<(Option<Self::PreparedEphemeralPublicKey>, EphemeralKeyBytes)> {
let (epks, ephemeral_keys): (Vec<_>, Vec<_>) = ephemeral_keys
.map(|ephemeral_key| (Self::epk(&ephemeral_key), ephemeral_key))
.unzip();
PreparedEphemeralPublicKey::batch_tabled(epks)
.into_iter()
.zip(ephemeral_keys)
.collect()
}
fn batch_ka_agree_dec<'a>(
ivk: &Self::IncomingViewingKey,
epks: impl Iterator<Item = Option<&'a Self::PreparedEphemeralPublicKey>>,
) -> Vec<Option<Self::SharedSecret>>
where
Self::PreparedEphemeralPublicKey: 'a,
{
let decomposed =
pasta_curves::glv::Decomposed::<pasta_curves::pallas::Point>::new(&ivk.raw_scalar());
epks.map(|epk| epk.map(|epk| epk.agree_with(ivk, &decomposed)))
.collect()
}
}
fn batch_kdf<'a>(
items: impl Iterator<Item = (Option<SharedSecret>, &'a EphemeralKeyBytes)>,
) -> Vec<Option<Hash>> {
let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
SharedSecret::batch_to_affine(shared_secrets)
.zip(ephemeral_keys)
.map(|(secret, ephemeral_key)| {
secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
})
.collect()
}
impl<P: DomainPolicy, T> ShieldedOutput<NoteEncryptionDomain<P>, ENC_CIPHERTEXT_SIZE>
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; ENC_CIPHERTEXT_SIZE] {
&self.encrypted_note().enc_ciphertext
}
}
impl<P: DomainPolicy> ShieldedOutput<NoteEncryptionDomain<P>, ENC_CIPHERTEXT_SIZE>
for crate::pczt::Action
{
fn ephemeral_key(&self) -> EphemeralKeyBytes {
EphemeralKeyBytes(self.output().encrypted_note().epk_bytes)
}
fn cmstar_bytes(&self) -> [u8; 32] {
self.output().cmx().to_bytes()
}
fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
&self.output().encrypted_note().enc_ciphertext
}
}
impl<P: DomainPolicy> ShieldedOutput<NoteEncryptionDomain<P>, COMPACT_NOTE_SIZE> 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; COMPACT_NOTE_SIZE] {
&self.enc_ciphertext
}
}
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
pub type IronwoodNoteEncryption = zcash_note_encryption::NoteEncryption<IronwoodDomain>;
#[derive(Clone)]
pub struct CompactAction {
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: [u8; 52],
}
impl fmt::Debug for CompactAction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CompactAction")
}
}
impl<T> From<&Action<T>> for CompactAction {
fn from(action: &Action<T>) -> Self {
CompactAction {
nullifier: *action.nullifier(),
cmx: *action.cmx(),
ephemeral_key: EphemeralKeyBytes(action.encrypted_note().epk_bytes),
enc_ciphertext: action.encrypted_note().enc_ciphertext[..52]
.try_into()
.unwrap(),
}
}
}
impl CompactAction {
pub fn from_parts(
nullifier: Nullifier,
cmx: ExtractedNoteCommitment,
ephemeral_key: EphemeralKeyBytes,
enc_ciphertext: [u8; 52],
) -> Self {
Self {
nullifier,
cmx,
ephemeral_key,
enc_ciphertext,
}
}
pub fn nullifier(&self) -> Nullifier {
self.nullifier
}
pub fn cmx(&self) -> ExtractedNoteCommitment {
self.cmx
}
pub fn rho(&self) -> Rho {
Rho::from_nf_old(self.nullifier)
}
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use rand::RngCore;
use zcash_note_encryption::Domain;
use crate::{
keys::OutgoingViewingKey,
note::{ExtractedNoteCommitment, NoteVersion, Nullifier, RandomSeed, Rho},
value::NoteValue,
Address, Note,
};
use super::{CompactAction, OrchardDomain, OrchardNoteEncryption};
pub fn fake_compact_action<R: RngCore>(
rng: &mut R,
nf_old: Nullifier,
recipient: Address,
value: NoteValue,
ovk: Option<OutgoingViewingKey>,
) -> (CompactAction, Note) {
let rho = Rho::from_nf_old(nf_old);
let rseed = {
loop {
let mut bytes = [0; 32];
rng.fill_bytes(&mut bytes);
let rseed = RandomSeed::from_bytes(bytes, &rho);
if rseed.is_some().into() {
break rseed.unwrap();
}
}
};
let note = Note::from_parts(recipient, value, rho, rseed, NoteVersion::V2).unwrap();
let encryptor = OrchardNoteEncryption::new(ovk, note, [0u8; 512]);
let cmx = ExtractedNoteCommitment::from(note.commitment());
let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk());
let enc_ciphertext = encryptor.encrypt_note_plaintext();
(
CompactAction {
nullifier: nf_old,
cmx,
ephemeral_key,
enc_ciphertext: enc_ciphertext.as_ref()[..52].try_into().unwrap(),
},
note,
)
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use rand::rngs::OsRng;
use zcash_note_encryption::{
batch, try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk,
BatchDomain, Domain, EphemeralKeyBytes, NoteEncryption,
};
use super::{
prf_ock_orchard, CompactAction, DomainVersion, IronwoodDomain, IronwoodNoteEncryption,
IronwoodVersion, NoteEncryptionDomain, OrchardDomain, OrchardNoteEncryption,
OrchardVersion,
};
use crate::{
action::Action,
keys::{
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, FullViewingKey,
IncomingViewingKey, OutgoingViewingKey, PreparedIncomingViewingKey, Scope, SpendingKey,
},
note::{
ExtractedNoteCommitment, NoteVersion, Nullifier, RandomSeed, Rho,
TransmittedNoteCiphertext,
},
primitives::redpallas,
value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum},
Address, Note,
};
fn v3_encrypted_action() -> (
Action<()>,
PreparedIncomingViewingKey,
Note,
Address,
[u8; 512],
) {
let mut rng = OsRng;
let sk = SpendingKey::random(&mut rng);
let fvk = crate::keys::FullViewingKey::from(&sk);
let incoming_viewing_key = fvk.to_ivk(Scope::External);
let prepared_ivk = PreparedIncomingViewingKey::new(&incoming_viewing_key);
let recipient = fvk.address_at(0u32, Scope::External);
let nf_old = Nullifier::dummy(&mut rng);
let rho = Rho::from_nf_old(nf_old);
let note = Note::new(
recipient,
NoteValue::from_raw(5),
rho,
NoteVersion::V3,
&mut rng,
);
let memo = [7u8; 512];
let cv_net = ValueCommitment::derive(ValueSum::from_raw(5), ValueCommitTrapdoor::zero());
let cmx = ExtractedNoteCommitment::from(note.commitment());
let encryptor = IronwoodNoteEncryption::new(Some(fvk.to_ovk(Scope::External)), note, memo);
let encrypted_note = TransmittedNoteCiphertext {
epk_bytes: IronwoodDomain::epk_bytes(encryptor.epk()).0,
enc_ciphertext: encryptor.encrypt_note_plaintext(),
out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng),
};
let action = Action::from_parts(
nf_old,
redpallas::VerificationKey::dummy(),
cmx,
encrypted_note,
cv_net,
(),
)
.expect("a dummy verification key is unlikely to be the identity");
(action, prepared_ivk, note, recipient, memo)
}
#[test]
fn test_vectors() {
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
for tv in test_vectors {
let ivk = PreparedIncomingViewingKey::new(
&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();
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
let nf_old = Nullifier::from_bytes(&tv.nf_old).unwrap();
let rho = Rho::from_nf_old(nf_old);
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
let value = NoteValue::from_raw(tv.v);
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
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_version = NoteVersion::V2;
let note = Note::from_parts(recipient, value, rho, rseed, note_version).unwrap();
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
let action = Action::from_parts(
nf_old,
redpallas::VerificationKey::dummy(),
cmx,
TransmittedNoteCiphertext {
epk_bytes: ephemeral_key.0,
enc_ciphertext: tv.c_enc,
out_ciphertext: tv.c_out,
},
cv_net.clone(),
(),
)
.expect("a key returned by VerificationKey::dummy() is vanishingly unlikely to be the identity");
let domain = OrchardDomain::from_rho(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"),
}
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, 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[..]
);
}
}
#[test]
fn domains_accept_only_their_note_plaintext_versions() {
let mut rng = OsRng;
let sk = crate::keys::SpendingKey::random(&mut rng);
let fvk = crate::keys::FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, crate::keys::Scope::External);
let rho = Rho::from_nf_old(Nullifier::dummy(&mut rng));
let memo = [0u8; 512];
let note_v2 = Note::new(
recipient,
NoteValue::from_raw(5),
rho,
NoteVersion::V2,
&mut rng,
);
let note_v3 = Note::new(
recipient,
NoteValue::from_raw(5),
rho,
NoteVersion::V3,
&mut rng,
);
let orchard_domain = OrchardDomain::from_rho(rho);
let ironwood_domain = IronwoodDomain::from_rho(rho);
let np_v2 = OrchardDomain::note_plaintext_bytes(¬e_v2, &memo);
let np_v3 = IronwoodDomain::note_plaintext_bytes(¬e_v3, &memo);
let pk_d = recipient.pk_d();
assert_eq!(
orchard_domain
.parse_note_plaintext_without_memo_ovk(pk_d, &np_v2)
.map(|(note, _)| note),
Some(note_v2)
);
assert_eq!(
ironwood_domain
.parse_note_plaintext_without_memo_ovk(pk_d, &np_v3)
.map(|(note, _)| note),
Some(note_v3)
);
assert!(orchard_domain
.parse_note_plaintext_without_memo_ovk(pk_d, &np_v3)
.is_none());
assert!(ironwood_domain
.parse_note_plaintext_without_memo_ovk(pk_d, &np_v2)
.is_none());
}
#[test]
fn ironwood_domain_decrypts_v3_encrypted_outputs() {
let (action, ivk, note, recipient, memo) = v3_encrypted_action();
let domain = IronwoodDomain::for_action(&action);
assert_eq!(
try_note_decryption(&domain, &ivk, &action),
Some((note, recipient, memo))
);
}
#[test]
fn orchard_domain_rejects_v3_encrypted_outputs() {
let (action, ivk, _, _, _) = v3_encrypted_action();
let domain = OrchardDomain::for_action(&action);
assert!(try_note_decryption(&domain, &ivk, &action).is_none());
}
#[test]
fn ironwood_domain_decrypts_v3_compact_outputs() {
let (action, ivk, note, recipient, _) = v3_encrypted_action();
let domain = IronwoodDomain::for_action(&action);
let compact = CompactAction::from(&action);
assert_eq!(
try_compact_note_decryption(&domain, &ivk, &compact),
Some((note, recipient))
);
}
fn encrypted_compact_action<V: DomainVersion>(
rng: &mut OsRng,
recipient: Address,
) -> CompactAction {
let nf_old = Nullifier::dummy(rng);
let rho = Rho::from_nf_old(nf_old);
let note = Note::new(
recipient,
NoteValue::from_raw(42),
rho,
V::NOTE_VERSION,
rng,
);
let encryptor = NoteEncryption::<NoteEncryptionDomain<V>>::new(None, note, [0u8; 512]);
let ephemeral_key = NoteEncryptionDomain::<V>::epk_bytes(encryptor.epk());
let enc_ciphertext = encryptor.encrypt_note_plaintext();
CompactAction::from_parts(
nf_old,
ExtractedNoteCommitment::from(note.commitment()),
ephemeral_key,
enc_ciphertext.as_ref()[..52].try_into().unwrap(),
)
}
fn check_batched_compact_decryption_matches_per_item<V: DomainVersion>() {
let mut rng = OsRng;
let our_fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
let other_fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
let foreign_fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
let ivks: Vec<PreparedIncomingViewingKey> = [
(&our_fvk, Scope::External),
(&our_fvk, Scope::Internal),
(&other_fvk, Scope::External),
(&other_fvk, Scope::Internal),
]
.into_iter()
.map(|(fvk, scope)| PreparedIncomingViewingKey::new(&fvk.to_ivk(scope)))
.collect();
let mut actions = vec![
encrypted_compact_action::<V>(&mut rng, our_fvk.address_at(0u32, Scope::External)),
encrypted_compact_action::<V>(&mut rng, our_fvk.address_at(0u32, Scope::Internal)),
encrypted_compact_action::<V>(&mut rng, other_fvk.address_at(0u32, Scope::External)),
];
for i in 0..5u32 {
actions.push(encrypted_compact_action::<V>(
&mut rng,
foreign_fvk.address_at(i, Scope::External),
));
}
actions.push(CompactAction::from_parts(
Nullifier::dummy(&mut rng),
actions[0].cmx(),
EphemeralKeyBytes([0u8; 32]),
[0u8; 52],
));
let items: Vec<(NoteEncryptionDomain<V>, CompactAction)> = actions
.iter()
.map(|a| (NoteEncryptionDomain::<V>::for_compact_action(a), a.clone()))
.collect();
let batched = batch::try_compact_note_decryption(&ivks, &items);
let per_item: Vec<Option<((Note, Address), usize)>> = actions
.iter()
.map(|a| {
let domain = NoteEncryptionDomain::<V>::for_compact_action(a);
ivks.iter().enumerate().find_map(|(i, ivk)| {
try_compact_note_decryption(&domain, ivk, a).map(|r| (r, i))
})
})
.collect();
assert_eq!(batched, per_item);
assert_eq!(batched[0].as_ref().map(|(_, i)| *i), Some(0));
assert_eq!(batched[1].as_ref().map(|(_, i)| *i), Some(1));
assert_eq!(batched[2].as_ref().map(|(_, i)| *i), Some(2));
assert!(batched[3..].iter().all(Option::is_none));
}
#[test]
fn batched_compact_decryption_matches_per_item_orchard() {
check_batched_compact_decryption_matches_per_item::<OrchardVersion>();
}
#[test]
fn batched_compact_decryption_matches_per_item_ironwood() {
check_batched_compact_decryption_matches_per_item::<IronwoodVersion>();
}
#[test]
fn batched_agreement_matches_per_item() {
let mut rng = OsRng;
let our_fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
let foreign_fvk = FullViewingKey::from(&SpendingKey::random(&mut rng));
let ivks: Vec<PreparedIncomingViewingKey> = [
(&our_fvk, Scope::External),
(&our_fvk, Scope::Internal),
(&foreign_fvk, Scope::External),
]
.into_iter()
.map(|(fvk, scope)| PreparedIncomingViewingKey::new(&fvk.to_ivk(scope)))
.collect();
let mut keys: Vec<EphemeralKeyBytes> = (0..12u32)
.map(|i| {
encrypted_compact_action::<OrchardVersion>(
&mut rng,
our_fvk.address_at(i, Scope::External),
)
.ephemeral_key
})
.collect();
keys.push(EphemeralKeyBytes([0u8; 32]));
let batch_prepared = <OrchardDomain as BatchDomain>::batch_epk(keys.iter().cloned());
assert!(batch_prepared.last().unwrap().0.is_none());
assert!(batch_prepared[..12].iter().all(|(p, _)| p.is_some()));
let wnaf_prepared: Vec<Option<crate::keys::PreparedEphemeralPublicKey>> = keys
.iter()
.map(|key| OrchardDomain::epk(key).map(OrchardDomain::prepare_epk))
.collect();
for ivk in &ivks {
let expected: Vec<Option<[u8; 32]>> = keys
.iter()
.map(|key| {
OrchardDomain::epk(key)
.map(OrchardDomain::prepare_epk)
.map(|epk| OrchardDomain::ka_agree_dec(ivk, &epk).to_bytes())
})
.collect();
let batched: Vec<Option<[u8; 32]>> =
<OrchardDomain as BatchDomain>::batch_ka_agree_dec(
ivk,
batch_prepared.iter().map(|(p, _)| p.as_ref()),
)
.into_iter()
.map(|s| s.map(|s| s.to_bytes()))
.collect();
assert_eq!(batched, expected);
let batched_wnaf: Vec<Option<[u8; 32]>> =
<OrchardDomain as BatchDomain>::batch_ka_agree_dec(
ivk,
wnaf_prepared.iter().map(|p| p.as_ref()),
)
.into_iter()
.map(|s| s.map(|s| s.to_bytes()))
.collect();
assert_eq!(batched_wnaf, expected);
}
}
}