mirror of https://github.com/zcash/orchard.git
V3 encryption (#38)
Added `OrchardDomainV3` on top of the encryption generalization (QED-it/librustzcash#18). not for review: note_encryption.rs, note_encryptionv2v3.rs and src/test_vectors/note_encryption.rs. These files represent two possible approaches for backward compatibility and will be finalized down the road. (the files were excluded from the build).
This commit is contained in:
parent
4681cc212a
commit
cec48d72a1
|
@ -84,3 +84,6 @@ debug = true
|
|||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
zcash_note_encryption = { git = "https://github.com/QED-it/librustzcash.git", rev = "07c377ddedf71ab7c7a266d284b054a2dafc2ed4" }
|
||||
|
|
|
@ -5,7 +5,7 @@ use orchard::{
|
|||
circuit::ProvingKey,
|
||||
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendingKey},
|
||||
note::AssetId,
|
||||
note_encryption::{CompactAction, OrchardDomain},
|
||||
note_encryption_v3::{CompactAction, OrchardDomainV3},
|
||||
value::NoteValue,
|
||||
Anchor, Bundle,
|
||||
};
|
||||
|
@ -79,7 +79,7 @@ fn bench_note_decryption(c: &mut Criterion) {
|
|||
};
|
||||
let action = bundle.actions().first();
|
||||
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
|
||||
let compact = {
|
||||
let mut group = c.benchmark_group("note-decryption");
|
||||
|
@ -120,12 +120,12 @@ fn bench_note_decryption(c: &mut Criterion) {
|
|||
let ivks = 2;
|
||||
let valid_ivks = vec![valid_ivk; ivks];
|
||||
let actions: Vec<_> = (0..100)
|
||||
.map(|_| (OrchardDomain::for_action(action), action.clone()))
|
||||
.map(|_| (OrchardDomainV3::for_action(action), action.clone()))
|
||||
.collect();
|
||||
let compact: Vec<_> = (0..100)
|
||||
.map(|_| {
|
||||
(
|
||||
OrchardDomain::for_action(action),
|
||||
OrchardDomainV3::for_action(action),
|
||||
CompactAction::from(action),
|
||||
)
|
||||
})
|
||||
|
|
|
@ -158,7 +158,7 @@ pub(crate) mod testing {
|
|||
// FIXME: make a real one from the note.
|
||||
let encrypted_note = TransmittedNoteCiphertext {
|
||||
epk_bytes: [0u8; 32],
|
||||
enc_ciphertext: [0u8; 580],
|
||||
enc_ciphertext: [0u8; 612],
|
||||
out_ciphertext: [0u8; 80]
|
||||
};
|
||||
Action {
|
||||
|
@ -192,7 +192,7 @@ pub(crate) mod testing {
|
|||
// FIXME: make a real one from the note.
|
||||
let encrypted_note = TransmittedNoteCiphertext {
|
||||
epk_bytes: [0u8; 32],
|
||||
enc_ciphertext: [0u8; 580],
|
||||
enc_ciphertext: [0u8; 612],
|
||||
out_ciphertext: [0u8; 80]
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
SpendingKey,
|
||||
},
|
||||
note::{Note, TransmittedNoteCiphertext},
|
||||
note_encryption::OrchardNoteEncryption,
|
||||
note_encryption_v3::OrchardNoteEncryption,
|
||||
primitives::redpallas::{self, Binding, SpendAuth},
|
||||
tree::{Anchor, MerklePath},
|
||||
value::{self, NoteValue, OverflowError, ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
||||
|
@ -79,7 +79,12 @@ impl SpendInfo {
|
|||
/// Returns `None` if the `fvk` does not own the `note`.
|
||||
///
|
||||
/// [`Builder::add_spend`]: Builder::add_spend
|
||||
pub fn new(fvk: FullViewingKey, note: Note, merkle_path: MerklePath) -> Option<Self> {
|
||||
pub fn new(
|
||||
fvk: FullViewingKey,
|
||||
note: Note,
|
||||
merkle_path: MerklePath,
|
||||
split_flag: bool,
|
||||
) -> Option<Self> {
|
||||
let scope = fvk.scope_for_address(¬e.recipient())?;
|
||||
Some(SpendInfo {
|
||||
dummy_sk: None,
|
||||
|
@ -87,7 +92,7 @@ impl SpendInfo {
|
|||
scope,
|
||||
note,
|
||||
merkle_path,
|
||||
split_flag: false,
|
||||
split_flag,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -112,10 +117,7 @@ impl SpendInfo {
|
|||
|
||||
/// Return a copy of this note with the split flag set to `true`.
|
||||
fn create_split_spend(&self) -> Self {
|
||||
let mut split_spend = SpendInfo::new(self.fvk.clone(), self.note, self.merkle_path.clone())
|
||||
.expect("The spend info is valid");
|
||||
split_spend.split_flag = true;
|
||||
split_spend
|
||||
SpendInfo::new(self.fvk.clone(), self.note, self.merkle_path.clone(), true).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -224,7 +226,7 @@ impl ActionInfo {
|
|||
|
||||
let encrypted_note = TransmittedNoteCiphertext {
|
||||
epk_bytes: encryptor.epk().to_bytes().0,
|
||||
enc_ciphertext: encryptor.encrypt_note_plaintext(),
|
||||
enc_ciphertext: encryptor.encrypt_note_plaintext().0,
|
||||
out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng),
|
||||
};
|
||||
|
||||
|
@ -278,7 +280,7 @@ impl Builder {
|
|||
/// Returns an error if the given Merkle path does not have the required anchor for
|
||||
/// the given note.
|
||||
///
|
||||
/// [`OrchardDomain`]: crate::note_encryption::OrchardDomain
|
||||
/// [`OrchardDomain`]: crate::note_encryption_v3::OrchardDomainV3
|
||||
/// [`MerkleHashOrchard`]: crate::tree::MerkleHashOrchard
|
||||
pub fn add_spend(
|
||||
&mut self,
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
circuit::{Instance, Proof, VerifyingKey},
|
||||
keys::{IncomingViewingKey, OutgoingViewingKey, PreparedIncomingViewingKey},
|
||||
note::Note,
|
||||
note_encryption::OrchardDomain,
|
||||
note_encryption_v3::OrchardDomainV3,
|
||||
primitives::redpallas::{self, Binding, SpendAuth},
|
||||
tree::Anchor,
|
||||
value::{ValueCommitTrapdoor, ValueCommitment, ValueSum},
|
||||
|
@ -305,7 +305,7 @@ impl<T: Authorization, V> Bundle<T, V> {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, action)| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
prepared_keys.iter().find_map(|(ivk, prepared_ivk)| {
|
||||
try_note_decryption(&domain, prepared_ivk, action)
|
||||
.map(|(n, a, m)| (idx, (*ivk).clone(), n, a, m))
|
||||
|
@ -324,7 +324,7 @@ impl<T: Authorization, V> Bundle<T, V> {
|
|||
) -> Option<(Note, Address, [u8; 512])> {
|
||||
let prepared_ivk = PreparedIncomingViewingKey::new(key);
|
||||
self.actions.get(action_idx).and_then(move |action| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
try_note_decryption(&domain, &prepared_ivk, action)
|
||||
})
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ impl<T: Authorization, V> Bundle<T, V> {
|
|||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, action)| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
keys.iter().find_map(move |key| {
|
||||
try_output_recovery_with_ovk(
|
||||
&domain,
|
||||
|
@ -365,7 +365,7 @@ impl<T: Authorization, V> Bundle<T, V> {
|
|||
key: &OutgoingViewingKey,
|
||||
) -> Option<(Note, Address, [u8; 512])> {
|
||||
self.actions.get(action_idx).and_then(move |action| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
try_output_recovery_with_ovk(
|
||||
&domain,
|
||||
key,
|
||||
|
@ -527,7 +527,7 @@ pub mod testing {
|
|||
use super::{Action, Authorization, Authorized, Bundle, Flags};
|
||||
|
||||
pub use crate::action::testing::{arb_action, arb_unauthorized_action};
|
||||
use crate::note::asset_id::testing::zsa_asset_id;
|
||||
use crate::note::asset_id::testing::arb_zsa_asset_id;
|
||||
use crate::note::AssetId;
|
||||
use crate::value::testing::arb_value_sum;
|
||||
|
||||
|
@ -591,7 +591,11 @@ pub mod testing {
|
|||
|
||||
prop_compose! {
|
||||
/// Create an arbitrary vector of assets to burn.
|
||||
pub fn arb_asset_to_burn()(asset_id in zsa_asset_id(), value in arb_value_sum()) -> (AssetId, ValueSum) {
|
||||
pub fn arb_asset_to_burn()
|
||||
(
|
||||
asset_id in arb_zsa_asset_id(),
|
||||
value in arb_value_sum()
|
||||
) -> (AssetId, ValueSum) {
|
||||
(asset_id, value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,13 +42,13 @@ pub(crate) fn hash_bundle_txid_data<A: Authorization, V: Copy + Into<i64>>(
|
|||
ch.update(&action.nullifier().to_bytes());
|
||||
ch.update(&action.cmx().to_bytes());
|
||||
ch.update(&action.encrypted_note().epk_bytes);
|
||||
ch.update(&action.encrypted_note().enc_ciphertext[..52]);
|
||||
ch.update(&action.encrypted_note().enc_ciphertext[..84]); // TODO: make sure it is backward compatible with Orchard [..52]
|
||||
|
||||
mh.update(&action.encrypted_note().enc_ciphertext[52..564]);
|
||||
mh.update(&action.encrypted_note().enc_ciphertext[84..596]);
|
||||
|
||||
nh.update(&action.cv_net().to_bytes());
|
||||
nh.update(&<[u8; 32]>::from(action.rk()));
|
||||
nh.update(&action.encrypted_note().enc_ciphertext[564..]);
|
||||
nh.update(&action.encrypted_note().enc_ciphertext[596..]);
|
||||
nh.update(&action.encrypted_note().out_ciphertext);
|
||||
}
|
||||
|
||||
|
|
|
@ -344,7 +344,7 @@ impl IssueBundle<Signed> {
|
|||
/// * Asset description size is collect.
|
||||
/// * `AssetId` for the `IssueAction` has not been previously finalized.
|
||||
/// * For each `Note` inside an `IssueAction`:
|
||||
/// * All notes have the same, correct `NoteType`.
|
||||
/// * All notes have the same, correct `AssetId`.
|
||||
pub fn verify_issue_bundle(
|
||||
bundle: &IssueBundle<Signed>,
|
||||
sighash: [u8; 32],
|
||||
|
@ -356,7 +356,6 @@ pub fn verify_issue_bundle(
|
|||
|
||||
let s = &mut HashSet::<AssetId>::new();
|
||||
|
||||
// An IssueAction could have just one properly derived AssetId.
|
||||
let newly_finalized = bundle
|
||||
.actions()
|
||||
.iter()
|
||||
|
@ -373,7 +372,7 @@ pub fn verify_issue_bundle(
|
|||
return Err(IssueActionPreviouslyFinalizedNoteType(asset));
|
||||
}
|
||||
|
||||
// Add to finalization set, if needed.
|
||||
// Add to the finalization set, if needed.
|
||||
if action.is_finalized() {
|
||||
newly_finalized.insert(asset);
|
||||
}
|
||||
|
@ -1025,30 +1024,31 @@ mod tests {
|
|||
pub mod testing {
|
||||
use crate::issuance::{IssueAction, IssueBundle, Prepared, Signed, Unauthorized};
|
||||
use crate::keys::testing::{arb_issuance_authorizing_key, arb_issuance_validating_key};
|
||||
use crate::note::asset_id::testing::zsa_asset_id;
|
||||
use crate::note::testing::arb_zsa_note;
|
||||
use proptest::collection::vec;
|
||||
use proptest::prelude::*;
|
||||
use proptest::prop_compose;
|
||||
use proptest::string::string_regex;
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
|
||||
prop_compose! {
|
||||
/// Generate an issue action given note value
|
||||
pub fn arb_issue_action()(
|
||||
note in arb_zsa_note(),
|
||||
asset_descr in string_regex(".{1,512}").unwrap()
|
||||
) -> IssueAction {
|
||||
IssueAction::new(asset_descr, ¬e)
|
||||
/// Generate an issue action
|
||||
pub fn arb_issue_action(asset_desc: String)
|
||||
(
|
||||
asset in zsa_asset_id(asset_desc.clone()),
|
||||
)
|
||||
(
|
||||
note in arb_zsa_note(asset),
|
||||
)-> IssueAction {
|
||||
IssueAction::new(asset_desc.clone(), ¬e)
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
|
||||
/// necessarily respect consensus rules; for that use
|
||||
/// [`crate::builder::testing::arb_issue_bundle`]
|
||||
/// Generate an arbitrary issue bundle with fake authorization data.
|
||||
pub fn arb_unathorized_issue_bundle(n_actions: usize)
|
||||
(
|
||||
actions in vec(arb_issue_action(), n_actions),
|
||||
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
|
||||
ik in arb_issuance_validating_key()
|
||||
) -> IssueBundle<Unauthorized> {
|
||||
IssueBundle {
|
||||
|
@ -1061,11 +1061,10 @@ pub mod testing {
|
|||
|
||||
prop_compose! {
|
||||
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
|
||||
/// necessarily respect consensus rules; for that use
|
||||
/// [`crate::builder::testing::arb_issue_bundle`]
|
||||
/// necessarily respect consensus rules
|
||||
pub fn arb_prepared_issue_bundle(n_actions: usize)
|
||||
(
|
||||
actions in vec(arb_issue_action(), n_actions),
|
||||
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
|
||||
ik in arb_issuance_validating_key(),
|
||||
fake_sighash in prop::array::uniform32(prop::num::u8::ANY)
|
||||
) -> IssueBundle<Prepared> {
|
||||
|
@ -1079,11 +1078,10 @@ pub mod testing {
|
|||
|
||||
prop_compose! {
|
||||
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
|
||||
/// necessarily respect consensus rules; for that use
|
||||
/// [`crate::builder::testing::arb_issue_bundle`]
|
||||
/// necessarily respect consensus rules
|
||||
pub fn arb_signed_issue_bundle(n_actions: usize)
|
||||
(
|
||||
actions in vec(arb_issue_action(), n_actions),
|
||||
actions in vec(arb_issue_action("asset_desc".to_string()), n_actions),
|
||||
ik in arb_issuance_validating_key(),
|
||||
isk in arb_issuance_authorizing_key(),
|
||||
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
|
||||
|
|
|
@ -25,7 +25,8 @@ mod constants;
|
|||
pub mod issuance;
|
||||
pub mod keys;
|
||||
pub mod note;
|
||||
pub mod note_encryption;
|
||||
// pub mod note_encryption; // disabled until backward compatability is implemented.
|
||||
pub mod note_encryption_v3;
|
||||
pub mod primitives;
|
||||
mod spec;
|
||||
pub mod tree;
|
||||
|
|
27
src/note.rs
27
src/note.rs
|
@ -279,7 +279,7 @@ pub struct TransmittedNoteCiphertext {
|
|||
/// The serialization of the ephemeral public key
|
||||
pub epk_bytes: [u8; 32],
|
||||
/// The encrypted note ciphertext
|
||||
pub enc_ciphertext: [u8; 580],
|
||||
pub enc_ciphertext: [u8; 612],
|
||||
/// An encrypted value that allows the holder of the outgoing cipher
|
||||
/// key for the note to recover the note plaintext.
|
||||
pub out_ciphertext: [u8; 80],
|
||||
|
@ -302,7 +302,7 @@ pub mod testing {
|
|||
use proptest::prelude::*;
|
||||
|
||||
use crate::note::asset_id::testing::arb_asset_id;
|
||||
use crate::note::asset_id::testing::zsa_asset_id;
|
||||
use crate::note::AssetId;
|
||||
use crate::value::testing::arb_note_value;
|
||||
use crate::{
|
||||
address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue,
|
||||
|
@ -336,13 +336,30 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
/// Generate an arbitrary ZSA note
|
||||
pub fn arb_zsa_note()(
|
||||
/// Generate an arbitrary native note
|
||||
pub fn arb_native_note()(
|
||||
recipient in arb_address(),
|
||||
value in arb_note_value(),
|
||||
rho in arb_nullifier(),
|
||||
rseed in arb_rseed(),
|
||||
) -> Note {
|
||||
Note {
|
||||
recipient,
|
||||
value,
|
||||
asset: AssetId::native(),
|
||||
rho,
|
||||
rseed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
/// Generate an arbitrary zsa note
|
||||
pub fn arb_zsa_note(asset: AssetId)(
|
||||
recipient in arb_address(),
|
||||
value in arb_note_value(),
|
||||
rho in arb_nullifier(),
|
||||
rseed in arb_rseed(),
|
||||
asset in zsa_asset_id(),
|
||||
) -> Note {
|
||||
Note {
|
||||
recipient,
|
||||
|
|
|
@ -120,8 +120,8 @@ pub mod testing {
|
|||
}
|
||||
|
||||
prop_compose! {
|
||||
/// Generate the ZSA note type
|
||||
pub fn zsa_asset_id()(
|
||||
/// Generate an asset ID
|
||||
pub fn arb_zsa_asset_id()(
|
||||
sk in arb_spending_key(),
|
||||
str in "[A-Za-z]{255}"
|
||||
) -> AssetId {
|
||||
|
@ -130,6 +130,17 @@ pub mod testing {
|
|||
}
|
||||
}
|
||||
|
||||
prop_compose! {
|
||||
/// Generate an asset ID using a specific description
|
||||
pub fn zsa_asset_id(asset_desc: String)(
|
||||
sk in arb_spending_key(),
|
||||
) -> AssetId {
|
||||
assert!(super::is_asset_desc_of_valid_size(&asset_desc));
|
||||
let isk = IssuanceAuthorizingKey::from(&sk);
|
||||
AssetId::derive(&IssuanceValidatingKey::from(&isk), &asset_desc)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors() {
|
||||
let test_vectors = crate::test_vectors::asset_id::test_vectors();
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
//! In-band secret distribution for Orchard bundles.
|
||||
|
||||
use blake2b_simd::{Hash, Params};
|
||||
use core::fmt;
|
||||
use group::ff::PrimeField;
|
||||
use std::fmt;
|
||||
use zcash_note_encryption::{
|
||||
BatchDomain, Domain, EphemeralKeyBytes, NotePlaintextBytes, OutPlaintextBytes,
|
||||
OutgoingCipherKey, ShieldedOutput, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE,
|
||||
OUT_PLAINTEXT_SIZE,
|
||||
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
|
||||
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
|
||||
};
|
||||
|
||||
use crate::note::AssetId;
|
||||
|
@ -24,14 +23,82 @@ 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;
|
||||
/// The size of a v2 compact note.
|
||||
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
|
||||
11 + // diversifier
|
||||
8 + // value
|
||||
32; // rseed (or rcm prior to ZIP 212)
|
||||
/// The size of [`NotePlaintextBytes`] for V2.
|
||||
pub const NOTE_PLAINTEXT_SIZE_V2: usize = COMPACT_NOTE_SIZE_V2 + MEMO_SIZE;
|
||||
/// The size of an encrypted note plaintext.
|
||||
pub const ENC_CIPHERTEXT_SIZE_V2: usize = NOTE_PLAINTEXT_SIZE_V2 + AEAD_TAG_SIZE;
|
||||
|
||||
/// a type to represent the raw bytes of a note plaintext.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE_V2]);
|
||||
|
||||
/// a type to represent the raw bytes of an encrypted note plaintext.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NoteCiphertextBytes(pub [u8; ENC_CIPHERTEXT_SIZE_V2]);
|
||||
|
||||
/// a type to represent the raw bytes of a compact note.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompactNotePlaintextBytes(pub [u8; COMPACT_NOTE_SIZE_V2]);
|
||||
|
||||
/// a type to represent the raw bytes of an encrypted compact note.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompactNoteCiphertextBytes(pub [u8; COMPACT_NOTE_SIZE_V2]);
|
||||
|
||||
impl AsMut<[u8]> for NotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for NotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
NotePlaintextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for NoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for NoteCiphertextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
NoteCiphertextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for CompactNotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for CompactNotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
CompactNotePlaintextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
|
||||
///
|
||||
|
@ -58,59 +125,58 @@ 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.
|
||||
/// note_version will return the version of the note plaintext.
|
||||
pub fn note_version(plaintext: &[u8]) -> Option<u8> {
|
||||
match plaintext[0] {
|
||||
0x02 => Some(0x02),
|
||||
0x03 => Some(0x03),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn orchard_parse_note_plaintext_without_memo<F>(
|
||||
domain: &OrchardDomain,
|
||||
plaintext: &[u8],
|
||||
domain: &OrchardDomainV2,
|
||||
plaintext: &[u8], // TODO: replace with CompactNotePlaintextBytes
|
||||
get_validated_pk_d: F,
|
||||
) -> Option<(Note, Address)>
|
||||
where
|
||||
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
||||
{
|
||||
assert!(plaintext.len() >= COMPACT_NOTE_SIZE);
|
||||
assert!(plaintext.len() >= COMPACT_NOTE_SIZE_V2);
|
||||
|
||||
// Check note plaintext version
|
||||
// and parse the asset type accordingly.
|
||||
let asset = parse_version_and_asset_type(plaintext)?;
|
||||
if note_version(plaintext).unwrap() != 0x02 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// The unwraps below are guaranteed to succeed by the assertion above
|
||||
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(),
|
||||
plaintext[20..COMPACT_NOTE_SIZE_V2].try_into().unwrap(),
|
||||
&domain.rho,
|
||||
))?;
|
||||
|
||||
let pk_d = get_validated_pk_d(&diversifier)?;
|
||||
|
||||
let recipient = Address::from_parts(diversifier, pk_d);
|
||||
|
||||
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
|
||||
let note = Option::from(Note::from_parts(
|
||||
recipient,
|
||||
value,
|
||||
AssetId::native(), //V2 notes are always native.
|
||||
domain.rho,
|
||||
rseed,
|
||||
))?;
|
||||
Some((note, recipient))
|
||||
}
|
||||
|
||||
fn parse_version_and_asset_type(plaintext: &[u8]) -> Option<AssetId> {
|
||||
// TODO: make this constant-time?
|
||||
match plaintext[0] {
|
||||
0x02 => Some(AssetId::native()),
|
||||
0x03 if plaintext.len() >= COMPACT_ZSA_NOTE_SIZE => {
|
||||
let bytes = &plaintext[COMPACT_NOTE_SIZE..COMPACT_ZSA_NOTE_SIZE]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
AssetId::from_bytes(bytes).into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Orchard-specific note encryption logic.
|
||||
#[derive(Debug)]
|
||||
pub struct OrchardDomain {
|
||||
pub struct OrchardDomainV2 {
|
||||
rho: Nullifier,
|
||||
}
|
||||
|
||||
impl memuse::DynamicUsage for OrchardDomain {
|
||||
impl memuse::DynamicUsage for OrchardDomainV2 {
|
||||
fn dynamic_usage(&self) -> usize {
|
||||
self.rho.dynamic_usage()
|
||||
}
|
||||
|
@ -120,21 +186,21 @@ impl memuse::DynamicUsage for OrchardDomain {
|
|||
}
|
||||
}
|
||||
|
||||
impl OrchardDomain {
|
||||
impl OrchardDomainV2 {
|
||||
/// Constructs a domain that can be used to trial-decrypt this action's output note.
|
||||
pub fn for_action<T>(act: &Action<T>) -> Self {
|
||||
OrchardDomain {
|
||||
OrchardDomainV2 {
|
||||
rho: *act.nullifier(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a domain from a nullifier.
|
||||
pub fn for_nullifier(nullifier: Nullifier) -> Self {
|
||||
OrchardDomain { rho: nullifier }
|
||||
OrchardDomainV2 { rho: nullifier }
|
||||
}
|
||||
}
|
||||
|
||||
impl Domain for OrchardDomain {
|
||||
impl Domain for OrchardDomainV2 {
|
||||
type EphemeralSecretKey = EphemeralSecretKey;
|
||||
type EphemeralPublicKey = EphemeralPublicKey;
|
||||
type PreparedEphemeralPublicKey = PreparedEphemeralPublicKey;
|
||||
|
@ -150,6 +216,11 @@ impl Domain for OrchardDomain {
|
|||
type ExtractedCommitmentBytes = [u8; 32];
|
||||
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
|
||||
|
||||
type NotePlaintextBytes = NotePlaintextBytes;
|
||||
type NoteCiphertextBytes = NoteCiphertextBytes;
|
||||
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
|
||||
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
|
||||
|
||||
fn derive_esk(note: &Self::Note) -> Option<Self::EphemeralSecretKey> {
|
||||
Some(note.esk())
|
||||
}
|
||||
|
@ -192,23 +263,12 @@ impl Domain for OrchardDomain {
|
|||
_: &Self::Recipient,
|
||||
memo: &Self::Memo,
|
||||
) -> NotePlaintextBytes {
|
||||
let is_native: bool = note.asset().is_native().into();
|
||||
|
||||
let mut np = [0; NOTE_PLAINTEXT_SIZE];
|
||||
np[0] = if is_native { 0x02 } else { 0x03 };
|
||||
let mut np = [0; NOTE_PLAINTEXT_SIZE_V2];
|
||||
np[0] = 0x02;
|
||||
np[1..12].copy_from_slice(note.recipient().diversifier().as_array());
|
||||
np[12..20].copy_from_slice(¬e.value().to_bytes());
|
||||
// todo: add asset_id
|
||||
np[20..52].copy_from_slice(note.rseed().as_bytes());
|
||||
if is_native {
|
||||
np[52..].copy_from_slice(memo);
|
||||
} else {
|
||||
let zsa_type = note.asset().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.
|
||||
};
|
||||
np[52..].copy_from_slice(memo);
|
||||
NotePlaintextBytes(np)
|
||||
}
|
||||
|
||||
|
@ -246,9 +306,9 @@ impl Domain for OrchardDomain {
|
|||
fn parse_note_plaintext_without_memo_ivk(
|
||||
&self,
|
||||
ivk: &Self::IncomingViewingKey,
|
||||
plaintext: &[u8],
|
||||
plaintext: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
|
||||
orchard_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
|
||||
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
|
||||
})
|
||||
}
|
||||
|
@ -258,7 +318,7 @@ impl Domain for OrchardDomain {
|
|||
pk_d: &Self::DiversifiedTransmissionKey,
|
||||
esk: &Self::EphemeralSecretKey,
|
||||
ephemeral_key: &EphemeralKeyBytes,
|
||||
plaintext: &NotePlaintextBytes,
|
||||
plaintext: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, &plaintext.0, |diversifier| {
|
||||
if esk
|
||||
|
@ -274,21 +334,12 @@ impl Domain for OrchardDomain {
|
|||
})
|
||||
}
|
||||
|
||||
fn extract_memo(&self, plaintext: &NotePlaintextBytes) -> Self::Memo {
|
||||
let mut memo = [0; MEMO_SIZE];
|
||||
match get_note_plaintext_version(plaintext) {
|
||||
0x02 => {
|
||||
let full_memo = &plaintext.0[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE];
|
||||
memo.copy_from_slice(full_memo);
|
||||
}
|
||||
0x03 => {
|
||||
// ZSA note plaintexts 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_memo(
|
||||
&self,
|
||||
plaintext: &NotePlaintextBytes,
|
||||
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
|
||||
let (compact, memo) = plaintext.0.split_at(COMPACT_NOTE_SIZE_V2);
|
||||
(compact.try_into().unwrap(), memo.try_into().unwrap())
|
||||
}
|
||||
|
||||
fn extract_pk_d(out_plaintext: &OutPlaintextBytes) -> Option<Self::DiversifiedTransmissionKey> {
|
||||
|
@ -301,7 +352,7 @@ impl Domain for OrchardDomain {
|
|||
}
|
||||
}
|
||||
|
||||
impl BatchDomain for OrchardDomain {
|
||||
impl BatchDomain for OrchardDomainV2 {
|
||||
fn batch_kdf<'a>(
|
||||
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
|
||||
) -> Vec<Option<Self::SymmetricKey>> {
|
||||
|
@ -316,14 +367,10 @@ impl BatchDomain for OrchardDomain {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_note_plaintext_version(plaintext: &NotePlaintextBytes) -> u8 {
|
||||
plaintext.0[0]
|
||||
}
|
||||
|
||||
/// Implementation of in-band secret distribution for Orchard bundles.
|
||||
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
|
||||
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomainV2>;
|
||||
|
||||
impl<T> ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for Action<T> {
|
||||
impl<T> ShieldedOutput<OrchardDomainV2> for Action<T> {
|
||||
fn ephemeral_key(&self) -> EphemeralKeyBytes {
|
||||
EphemeralKeyBytes(self.encrypted_note().epk_bytes)
|
||||
}
|
||||
|
@ -332,8 +379,16 @@ impl<T> ShieldedOutput<OrchardDomain, ENC_CIPHERTEXT_SIZE> for Action<T> {
|
|||
self.cmx().to_bytes()
|
||||
}
|
||||
|
||||
fn enc_ciphertext(&self) -> &[u8; ENC_CIPHERTEXT_SIZE] {
|
||||
&self.encrypted_note().enc_ciphertext
|
||||
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
|
||||
Some(NoteCiphertextBytes(self.encrypted_note().enc_ciphertext))
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
CompactNoteCiphertextBytes(
|
||||
self.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,7 +397,7 @@ pub struct CompactAction {
|
|||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: [u8; COMPACT_NOTE_SIZE],
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CompactAction {
|
||||
|
@ -351,20 +406,25 @@ impl fmt::Debug for CompactAction {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> From<&Action<T>> for CompactAction {
|
||||
impl<T> From<&Action<T>> for CompactAction
|
||||
where
|
||||
Action<T>: ShieldedOutput<OrchardDomainV2>,
|
||||
{
|
||||
fn from(action: &Action<T>) -> Self {
|
||||
CompactAction {
|
||||
nullifier: *action.nullifier(),
|
||||
cmx: *action.cmx(),
|
||||
ephemeral_key: action.ephemeral_key(),
|
||||
enc_ciphertext: action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
enc_ciphertext: CompactNoteCiphertextBytes(
|
||||
action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShieldedOutput<OrchardDomain, COMPACT_NOTE_SIZE> for CompactAction {
|
||||
impl ShieldedOutput<OrchardDomainV2> for CompactAction {
|
||||
fn ephemeral_key(&self) -> EphemeralKeyBytes {
|
||||
EphemeralKeyBytes(self.ephemeral_key.0)
|
||||
}
|
||||
|
@ -373,8 +433,12 @@ impl ShieldedOutput<OrchardDomain, COMPACT_NOTE_SIZE> for CompactAction {
|
|||
self.cmx.to_bytes()
|
||||
}
|
||||
|
||||
fn enc_ciphertext(&self) -> &[u8; COMPACT_NOTE_SIZE] {
|
||||
&self.enc_ciphertext
|
||||
fn enc_ciphertext(&self) -> Option<NoteCiphertextBytes> {
|
||||
None
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
self.enc_ciphertext.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -384,7 +448,7 @@ impl CompactAction {
|
|||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: [u8; 52],
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
) -> Self {
|
||||
Self {
|
||||
nullifier,
|
||||
|
@ -402,14 +466,14 @@ impl CompactAction {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use proptest::prelude::*;
|
||||
use proptest::proptest;
|
||||
use rand::rngs::OsRng;
|
||||
use zcash_note_encryption::{
|
||||
try_compact_note_decryption, try_note_decryption, try_output_recovery_with_ovk, Domain,
|
||||
EphemeralKeyBytes,
|
||||
};
|
||||
|
||||
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
|
||||
use super::{prf_ock_orchard, CompactAction, OrchardDomainV2, OrchardNoteEncryption};
|
||||
use crate::note::AssetId;
|
||||
use crate::{
|
||||
action::Action,
|
||||
|
@ -418,53 +482,46 @@ mod tests {
|
|||
OutgoingViewingKey, PreparedIncomingViewingKey,
|
||||
},
|
||||
note::{
|
||||
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
|
||||
testing::arb_native_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
|
||||
TransmittedNoteCiphertext,
|
||||
},
|
||||
note_encryption::{
|
||||
note_version, orchard_parse_note_plaintext_without_memo, NoteCiphertextBytes,
|
||||
},
|
||||
primitives::redpallas,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
use super::{get_note_plaintext_version, orchard_parse_note_plaintext_without_memo};
|
||||
|
||||
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;
|
||||
#[test]
|
||||
fn test_encoding_roundtrip(
|
||||
note in arb_native_note(),
|
||||
) {
|
||||
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
|
||||
|
||||
// Encode.
|
||||
let plaintext = OrchardDomain::note_plaintext_bytes(¬e, ¬e.recipient(), memo);
|
||||
// Encode.
|
||||
let mut plaintext = OrchardDomainV2::note_plaintext_bytes(¬e, ¬e.recipient(), memo);
|
||||
|
||||
// Decode.
|
||||
let domain = OrchardDomain { rho: note.rho() };
|
||||
let parsed_version = get_note_plaintext_version(&plaintext);
|
||||
let parsed_memo = domain.extract_memo(&plaintext);
|
||||
// Decode.
|
||||
let domain = OrchardDomainV2 { rho: note.rho() };
|
||||
let parsed_version = note_version(plaintext.as_mut()).unwrap();
|
||||
let (compact,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");
|
||||
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact.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.asset().is_native().into() {
|
||||
assert_eq!(parsed_version, 0x02);
|
||||
// Check.
|
||||
assert_eq!(parsed_note, note);
|
||||
assert_eq!(parsed_recipient, note.recipient());
|
||||
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);
|
||||
assert_eq!(parsed_version, 0x02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors() {
|
||||
|
@ -509,13 +566,7 @@ mod tests {
|
|||
assert_eq!(ock.as_ref(), tv.ock);
|
||||
|
||||
let recipient = Address::from_parts(d, pk_d);
|
||||
|
||||
let asset = match tv.asset {
|
||||
None => AssetId::native(),
|
||||
Some(type_bytes) => AssetId::from_bytes(&type_bytes).unwrap(),
|
||||
};
|
||||
|
||||
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
|
||||
let note = Note::from_parts(recipient, value, AssetId::native(), rho, rseed).unwrap();
|
||||
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||
|
||||
let action = Action::from_parts(
|
||||
|
@ -538,7 +589,7 @@ mod tests {
|
|||
// (Tested first because it only requires immutable references.)
|
||||
//
|
||||
|
||||
let domain = OrchardDomain { rho };
|
||||
let domain = OrchardDomainV2 { rho };
|
||||
|
||||
match try_note_decryption(&domain, &ivk, &action) {
|
||||
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
|
||||
|
@ -554,8 +605,7 @@ mod tests {
|
|||
assert_eq!(decrypted_note, note);
|
||||
assert_eq!(decrypted_to, recipient);
|
||||
}
|
||||
None => assert!(tv.asset.is_some(), "Compact note decryption failed"),
|
||||
// Ignore that ZSA notes are not detected in compact decryption.
|
||||
None => panic!("Compact note decryption failed"),
|
||||
}
|
||||
|
||||
match try_output_recovery_with_ovk(&domain, &ovk, &action, &cv_net, &tv.c_out) {
|
||||
|
|
|
@ -0,0 +1,717 @@
|
|||
//! In-band secret distribution for Orchard bundles.
|
||||
|
||||
use blake2b_simd::{Hash, Params};
|
||||
use core::fmt;
|
||||
use group::ff::PrimeField;
|
||||
use zcash_note_encryption::{
|
||||
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
|
||||
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
|
||||
};
|
||||
|
||||
use crate::note::AssetId;
|
||||
use crate::{
|
||||
action::Action,
|
||||
keys::{
|
||||
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
|
||||
IncomingViewingKey, OutgoingViewingKey, SharedSecret,
|
||||
},
|
||||
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
|
||||
spec::diversify_hash,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
||||
|
||||
/// The size of a v2 compact note.
|
||||
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
|
||||
11 + // diversifier
|
||||
8 + // value
|
||||
32; // rseed (or rcm prior to ZIP 212)
|
||||
/// The size of [`NotePlaintextBytes`] for V2.
|
||||
pub const NOTE_PLAINTEXT_SIZE_V2: usize = COMPACT_NOTE_SIZE_V2 + MEMO_SIZE;
|
||||
/// The size of an encrypted note plaintext.
|
||||
pub const ENC_CIPHERTEXT_SIZE_V2: usize = NOTE_PLAINTEXT_SIZE_V2 + AEAD_TAG_SIZE;
|
||||
|
||||
/// The size of the encoding of a ZSA asset id.
|
||||
const ZSA_ASSET_SIZE: usize = 32;
|
||||
/// The size of a v3 compact note.
|
||||
const COMPACT_NOTE_SIZE_V3: usize = COMPACT_NOTE_SIZE_V2 + ZSA_ASSET_SIZE;
|
||||
/// The size of [`NotePlaintextBytes`] for V3.
|
||||
const NOTE_PLAINTEXT_SIZE_V3: usize = COMPACT_NOTE_SIZE_V3 + MEMO_SIZE;
|
||||
/// The size of the encrypted ciphertext of the ZSA variant of a note.
|
||||
const ENC_CIPHERTEXT_SIZE_V3: usize = NOTE_PLAINTEXT_SIZE_V3 + AEAD_TAG_SIZE;
|
||||
|
||||
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
|
||||
///
|
||||
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: VA: Needs updating
|
||||
/// 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: &CompactNotePlaintextBytes,
|
||||
get_validated_pk_d: F,
|
||||
) -> Option<(Note, Address)>
|
||||
where
|
||||
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
||||
{
|
||||
// Check note plaintext version
|
||||
// and parse the asset type accordingly.
|
||||
let asset = parse_version_and_asset_type(plaintext)?;
|
||||
|
||||
let mut plaintext_inner = [0u8; COMPACT_NOTE_SIZE_V2];
|
||||
match plaintext {
|
||||
CompactNotePlaintextBytes::V2(x) => {
|
||||
plaintext_inner.copy_from_slice(&x[..COMPACT_NOTE_SIZE_V2])
|
||||
}
|
||||
CompactNotePlaintextBytes::V3(x) => {
|
||||
plaintext_inner.copy_from_slice(&x[..COMPACT_NOTE_SIZE_V2])
|
||||
}
|
||||
}
|
||||
|
||||
// The unwraps below are guaranteed to succeed by the assertion above
|
||||
let diversifier = Diversifier::from_bytes(plaintext_inner[1..12].try_into().unwrap());
|
||||
let value = NoteValue::from_bytes(plaintext_inner[12..20].try_into().unwrap());
|
||||
let rseed = Option::from(RandomSeed::from_bytes(
|
||||
plaintext_inner[20..COMPACT_NOTE_SIZE_V2]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
&domain.rho,
|
||||
))?;
|
||||
|
||||
let pk_d = get_validated_pk_d(&diversifier)?;
|
||||
|
||||
let recipient = Address::from_parts(diversifier, pk_d);
|
||||
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
|
||||
Some((note, recipient))
|
||||
}
|
||||
|
||||
fn parse_version_and_asset_type(plaintext: &CompactNotePlaintextBytes) -> Option<AssetId> {
|
||||
match plaintext {
|
||||
CompactNotePlaintextBytes::V2(x) if x[0] == 0x02 => Some(AssetId::native()),
|
||||
CompactNotePlaintextBytes::V3(x) if x[0] == 0x03 => {
|
||||
let bytes = x[COMPACT_NOTE_SIZE_V2..COMPACT_NOTE_SIZE_V3]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
AssetId::from_bytes(bytes).into()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Orchard-specific note encryption logic.
|
||||
#[derive(Debug)]
|
||||
pub struct OrchardDomain {
|
||||
rho: Nullifier,
|
||||
}
|
||||
|
||||
/// Newtype for encoding the note plaintext post ZSA.
|
||||
// pub struct NotePlaintextZSA (pub [u8; ZSA_NOTE_PLAINTEXT_SIZE]);
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NotePlaintextBytes {
|
||||
/// Variant for old note plaintexts.
|
||||
V2([u8; NOTE_PLAINTEXT_SIZE_V2]),
|
||||
/// Variant for the new note plaintexts post ZSA.
|
||||
V3([u8; NOTE_PLAINTEXT_SIZE_V3]),
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for NotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
match self {
|
||||
NotePlaintextBytes::V2(x) => x.as_mut(),
|
||||
NotePlaintextBytes::V3(x) => x.as_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for NotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match s.len() {
|
||||
NOTE_PLAINTEXT_SIZE_V2 => NotePlaintextBytes::V2(s.try_into().unwrap()),
|
||||
NOTE_PLAINTEXT_SIZE_V3 => NotePlaintextBytes::V3(s.try_into().unwrap()),
|
||||
_ => panic!("Invalid note plaintext size"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype for encoding the encrypted note ciphertext post ZSA.
|
||||
// pub struct EncNoteCiphertextZSA (pub [u8; ZSA_ENC_CIPHERTEXT_SIZE]);
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NoteCiphertextBytes {
|
||||
/// Variant for old encrypted note ciphertexts.
|
||||
V2([u8; ENC_CIPHERTEXT_SIZE_V2]),
|
||||
/// Variant for new encrypted note ciphertexts post ZSA.
|
||||
V3([u8; ENC_CIPHERTEXT_SIZE_V3]),
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for NoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
NoteCiphertextBytes::V2(x) => x,
|
||||
NoteCiphertextBytes::V3(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Panics if the given slice is not `ENC_CIPHERTEXT_SIZE_V2` or `ENC_CIPHERTEXT_SIZE_V3` bytes long.
|
||||
impl From<&[u8]> for NoteCiphertextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match s.len() {
|
||||
ENC_CIPHERTEXT_SIZE_V2 => NoteCiphertextBytes::V2(s.try_into().unwrap()),
|
||||
ENC_CIPHERTEXT_SIZE_V3 => NoteCiphertextBytes::V3(s.try_into().unwrap()),
|
||||
_ => panic!("Invalid length for compact note plaintext"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype for encoding a compact note
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CompactNotePlaintextBytes {
|
||||
/// Variant for old compact notes.
|
||||
V2([u8; COMPACT_NOTE_SIZE_V2]),
|
||||
/// Variant for new compact notes post ZSA.
|
||||
V3([u8; COMPACT_NOTE_SIZE_V3]),
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for CompactNotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
match self {
|
||||
CompactNotePlaintextBytes::V2(x) => x,
|
||||
CompactNotePlaintextBytes::V3(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Panics if the given slice is not `COMPACT_NOTE_SIZE_V2` or `COMPACT_NOTE_SIZE_V3` bytes long.
|
||||
impl From<&[u8]> for CompactNotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match s.len() {
|
||||
COMPACT_NOTE_SIZE_V2 => CompactNotePlaintextBytes::V2(s.try_into().unwrap()),
|
||||
COMPACT_NOTE_SIZE_V3 => CompactNotePlaintextBytes::V3(s.try_into().unwrap()),
|
||||
_ => panic!("Invalid length for compact note plaintext"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Newtype for encoding a compact note
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CompactNoteCiphertextBytes {
|
||||
/// Variant for old compact notes.
|
||||
V2([u8; COMPACT_NOTE_SIZE_V2]),
|
||||
/// Variant for new compact notes post ZSA.
|
||||
V3([u8; COMPACT_NOTE_SIZE_V3]),
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
CompactNoteCiphertextBytes::V2(x) => x,
|
||||
CompactNoteCiphertextBytes::V3(x) => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrchardDomain {
|
||||
/// Constructs a domain that can be used to trial-decrypt this action's output note.
|
||||
pub fn for_action<T>(act: &Action<T>) -> Self {
|
||||
OrchardDomain {
|
||||
rho: *act.nullifier(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a domain from a nullifier.
|
||||
pub fn for_nullifier(nullifier: Nullifier) -> Self {
|
||||
OrchardDomain { rho: nullifier }
|
||||
}
|
||||
}
|
||||
|
||||
impl Domain for OrchardDomain {
|
||||
type EphemeralSecretKey = EphemeralSecretKey;
|
||||
type EphemeralPublicKey = EphemeralPublicKey;
|
||||
type PreparedEphemeralPublicKey = EphemeralPublicKey;
|
||||
type SharedSecret = SharedSecret;
|
||||
type SymmetricKey = Hash;
|
||||
type Note = Note;
|
||||
type Recipient = Address;
|
||||
type DiversifiedTransmissionKey = DiversifiedTransmissionKey;
|
||||
type IncomingViewingKey = IncomingViewingKey;
|
||||
type OutgoingViewingKey = OutgoingViewingKey;
|
||||
type ValueCommitment = ValueCommitment;
|
||||
type ExtractedCommitment = ExtractedNoteCommitment;
|
||||
type ExtractedCommitmentBytes = [u8; 32];
|
||||
type Memo = [u8; MEMO_SIZE]; // TODO use a more interesting type
|
||||
|
||||
type NotePlaintextBytes = NotePlaintextBytes;
|
||||
type NoteCiphertextBytes = NoteCiphertextBytes;
|
||||
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
|
||||
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
|
||||
|
||||
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 {
|
||||
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,
|
||||
_: &Self::Recipient,
|
||||
memo: &Self::Memo,
|
||||
) -> NotePlaintextBytes {
|
||||
let mut np = [0u8; NOTE_PLAINTEXT_SIZE_V3];
|
||||
np[0] = 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());
|
||||
let zsa_type = note.asset().to_bytes();
|
||||
np[52..84].copy_from_slice(&zsa_type);
|
||||
np[84..].copy_from_slice(memo);
|
||||
NotePlaintextBytes::V3(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: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
|
||||
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_note_plaintext_without_memo_ovk(
|
||||
&self,
|
||||
pk_d: &Self::DiversifiedTransmissionKey,
|
||||
esk: &Self::EphemeralSecretKey,
|
||||
ephemeral_key: &EphemeralKeyBytes,
|
||||
plaintext: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
|
||||
if esk
|
||||
.derive_public(diversify_hash(diversifier.as_array()))
|
||||
.to_bytes()
|
||||
.0
|
||||
== ephemeral_key.0
|
||||
{
|
||||
Some(*pk_d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_memo(
|
||||
&self,
|
||||
plaintext: &NotePlaintextBytes,
|
||||
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
|
||||
match plaintext {
|
||||
NotePlaintextBytes::V2(np) => {
|
||||
let (compact, memo) = np.split_at(COMPACT_NOTE_SIZE_V2);
|
||||
(
|
||||
CompactNotePlaintextBytes::V2(compact.try_into().unwrap()),
|
||||
memo.try_into().unwrap(),
|
||||
)
|
||||
}
|
||||
NotePlaintextBytes::V3(np) => {
|
||||
let (compact, memo) = np.split_at(COMPACT_NOTE_SIZE_V3);
|
||||
(
|
||||
CompactNotePlaintextBytes::V3(compact.try_into().unwrap()),
|
||||
memo.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 BatchDomain for OrchardDomain {
|
||||
fn batch_kdf<'a>(
|
||||
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
|
||||
) -> Vec<Option<Self::SymmetricKey>> {
|
||||
let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
|
||||
|
||||
SharedSecret::batch_to_affine(shared_secrets)
|
||||
.zip(ephemeral_keys.into_iter())
|
||||
.map(|(secret, ephemeral_key)| {
|
||||
secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Implementation of in-band secret distribution for Orchard bundles.
|
||||
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomain>;
|
||||
|
||||
impl<T> ShieldedOutput<OrchardDomain> 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) -> Option<NoteCiphertextBytes> {
|
||||
let result = self.encrypted_note().enc_ciphertext.clone();
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
match self.encrypted_note().enc_ciphertext {
|
||||
NoteCiphertextBytes::V2(ncx) => {
|
||||
CompactNoteCiphertextBytes::V2(ncx[..COMPACT_NOTE_SIZE_V2].try_into().unwrap())
|
||||
}
|
||||
NoteCiphertextBytes::V3(ncx) => {
|
||||
CompactNoteCiphertextBytes::V3(ncx[..COMPACT_NOTE_SIZE_V3].try_into().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact Action for light clients.
|
||||
pub struct CompactAction {
|
||||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
}
|
||||
|
||||
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 {
|
||||
let comp_ciphertext: CompactNoteCiphertextBytes =
|
||||
match action.encrypted_note().enc_ciphertext {
|
||||
NoteCiphertextBytes::V2(ncx) => {
|
||||
CompactNoteCiphertextBytes::V2(ncx[..COMPACT_NOTE_SIZE_V2].try_into().unwrap())
|
||||
}
|
||||
NoteCiphertextBytes::V3(ncx) => {
|
||||
CompactNoteCiphertextBytes::V3(ncx[..COMPACT_NOTE_SIZE_V3].try_into().unwrap())
|
||||
}
|
||||
};
|
||||
CompactAction {
|
||||
nullifier: *action.nullifier(),
|
||||
cmx: *action.cmx(),
|
||||
ephemeral_key: action.ephemeral_key(),
|
||||
enc_ciphertext: comp_ciphertext,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShieldedOutput<OrchardDomain> 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) -> Option<NoteCiphertextBytes> {
|
||||
None
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
&self.enc_ciphertext
|
||||
}
|
||||
}
|
||||
|
||||
impl CompactAction {
|
||||
/// Create a CompactAction from its constituent parts
|
||||
pub fn from_parts(
|
||||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
) -> Self {
|
||||
Self {
|
||||
nullifier,
|
||||
cmx,
|
||||
ephemeral_key,
|
||||
enc_ciphertext,
|
||||
}
|
||||
}
|
||||
|
||||
///Returns the nullifier of the note being spent.
|
||||
pub fn nullifier(&self) -> Nullifier {
|
||||
self.nullifier
|
||||
}
|
||||
}
|
||||
|
||||
#[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, Domain,
|
||||
EphemeralKeyBytes,
|
||||
};
|
||||
|
||||
use super::{prf_ock_orchard, CompactAction, OrchardDomain, OrchardNoteEncryption};
|
||||
use crate::note::AssetId;
|
||||
use crate::note_encryption::NoteCiphertextBytes;
|
||||
use crate::{
|
||||
action::Action,
|
||||
keys::{
|
||||
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
|
||||
OutgoingViewingKey,
|
||||
},
|
||||
note::{
|
||||
testing::arb_note, ExtractedNoteCommitment, Nullifier, RandomSeed,
|
||||
TransmittedNoteCiphertext,
|
||||
},
|
||||
primitives::redpallas,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
use super::{version, orchard_parse_note_plaintext_without_memo};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_encoding_roundtrip(
|
||||
note in arb_note(NoteValue::from_raw(100)),
|
||||
) {
|
||||
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
|
||||
|
||||
// Encode.
|
||||
let mut plaintext = OrchardDomain::note_plaintext_bytes(¬e, ¬e.recipient(), memo);
|
||||
|
||||
// Decode.
|
||||
let domain = OrchardDomain { rho: note.rho() };
|
||||
let parsed_version = version(plaintext.as_mut()).unwrap();
|
||||
let (mut compact,parsed_memo) = domain.extract_memo(&plaintext);
|
||||
|
||||
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact.as_mut(),
|
||||
|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());
|
||||
assert_eq!(&parsed_memo, memo);
|
||||
assert_eq!(parsed_version, 0x03);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors() {
|
||||
let test_vectors = crate::test_vectors::note_encryption::test_vectors();
|
||||
|
||||
for tv in test_vectors {
|
||||
//
|
||||
// Load the test vector components
|
||||
//
|
||||
|
||||
// Recipient key material
|
||||
let ivk = 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();
|
||||
|
||||
// Received Action
|
||||
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
|
||||
let rho = Nullifier::from_bytes(&tv.rho).unwrap();
|
||||
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
|
||||
|
||||
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
|
||||
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
|
||||
|
||||
// Details about the expected note
|
||||
let value = NoteValue::from_raw(tv.v);
|
||||
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
|
||||
|
||||
//
|
||||
// Test the individual components
|
||||
//
|
||||
|
||||
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 asset = match tv.asset {
|
||||
None => AssetId::native(),
|
||||
Some(type_bytes) => AssetId::from_bytes(&type_bytes).unwrap(),
|
||||
};
|
||||
|
||||
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
|
||||
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||
|
||||
let action = Action::from_parts(
|
||||
// rho is the nullifier in the receiving Action.
|
||||
rho,
|
||||
// We don't need a valid rk for this test.
|
||||
redpallas::VerificationKey::dummy(),
|
||||
cmx,
|
||||
TransmittedNoteCiphertext {
|
||||
epk_bytes: ephemeral_key.0,
|
||||
enc_ciphertext: NoteCiphertextBytes::V3(tv.c_enc), // TODO: VA: Would need a mix of V2 and V3 eventually
|
||||
out_ciphertext: tv.c_out,
|
||||
},
|
||||
cv_net.clone(),
|
||||
(),
|
||||
);
|
||||
|
||||
//
|
||||
// Test decryption
|
||||
// (Tested first because it only requires immutable references.)
|
||||
//
|
||||
|
||||
let domain = OrchardDomain { 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"),
|
||||
}
|
||||
|
||||
//
|
||||
// Test encryption
|
||||
//
|
||||
|
||||
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, recipient, 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[..]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,634 @@
|
|||
//! In-band secret distribution for Orchard bundles.
|
||||
|
||||
use blake2b_simd::{Hash, Params};
|
||||
use core::fmt;
|
||||
use group::ff::PrimeField;
|
||||
use zcash_note_encryption::{
|
||||
BatchDomain, Domain, EphemeralKeyBytes, OutPlaintextBytes, OutgoingCipherKey, ShieldedOutput,
|
||||
AEAD_TAG_SIZE, MEMO_SIZE, OUT_PLAINTEXT_SIZE,
|
||||
};
|
||||
|
||||
use crate::note::AssetId;
|
||||
use crate::{
|
||||
action::Action,
|
||||
keys::{
|
||||
DiversifiedTransmissionKey, Diversifier, EphemeralPublicKey, EphemeralSecretKey,
|
||||
OutgoingViewingKey, PreparedEphemeralPublicKey, PreparedIncomingViewingKey, SharedSecret,
|
||||
},
|
||||
note::{ExtractedNoteCommitment, Nullifier, RandomSeed},
|
||||
spec::diversify_hash,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
const PRF_OCK_ORCHARD_PERSONALIZATION: &[u8; 16] = b"Zcash_Orchardock";
|
||||
|
||||
/// The size of a v2 compact note.
|
||||
pub const COMPACT_NOTE_SIZE_V2: usize = 1 + // version
|
||||
11 + // diversifier
|
||||
8 + // value
|
||||
32; // rseed (or rcm prior to ZIP 212)
|
||||
/// The size of [`NotePlaintextBytes`] for V2.
|
||||
|
||||
/// The size of the encoding of a ZSA asset id.
|
||||
const ZSA_ASSET_SIZE: usize = 32;
|
||||
/// The size of a v3 compact note.
|
||||
pub const COMPACT_NOTE_SIZE_V3: usize = COMPACT_NOTE_SIZE_V2 + ZSA_ASSET_SIZE;
|
||||
/// The size of [`NotePlaintextBytes`] for V3.
|
||||
pub const NOTE_PLAINTEXT_SIZE_V3: usize = COMPACT_NOTE_SIZE_V3 + MEMO_SIZE;
|
||||
/// The size of the encrypted ciphertext of the ZSA variant of a note.
|
||||
pub const ENC_CIPHERTEXT_SIZE_V3: usize = NOTE_PLAINTEXT_SIZE_V3 + AEAD_TAG_SIZE;
|
||||
|
||||
/// a type to represent the raw bytes of a note plaintext.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE_V3]);
|
||||
|
||||
/// a type to represent the raw bytes of an encrypted note plaintext.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NoteCiphertextBytes(pub [u8; ENC_CIPHERTEXT_SIZE_V3]);
|
||||
|
||||
/// a type to represent the raw bytes of a compact note.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompactNotePlaintextBytes(pub [u8; COMPACT_NOTE_SIZE_V3]);
|
||||
|
||||
/// a type to represent the raw bytes of an encrypted compact note.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CompactNoteCiphertextBytes(pub [u8; COMPACT_NOTE_SIZE_V3]);
|
||||
|
||||
impl AsMut<[u8]> for NotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for NotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
NotePlaintextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for NoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for NoteCiphertextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
NoteCiphertextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for CompactNotePlaintextBytes {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for CompactNotePlaintextBytes {
|
||||
fn from(s: &[u8]) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
CompactNotePlaintextBytes(s.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for CompactNoteCiphertextBytes {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defined in [Zcash Protocol Spec § 5.4.2: Pseudo Random Functions][concreteprfs].
|
||||
///
|
||||
/// [concreteprfs]: https://zips.z.cash/protocol/nu5.pdf#concreteprfs
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
/// note_version will return the version of the note plaintext.
|
||||
pub fn note_version(plaintext: &[u8]) -> Option<u8> {
|
||||
match plaintext[0] {
|
||||
0x02 => Some(0x02),
|
||||
0x03 => Some(0x03),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: &OrchardDomainV3,
|
||||
plaintext: &CompactNotePlaintextBytes,
|
||||
get_validated_pk_d: F,
|
||||
) -> Option<(Note, Address)>
|
||||
where
|
||||
F: FnOnce(&Diversifier) -> Option<DiversifiedTransmissionKey>,
|
||||
{
|
||||
// The unwraps below are guaranteed to succeed by the assertion above
|
||||
let diversifier = Diversifier::from_bytes(plaintext.0[1..12].try_into().unwrap());
|
||||
let value = NoteValue::from_bytes(plaintext.0[12..20].try_into().unwrap());
|
||||
let rseed = Option::from(RandomSeed::from_bytes(
|
||||
plaintext.0[20..COMPACT_NOTE_SIZE_V2].try_into().unwrap(),
|
||||
&domain.rho,
|
||||
))?;
|
||||
let pk_d = get_validated_pk_d(&diversifier)?;
|
||||
let recipient = Address::from_parts(diversifier, pk_d);
|
||||
|
||||
let asset = match note_version(plaintext.0.as_ref())? {
|
||||
0x02 => AssetId::native(),
|
||||
0x03 => {
|
||||
let bytes = plaintext.0[COMPACT_NOTE_SIZE_V2..COMPACT_NOTE_SIZE_V3]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
AssetId::from_bytes(bytes).unwrap()
|
||||
}
|
||||
_ => panic!("invalid note version"),
|
||||
};
|
||||
|
||||
let note = Option::from(Note::from_parts(recipient, value, asset, domain.rho, rseed))?;
|
||||
Some((note, recipient))
|
||||
}
|
||||
|
||||
/// Orchard-specific note encryption logic.
|
||||
#[derive(Debug)]
|
||||
pub struct OrchardDomainV3 {
|
||||
rho: Nullifier,
|
||||
}
|
||||
|
||||
impl OrchardDomainV3 {
|
||||
/// Constructs a domain that can be used to trial-decrypt this action's output note.
|
||||
pub fn for_action<T>(act: &Action<T>) -> Self {
|
||||
OrchardDomainV3 {
|
||||
rho: *act.nullifier(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a domain from a nullifier.
|
||||
pub fn for_nullifier(nullifier: Nullifier) -> Self {
|
||||
OrchardDomainV3 { rho: nullifier }
|
||||
}
|
||||
}
|
||||
|
||||
impl Domain for OrchardDomainV3 {
|
||||
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; MEMO_SIZE];
|
||||
|
||||
type NotePlaintextBytes = NotePlaintextBytes;
|
||||
type NoteCiphertextBytes = NoteCiphertextBytes;
|
||||
type CompactNotePlaintextBytes = CompactNotePlaintextBytes;
|
||||
type CompactNoteCiphertextBytes = CompactNoteCiphertextBytes;
|
||||
|
||||
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,
|
||||
_: &Self::Recipient,
|
||||
memo: &Self::Memo,
|
||||
) -> NotePlaintextBytes {
|
||||
let mut np = [0u8; NOTE_PLAINTEXT_SIZE_V3];
|
||||
np[0] = 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..84].copy_from_slice(¬e.asset().to_bytes());
|
||||
np[84..].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: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
|
||||
Some(DiversifiedTransmissionKey::derive(ivk, diversifier))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_note_plaintext_without_memo_ovk(
|
||||
&self,
|
||||
pk_d: &Self::DiversifiedTransmissionKey,
|
||||
esk: &Self::EphemeralSecretKey,
|
||||
ephemeral_key: &EphemeralKeyBytes,
|
||||
plaintext: &CompactNotePlaintextBytes,
|
||||
) -> Option<(Self::Note, Self::Recipient)> {
|
||||
orchard_parse_note_plaintext_without_memo(self, plaintext, |diversifier| {
|
||||
if esk
|
||||
.derive_public(diversify_hash(diversifier.as_array()))
|
||||
.to_bytes()
|
||||
.0
|
||||
== ephemeral_key.0
|
||||
{
|
||||
Some(*pk_d)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_memo(
|
||||
&self,
|
||||
plaintext: &NotePlaintextBytes,
|
||||
) -> (Self::CompactNotePlaintextBytes, Self::Memo) {
|
||||
let (compact, memo) = plaintext.0.split_at(COMPACT_NOTE_SIZE_V3);
|
||||
(
|
||||
CompactNotePlaintextBytes(compact.try_into().unwrap()),
|
||||
memo.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 BatchDomain for OrchardDomainV3 {
|
||||
fn batch_kdf<'a>(
|
||||
items: impl Iterator<Item = (Option<Self::SharedSecret>, &'a EphemeralKeyBytes)>,
|
||||
) -> Vec<Option<Self::SymmetricKey>> {
|
||||
let (shared_secrets, ephemeral_keys): (Vec<_>, Vec<_>) = items.unzip();
|
||||
|
||||
SharedSecret::batch_to_affine(shared_secrets)
|
||||
.zip(ephemeral_keys.into_iter())
|
||||
.map(|(secret, ephemeral_key)| {
|
||||
secret.map(|dhsecret| SharedSecret::kdf_orchard_inner(dhsecret, ephemeral_key))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of in-band secret distribution for Orchard bundles.
|
||||
pub type OrchardNoteEncryption = zcash_note_encryption::NoteEncryption<OrchardDomainV3>;
|
||||
|
||||
impl<T> ShieldedOutput<OrchardDomainV3> 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) -> Option<NoteCiphertextBytes> {
|
||||
Some(NoteCiphertextBytes(self.encrypted_note().enc_ciphertext))
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
CompactNoteCiphertextBytes(
|
||||
self.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V3]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A compact Action for light clients.
|
||||
pub struct CompactAction {
|
||||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CompactAction {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "CompactAction")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&Action<T>> for CompactAction
|
||||
where
|
||||
Action<T>: ShieldedOutput<OrchardDomainV3>,
|
||||
{
|
||||
fn from(action: &Action<T>) -> Self {
|
||||
CompactAction {
|
||||
nullifier: *action.nullifier(),
|
||||
cmx: *action.cmx(),
|
||||
ephemeral_key: action.ephemeral_key(),
|
||||
enc_ciphertext: CompactNoteCiphertextBytes(
|
||||
action.encrypted_note().enc_ciphertext[..COMPACT_NOTE_SIZE_V3]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShieldedOutput<OrchardDomainV3> 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) -> Option<NoteCiphertextBytes> {
|
||||
None
|
||||
}
|
||||
|
||||
fn enc_ciphertext_compact(&self) -> CompactNoteCiphertextBytes {
|
||||
self.enc_ciphertext.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl CompactAction {
|
||||
/// Create a CompactAction from its constituent parts
|
||||
pub fn from_parts(
|
||||
nullifier: Nullifier,
|
||||
cmx: ExtractedNoteCommitment,
|
||||
ephemeral_key: EphemeralKeyBytes,
|
||||
enc_ciphertext: CompactNoteCiphertextBytes,
|
||||
) -> Self {
|
||||
Self {
|
||||
nullifier,
|
||||
cmx,
|
||||
ephemeral_key,
|
||||
enc_ciphertext,
|
||||
}
|
||||
}
|
||||
|
||||
///Returns the nullifier of the note being spent.
|
||||
pub fn nullifier(&self) -> Nullifier {
|
||||
self.nullifier
|
||||
}
|
||||
}
|
||||
|
||||
#[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, Domain,
|
||||
EphemeralKeyBytes,
|
||||
};
|
||||
|
||||
use super::{
|
||||
note_version, orchard_parse_note_plaintext_without_memo, prf_ock_orchard, CompactAction,
|
||||
OrchardDomainV3, OrchardNoteEncryption,
|
||||
};
|
||||
use crate::{
|
||||
action::Action,
|
||||
keys::{
|
||||
DiversifiedTransmissionKey, Diversifier, EphemeralSecretKey, IncomingViewingKey,
|
||||
OutgoingViewingKey, PreparedIncomingViewingKey,
|
||||
},
|
||||
note::{
|
||||
testing::arb_note, AssetId, ExtractedNoteCommitment, Nullifier, RandomSeed,
|
||||
TransmittedNoteCiphertext,
|
||||
},
|
||||
primitives::redpallas,
|
||||
value::{NoteValue, ValueCommitment},
|
||||
Address, Note,
|
||||
};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn test_encoding_roundtrip(
|
||||
note in arb_note(NoteValue::from_raw(100)),
|
||||
) {
|
||||
let memo = &crate::test_vectors::note_encryption::test_vectors()[0].memo;
|
||||
|
||||
// Encode.
|
||||
let mut plaintext = OrchardDomainV3::note_plaintext_bytes(¬e, ¬e.recipient(), memo);
|
||||
|
||||
// Decode.
|
||||
let domain = OrchardDomainV3 { rho: note.rho() };
|
||||
let parsed_version = note_version(plaintext.as_mut()).unwrap();
|
||||
let (compact,parsed_memo) = domain.extract_memo(&plaintext);
|
||||
|
||||
let (parsed_note, parsed_recipient) = orchard_parse_note_plaintext_without_memo(&domain, &compact,
|
||||
|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());
|
||||
assert_eq!(&parsed_memo, memo);
|
||||
assert_eq!(parsed_version, 0x03);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vectors() {
|
||||
let test_vectors = crate::test_vectors::note_encryption_v3::test_vectors();
|
||||
|
||||
for tv in test_vectors {
|
||||
//
|
||||
// Load the test vector components
|
||||
//
|
||||
|
||||
// Recipient key material
|
||||
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();
|
||||
|
||||
// Received Action
|
||||
let cv_net = ValueCommitment::from_bytes(&tv.cv_net).unwrap();
|
||||
let rho = Nullifier::from_bytes(&tv.rho).unwrap();
|
||||
let cmx = ExtractedNoteCommitment::from_bytes(&tv.cmx).unwrap();
|
||||
|
||||
let esk = EphemeralSecretKey::from_bytes(&tv.esk).unwrap();
|
||||
let ephemeral_key = EphemeralKeyBytes(tv.ephemeral_key);
|
||||
|
||||
// Details about the expected note
|
||||
let value = NoteValue::from_raw(tv.v);
|
||||
let rseed = RandomSeed::from_bytes(tv.rseed, &rho).unwrap();
|
||||
|
||||
//
|
||||
// Test the individual components
|
||||
//
|
||||
|
||||
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 asset = AssetId::from_bytes(&tv.asset).unwrap();
|
||||
|
||||
let note = Note::from_parts(recipient, value, asset, rho, rseed).unwrap();
|
||||
assert_eq!(ExtractedNoteCommitment::from(note.commitment()), cmx);
|
||||
|
||||
let action = Action::from_parts(
|
||||
// rho is the nullifier in the receiving Action.
|
||||
rho,
|
||||
// We don't need a valid rk for this test.
|
||||
redpallas::VerificationKey::dummy(),
|
||||
cmx,
|
||||
TransmittedNoteCiphertext {
|
||||
epk_bytes: ephemeral_key.0,
|
||||
enc_ciphertext: tv.c_enc,
|
||||
out_ciphertext: tv.c_out,
|
||||
},
|
||||
cv_net.clone(),
|
||||
(),
|
||||
);
|
||||
|
||||
//
|
||||
// Test decryption
|
||||
// (Tested first because it only requires immutable references.)
|
||||
//
|
||||
|
||||
let domain = OrchardDomainV3 { 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"),
|
||||
}
|
||||
|
||||
//
|
||||
// Test encryption
|
||||
//
|
||||
|
||||
let ne = OrchardNoteEncryption::new_with_esk(esk, Some(ovk), note, recipient, 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[..]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,3 +3,4 @@ pub(crate) mod commitment_tree;
|
|||
pub(crate) mod keys;
|
||||
pub(crate) mod merkle_path;
|
||||
pub(crate) mod note_encryption;
|
||||
pub(crate) mod note_encryption_v3;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -53,13 +53,14 @@ use pasta_curves::{
|
|||
use rand::RngCore;
|
||||
use subtle::CtOption;
|
||||
|
||||
use crate::builder::Error;
|
||||
use crate::note::AssetId;
|
||||
use crate::{
|
||||
constants::fixed_bases::{VALUE_COMMITMENT_PERSONALIZATION, VALUE_COMMITMENT_R_BYTES},
|
||||
primitives::redpallas::{self, Binding},
|
||||
};
|
||||
|
||||
use crate::builder::Error;
|
||||
use crate::note::AssetId;
|
||||
|
||||
/// Maximum note value.
|
||||
pub const MAX_NOTE_VALUE: u64 = u64::MAX;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use orchard::{
|
|||
circuit::{ProvingKey, VerifyingKey},
|
||||
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
|
||||
note::ExtractedNoteCommitment,
|
||||
note_encryption::OrchardDomain,
|
||||
note_encryption_v3::OrchardDomainV3,
|
||||
tree::{MerkleHashOrchard, MerklePath},
|
||||
value::NoteValue,
|
||||
Anchor, Bundle, Note,
|
||||
|
@ -89,7 +89,7 @@ fn bundle_chain() {
|
|||
.actions()
|
||||
.iter()
|
||||
.find_map(|action| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
try_note_decryption(&domain, &ivk, action)
|
||||
})
|
||||
.unwrap();
|
||||
|
|
|
@ -7,7 +7,7 @@ use orchard::bundle::Authorized;
|
|||
use orchard::issuance::{verify_issue_bundle, IssueBundle, Signed, Unauthorized};
|
||||
use orchard::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey};
|
||||
use orchard::note::{AssetId, ExtractedNoteCommitment};
|
||||
use orchard::note_encryption::OrchardDomain;
|
||||
use orchard::note_encryption_v3::OrchardDomainV3;
|
||||
use orchard::tree::{MerkleHashOrchard, MerklePath};
|
||||
use orchard::{
|
||||
builder::Builder,
|
||||
|
@ -204,7 +204,7 @@ fn create_native_note(keys: &Keychain) -> Note {
|
|||
.actions()
|
||||
.iter()
|
||||
.find_map(|action| {
|
||||
let domain = OrchardDomain::for_action(action);
|
||||
let domain = OrchardDomainV3::for_action(action);
|
||||
try_note_decryption(&domain, &PreparedIncomingViewingKey::new(&ivk), action)
|
||||
})
|
||||
.unwrap();
|
||||
|
|
Loading…
Reference in New Issue