Merge pull request #589 from nuttycom/wallet_scan_batch_decrypt
Use batch decryption for wallet scanning
This commit is contained in:
commit
6cf088004f
|
@ -7,6 +7,15 @@ and this library adheres to Rust's notion of
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- Changes to batch decryption APIs:
|
||||
- The return types of `batch::try_note_decryption` and
|
||||
`batch::try_compact_note_decryption` have changed. Now, instead of
|
||||
returning entries corresponding to the cartesian product of the IVKs used for
|
||||
decryption with the outputs being decrypted, this now returns a vector of
|
||||
decryption results of the same length and in the same order as the `outputs`
|
||||
argument to the function. Each successful result includes the index of the
|
||||
entry in `ivks` used to decrypt the value.
|
||||
|
||||
### Changed
|
||||
- MSRV is now 1.56.1.
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//! APIs for batch trial decryption.
|
||||
|
||||
use alloc::vec::Vec; // module is alloc only
|
||||
use core::iter;
|
||||
|
||||
use crate::{
|
||||
try_compact_note_decryption_inner, try_note_decryption_inner, BatchDomain, EphemeralKeyBytes,
|
||||
|
@ -11,21 +10,32 @@ use crate::{
|
|||
/// Trial decryption of a batch of notes with a set of recipients.
|
||||
///
|
||||
/// This is the batched version of [`crate::try_note_decryption`].
|
||||
///
|
||||
/// Returns a vector containing the decrypted result for each output,
|
||||
/// with the same length and in the same order as the outputs were
|
||||
/// provided, along with the index in the `ivks` slice associated with
|
||||
/// the IVK that successfully decrypted the output.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn try_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
|
||||
ivks: &[D::IncomingViewingKey],
|
||||
outputs: &[(D, Output)],
|
||||
) -> Vec<Option<(D::Note, D::Recipient, D::Memo)>> {
|
||||
) -> Vec<Option<((D::Note, D::Recipient, D::Memo), usize)>> {
|
||||
batch_note_decryption(ivks, outputs, try_note_decryption_inner)
|
||||
}
|
||||
|
||||
/// Trial decryption of a batch of notes for light clients with a set of recipients.
|
||||
///
|
||||
/// This is the batched version of [`crate::try_compact_note_decryption`].
|
||||
///
|
||||
/// Returns a vector containing the decrypted result for each output,
|
||||
/// with the same length and in the same order as the outputs were
|
||||
/// provided, along with the index in the `ivks` slice associated with
|
||||
/// the IVK that successfully decrypted the output.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn try_compact_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
|
||||
ivks: &[D::IncomingViewingKey],
|
||||
outputs: &[(D, Output)],
|
||||
) -> Vec<Option<(D::Note, D::Recipient)>> {
|
||||
) -> Vec<Option<((D::Note, D::Recipient), usize)>> {
|
||||
batch_note_decryption(ivks, outputs, try_compact_note_decryption_inner)
|
||||
}
|
||||
|
||||
|
@ -33,17 +43,21 @@ fn batch_note_decryption<D: BatchDomain, Output: ShieldedOutput<D, CS>, F, FR, c
|
|||
ivks: &[D::IncomingViewingKey],
|
||||
outputs: &[(D, Output)],
|
||||
decrypt_inner: F,
|
||||
) -> Vec<Option<FR>>
|
||||
) -> Vec<Option<(FR, usize)>>
|
||||
where
|
||||
F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, D::SymmetricKey) -> Option<FR>,
|
||||
F: Fn(&D, &D::IncomingViewingKey, &EphemeralKeyBytes, &Output, &D::SymmetricKey) -> Option<FR>,
|
||||
{
|
||||
if ivks.is_empty() {
|
||||
return (0..outputs.len()).map(|_| None).collect();
|
||||
};
|
||||
|
||||
// Fetch the ephemeral keys for each output and batch-parse them.
|
||||
let ephemeral_keys = D::batch_epk(outputs.iter().map(|(_, output)| output.ephemeral_key()));
|
||||
|
||||
// Derive the shared secrets for all combinations of (ivk, output).
|
||||
// The scalar multiplications cannot benefit from batching.
|
||||
let items = ivks.iter().flat_map(|ivk| {
|
||||
ephemeral_keys.iter().map(move |(epk, ephemeral_key)| {
|
||||
let items = ephemeral_keys.iter().flat_map(|(epk, ephemeral_key)| {
|
||||
ivks.iter().map(move |ivk| {
|
||||
(
|
||||
epk.as_ref().map(|epk| D::ka_agree_dec(ivk, epk)),
|
||||
ephemeral_key,
|
||||
|
@ -55,17 +69,18 @@ where
|
|||
let keys = D::batch_kdf(items);
|
||||
|
||||
// Finish the trial decryption!
|
||||
ivks.iter()
|
||||
.flat_map(|ivk| {
|
||||
// Reconstruct the matrix of (ivk, output) combinations.
|
||||
iter::repeat(ivk)
|
||||
.zip(ephemeral_keys.iter())
|
||||
.zip(outputs.iter())
|
||||
keys.chunks(ivks.len())
|
||||
.zip(ephemeral_keys.iter().zip(outputs.iter()))
|
||||
.map(|(key_chunk, ((_, ephemeral_key), (domain, output)))| {
|
||||
key_chunk
|
||||
.iter()
|
||||
.zip(ivks.iter().enumerate())
|
||||
.filter_map(|(key, (i, ivk))| {
|
||||
key.as_ref()
|
||||
.and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key))
|
||||
.map(|out| (out, i))
|
||||
})
|
||||
.next()
|
||||
})
|
||||
.zip(keys)
|
||||
.map(|(((ivk, (_, ephemeral_key)), (domain, output)), key)| {
|
||||
// The `and_then` propagates any potential rejection from `D::epk`.
|
||||
key.and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key))
|
||||
})
|
||||
.collect()
|
||||
.collect::<Vec<Option<_>>>()
|
||||
}
|
||||
|
|
|
@ -520,7 +520,7 @@ pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_S
|
|||
let shared_secret = D::ka_agree_dec(ivk, &epk);
|
||||
let key = D::kdf(shared_secret, &ephemeral_key);
|
||||
|
||||
try_note_decryption_inner(domain, ivk, &ephemeral_key, output, key)
|
||||
try_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
|
||||
}
|
||||
|
||||
fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(
|
||||
|
@ -528,7 +528,7 @@ fn try_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT
|
|||
ivk: &D::IncomingViewingKey,
|
||||
ephemeral_key: &EphemeralKeyBytes,
|
||||
output: &Output,
|
||||
key: D::SymmetricKey,
|
||||
key: &D::SymmetricKey,
|
||||
) -> Option<(D::Note, D::Recipient, D::Memo)> {
|
||||
let enc_ciphertext = output.enc_ciphertext();
|
||||
|
||||
|
@ -617,7 +617,7 @@ pub fn try_compact_note_decryption<D: Domain, Output: ShieldedOutput<D, COMPACT_
|
|||
let shared_secret = D::ka_agree_dec(ivk, &epk);
|
||||
let key = D::kdf(shared_secret, &ephemeral_key);
|
||||
|
||||
try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, key)
|
||||
try_compact_note_decryption_inner(domain, ivk, &ephemeral_key, output, &key)
|
||||
}
|
||||
|
||||
fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPACT_NOTE_SIZE>>(
|
||||
|
@ -625,7 +625,7 @@ fn try_compact_note_decryption_inner<D: Domain, Output: ShieldedOutput<D, COMPAC
|
|||
ivk: &D::IncomingViewingKey,
|
||||
ephemeral_key: &EphemeralKeyBytes,
|
||||
output: &Output,
|
||||
key: D::SymmetricKey,
|
||||
key: &D::SymmetricKey,
|
||||
) -> Option<(D::Note, D::Recipient)> {
|
||||
// Start from block 1 to skip over Poly1305 keying output
|
||||
let mut plaintext = [0; COMPACT_NOTE_SIZE];
|
||||
|
|
|
@ -6,6 +6,7 @@ and this library adheres to Rust's notion of
|
|||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Functionality that enables the receiving and spending of transparent funds,
|
||||
behind the new `transparent-inputs` feature flag.
|
||||
|
@ -60,7 +61,7 @@ and this library adheres to Rust's notion of
|
|||
been replaced by `ephemeral_key`.
|
||||
- `zcash_client_backend::proto::compact_formats::CompactSaplingOutput`: the
|
||||
`epk` method has been replaced by `ephemeral_key`.
|
||||
- `data_api::wallet::spend_to_address` now takes a `min_confirmations`
|
||||
- `data_api::wallet::spend_to_address` now takes a `min_confirmations`
|
||||
parameter, which the caller can provide to specify the minimum number of
|
||||
confirmations required for notes being selected. A default value of 10
|
||||
confirmations is recommended.
|
||||
|
@ -107,6 +108,12 @@ and this library adheres to Rust's notion of
|
|||
- `Zip321Error::TransparentMemo(usize)`
|
||||
- `Zip321Error::RecipientMissing(usize)`
|
||||
- `Zip321Error::ParseError(String)`
|
||||
- The api of `welding_rig::ScanningKey` has changed to accommodate batch
|
||||
decryption and to correctly handle scanning with the internal (change) keys
|
||||
derived from ZIP 316 UFVKs and UIVKs.
|
||||
- `welding_rig::scan_block` now uses batching for trial-decryption of
|
||||
transaction outputs.
|
||||
|
||||
|
||||
### Removed
|
||||
- `zcash_client_backend::data_api`:
|
||||
|
|
|
@ -4,93 +4,23 @@ use ff::PrimeField;
|
|||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
|
||||
use zcash_note_encryption::{ShieldedOutput, COMPACT_NOTE_SIZE};
|
||||
use zcash_note_encryption::batch;
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
consensus,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::{
|
||||
self,
|
||||
keys::DiversifiableFullViewingKey,
|
||||
note_encryption::{try_sapling_compact_note_decryption, SaplingDomain},
|
||||
Node, Note, Nullifier, PaymentAddress, SaplingIvk,
|
||||
keys::{DiversifiableFullViewingKey, Scope},
|
||||
note_encryption::SaplingDomain,
|
||||
Node, Note, Nullifier, NullifierDerivingKey, SaplingIvk,
|
||||
},
|
||||
transaction::components::sapling::CompactOutputDescription,
|
||||
zip32::{AccountId, ExtendedFullViewingKey},
|
||||
};
|
||||
|
||||
use crate::proto::compact_formats::{CompactBlock, CompactSaplingOutput};
|
||||
use crate::proto::compact_formats::CompactBlock;
|
||||
use crate::wallet::{WalletShieldedOutput, WalletShieldedSpend, WalletTx};
|
||||
|
||||
/// Scans a [`CompactSaplingOutput`] with a set of [`ScanningKey`]s.
|
||||
///
|
||||
/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this
|
||||
/// output belongs to any of the given [`ScanningKey`]s.
|
||||
///
|
||||
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented
|
||||
/// with this output's commitment.
|
||||
///
|
||||
/// [`ScanningKey`]: crate::welding_rig::ScanningKey
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn scan_output<P: consensus::Parameters, K: ScanningKey>(
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
index: usize,
|
||||
output: CompactSaplingOutput,
|
||||
vks: &[(&AccountId, &K)],
|
||||
spent_from_accounts: &HashSet<AccountId>,
|
||||
tree: &mut CommitmentTree<Node>,
|
||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
block_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
new_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
) -> Option<WalletShieldedOutput<K::Nf>> {
|
||||
let output = CompactOutputDescription::try_from(output).ok()?;
|
||||
|
||||
// Increment tree and witnesses
|
||||
let node = Node::new(output.cmu.to_repr());
|
||||
for witness in existing_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in block_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in new_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
tree.append(node).unwrap();
|
||||
|
||||
for (account, vk) in vks.iter() {
|
||||
let (note, to) = match vk.try_decryption(params, height, &output) {
|
||||
Some(ret) => ret,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// A note is marked as "change" if the account that received it
|
||||
// also spent notes in the same transaction. This will catch,
|
||||
// for instance:
|
||||
// - Change created by spending fractions of notes.
|
||||
// - Notes created by consolidation transactions.
|
||||
// - Notes sent from one account to itself.
|
||||
let is_change = spent_from_accounts.contains(account);
|
||||
|
||||
let witness = IncrementalWitness::from_tree(tree);
|
||||
let nf = vk.nf(¬e, &witness);
|
||||
|
||||
return Some(WalletShieldedOutput {
|
||||
index,
|
||||
cmu: output.cmu,
|
||||
ephemeral_key: output.ephemeral_key,
|
||||
account: **account,
|
||||
note,
|
||||
to,
|
||||
is_change,
|
||||
witness,
|
||||
nf,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// A key that can be used to perform trial decryption and nullifier
|
||||
/// computation for a Sapling [`CompactSaplingOutput`]
|
||||
///
|
||||
|
@ -105,47 +35,48 @@ fn scan_output<P: consensus::Parameters, K: ScanningKey>(
|
|||
/// [`CompactSaplingOutput`]: crate::proto::compact_formats::CompactSaplingOutput
|
||||
/// [`scan_block`]: crate::welding_rig::scan_block
|
||||
pub trait ScanningKey {
|
||||
/// The type of key that is used to decrypt Sapling outputs;
|
||||
type SaplingNk;
|
||||
|
||||
type SaplingKeys: IntoIterator<Item = (SaplingIvk, Self::SaplingNk)>;
|
||||
|
||||
/// The type of nullifier extracted when a note is successfully
|
||||
/// obtained by trial decryption.
|
||||
type Nf;
|
||||
|
||||
/// Attempts to decrypt a Sapling note and payment address
|
||||
/// from the specified ciphertext using this scanning key.
|
||||
fn try_decryption<
|
||||
P: consensus::Parameters,
|
||||
Output: ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>,
|
||||
>(
|
||||
&self,
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
output: &Output,
|
||||
) -> Option<(Note, PaymentAddress)>;
|
||||
/// Obtain the underlying Sapling incoming viewing key(s) for this scanning key.
|
||||
fn to_sapling_keys(&self) -> Self::SaplingKeys;
|
||||
|
||||
/// Produces the nullifier for the specified note and witness, if possible.
|
||||
///
|
||||
/// IVK-based implementations of this trait cannot successfully derive
|
||||
/// nullifiers, in which case `Self::Nf` should be set to the unit type
|
||||
/// and this function is a no-op.
|
||||
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf;
|
||||
fn sapling_nf(
|
||||
key: &Self::SaplingNk,
|
||||
note: &Note,
|
||||
witness: &IncrementalWitness<Node>,
|
||||
) -> Self::Nf;
|
||||
}
|
||||
|
||||
impl ScanningKey for DiversifiableFullViewingKey {
|
||||
type SaplingNk = NullifierDerivingKey;
|
||||
type SaplingKeys = [(SaplingIvk, Self::SaplingNk); 2];
|
||||
type Nf = sapling::Nullifier;
|
||||
|
||||
fn try_decryption<
|
||||
P: consensus::Parameters,
|
||||
Output: ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>,
|
||||
>(
|
||||
&self,
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
output: &Output,
|
||||
) -> Option<(Note, PaymentAddress)> {
|
||||
try_sapling_compact_note_decryption(params, height, &self.fvk().vk.ivk(), output)
|
||||
fn to_sapling_keys(&self) -> Self::SaplingKeys {
|
||||
[
|
||||
(self.to_ivk(Scope::External), self.to_nk(Scope::External)),
|
||||
(self.to_ivk(Scope::Internal), self.to_nk(Scope::Internal)),
|
||||
]
|
||||
}
|
||||
|
||||
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf {
|
||||
note.nf(&self.fvk().vk, witness.position() as u64)
|
||||
fn sapling_nf(
|
||||
key: &Self::SaplingNk,
|
||||
note: &Note,
|
||||
witness: &IncrementalWitness<Node>,
|
||||
) -> Self::Nf {
|
||||
note.nf(key, witness.position() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,22 +85,20 @@ impl ScanningKey for DiversifiableFullViewingKey {
|
|||
///
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
impl ScanningKey for ExtendedFullViewingKey {
|
||||
type Nf = Nullifier;
|
||||
type SaplingNk = NullifierDerivingKey;
|
||||
type SaplingKeys = [(SaplingIvk, Self::SaplingNk); 1];
|
||||
type Nf = sapling::Nullifier;
|
||||
|
||||
fn try_decryption<
|
||||
P: consensus::Parameters,
|
||||
Output: ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>,
|
||||
>(
|
||||
&self,
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
output: &Output,
|
||||
) -> Option<(Note, PaymentAddress)> {
|
||||
try_sapling_compact_note_decryption(params, height, &self.fvk.vk.ivk(), output)
|
||||
fn to_sapling_keys(&self) -> Self::SaplingKeys {
|
||||
[(self.fvk.vk.ivk(), self.fvk.vk.nk)]
|
||||
}
|
||||
|
||||
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf {
|
||||
note.nf(&self.fvk.vk, witness.position() as u64)
|
||||
fn sapling_nf(
|
||||
key: &Self::SaplingNk,
|
||||
note: &Note,
|
||||
witness: &IncrementalWitness<Node>,
|
||||
) -> Self::Nf {
|
||||
note.nf(key, witness.position() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,21 +107,15 @@ impl ScanningKey for ExtendedFullViewingKey {
|
|||
///
|
||||
/// [`SaplingIvk`]: zcash_primitives::sapling::SaplingIvk
|
||||
impl ScanningKey for SaplingIvk {
|
||||
type SaplingNk = ();
|
||||
type SaplingKeys = [(SaplingIvk, Self::SaplingNk); 1];
|
||||
type Nf = ();
|
||||
|
||||
fn try_decryption<
|
||||
P: consensus::Parameters,
|
||||
Output: ShieldedOutput<SaplingDomain<P>, COMPACT_NOTE_SIZE>,
|
||||
>(
|
||||
&self,
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
output: &Output,
|
||||
) -> Option<(Note, PaymentAddress)> {
|
||||
try_sapling_compact_note_decryption(params, height, self, output)
|
||||
fn to_sapling_keys(&self) -> Self::SaplingKeys {
|
||||
[(self.clone(), ())]
|
||||
}
|
||||
|
||||
fn nf(&self, _note: &Note, _witness: &IncrementalWitness<Node>) {}
|
||||
fn sapling_nf(_key: &Self::SaplingNk, _note: &Note, _witness: &IncrementalWitness<Node>) {}
|
||||
}
|
||||
|
||||
/// Scans a [`CompactBlock`] with a set of [`ScanningKey`]s.
|
||||
|
@ -283,28 +206,79 @@ pub fn scan_block<P: consensus::Parameters, K: ScanningKey>(
|
|||
})
|
||||
.collect();
|
||||
|
||||
for (idx, c_out) in tx.outputs.into_iter().enumerate() {
|
||||
let decoded = &tx
|
||||
.outputs
|
||||
.into_iter()
|
||||
.map(|output| {
|
||||
(
|
||||
SaplingDomain::for_height(params.clone(), block_height),
|
||||
CompactOutputDescription::try_from(output)
|
||||
.expect("Invalid output found in compact block decoding."),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vks = vks
|
||||
.iter()
|
||||
.flat_map(|(a, k)| {
|
||||
k.to_sapling_keys()
|
||||
.into_iter()
|
||||
.map(move |(ivk, nk)| (**a, ivk, nk))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let ivks = vks
|
||||
.iter()
|
||||
.map(|(_, ivk, _)| (*ivk).clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let decrypted = batch::try_compact_note_decryption(&ivks, decoded);
|
||||
|
||||
for (index, ((_, output), dec_output)) in decoded.iter().zip(decrypted).enumerate() {
|
||||
// Grab mutable references to new witnesses from previous outputs
|
||||
// in this transaction so that we can update them. Scoped so we
|
||||
// don't hold mutable references to shielded_outputs for too long.
|
||||
let mut new_witnesses: Vec<_> = shielded_outputs
|
||||
let new_witnesses: Vec<_> = shielded_outputs
|
||||
.iter_mut()
|
||||
.map(|output| &mut output.witness)
|
||||
.map(|out| &mut out.witness)
|
||||
.collect();
|
||||
|
||||
if let Some(output) = scan_output(
|
||||
params,
|
||||
block_height,
|
||||
idx,
|
||||
c_out,
|
||||
vks,
|
||||
&spent_from_accounts,
|
||||
tree,
|
||||
existing_witnesses,
|
||||
&mut block_witnesses,
|
||||
&mut new_witnesses,
|
||||
) {
|
||||
shielded_outputs.push(output);
|
||||
// Increment tree and witnesses
|
||||
let node = Node::new(output.cmu.to_repr());
|
||||
for witness in &mut *existing_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in &mut block_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
for witness in new_witnesses {
|
||||
witness.append(node).unwrap();
|
||||
}
|
||||
tree.append(node).unwrap();
|
||||
|
||||
if let Some(((note, to), ivk_idx)) = dec_output {
|
||||
// A note is marked as "change" if the account that received it
|
||||
// also spent notes in the same transaction. This will catch,
|
||||
// for instance:
|
||||
// - Change created by spending fractions of notes.
|
||||
// - Notes created by consolidation transactions.
|
||||
// - Notes sent from one account to itself.
|
||||
let (account, _, nk) = &vks[ivk_idx];
|
||||
let is_change = spent_from_accounts.contains(account);
|
||||
let witness = IncrementalWitness::from_tree(tree);
|
||||
let nf = K::sapling_nf(nk, ¬e, &witness);
|
||||
|
||||
shielded_outputs.push(WalletShieldedOutput {
|
||||
index,
|
||||
cmu: output.cmu,
|
||||
ephemeral_key: output.ephemeral_key.clone(),
|
||||
account: *account,
|
||||
note,
|
||||
to,
|
||||
is_change,
|
||||
witness,
|
||||
nf,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -862,7 +862,7 @@ mod tests {
|
|||
rng.fill_bytes(&mut cb.hash);
|
||||
cb.prevHash.extend_from_slice(&prev_hash.0);
|
||||
cb.vtx.push(ctx);
|
||||
(cb, note.nf(&dfvk.fvk().vk, 0))
|
||||
(cb, note.nf(&dfvk.fvk().vk.nk, 0))
|
||||
}
|
||||
|
||||
/// Create a fake CompactBlock at the given height, spending a single note from the
|
||||
|
|
|
@ -9,6 +9,14 @@ and this library adheres to Rust's notion of
|
|||
### Added
|
||||
- `zcash_primitives::sapling::keys::DiversifiableFullViewingKey`
|
||||
- `zcash_primitives::sapling::keys::Scope`
|
||||
- `zcash_primitives::sapling::NullifierDerivingKey`
|
||||
|
||||
### Changed
|
||||
- `zcash_primitives::sapling::ViewingKey` now stores `nk` as a
|
||||
`NullifierDerivingKey` instead of as a bare `jubjub::SubgroupPoint`.
|
||||
- The signature of `zcash_primitives::sapling::Note::nf` has changed to
|
||||
take just a `NullifierDerivingKey` (the only capability it actually required)
|
||||
rather than the full `ViewingKey` as its first argument.
|
||||
|
||||
## [0.7.0] - 2022-06-24
|
||||
### Changed
|
||||
|
|
|
@ -185,15 +185,19 @@ impl ProofGenerationKey {
|
|||
pub fn to_viewing_key(&self) -> ViewingKey {
|
||||
ViewingKey {
|
||||
ak: self.ak,
|
||||
nk: constants::PROOF_GENERATION_KEY_GENERATOR * self.nsk,
|
||||
nk: NullifierDerivingKey(constants::PROOF_GENERATION_KEY_GENERATOR * self.nsk),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A key used to derive the nullifier for a Sapling note.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct NullifierDerivingKey(pub(crate) jubjub::SubgroupPoint);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ViewingKey {
|
||||
pub ak: jubjub::SubgroupPoint,
|
||||
pub nk: jubjub::SubgroupPoint,
|
||||
pub nk: NullifierDerivingKey,
|
||||
}
|
||||
|
||||
impl ViewingKey {
|
||||
|
@ -209,7 +213,7 @@ impl ViewingKey {
|
|||
.personal(constants::CRH_IVK_PERSONALIZATION)
|
||||
.to_state()
|
||||
.update(&self.ak.to_bytes())
|
||||
.update(&self.nk.to_bytes())
|
||||
.update(&self.nk.0.to_bytes())
|
||||
.finalize()
|
||||
.as_bytes(),
|
||||
);
|
||||
|
@ -457,9 +461,9 @@ impl Note {
|
|||
(constants::NOTE_COMMITMENT_RANDOMNESS_GENERATOR * self.rcm()) + hash_of_contents
|
||||
}
|
||||
|
||||
/// Computes the nullifier given the viewing key and
|
||||
/// Computes the nullifier given the nullifier deriving key and
|
||||
/// note position
|
||||
pub fn nf(&self, viewing_key: &ViewingKey, position: u64) -> Nullifier {
|
||||
pub fn nf(&self, nk: &NullifierDerivingKey, position: u64) -> Nullifier {
|
||||
// Compute rho = cm + position.G
|
||||
let rho = self.cm_full_point()
|
||||
+ (constants::NULLIFIER_POSITION_GENERATOR * jubjub::Fr::from(position));
|
||||
|
@ -470,7 +474,7 @@ impl Note {
|
|||
.hash_length(32)
|
||||
.personal(constants::PRF_NF_PERSONALIZATION)
|
||||
.to_state()
|
||||
.update(&viewing_key.nk.to_bytes())
|
||||
.update(&nk.0.to_bytes())
|
||||
.update(&rho.to_bytes())
|
||||
.finalize()
|
||||
.as_bytes(),
|
||||
|
|
|
@ -16,7 +16,7 @@ use ff::PrimeField;
|
|||
use group::{Group, GroupEncoding};
|
||||
use subtle::CtOption;
|
||||
|
||||
use super::{PaymentAddress, ProofGenerationKey, SaplingIvk, ViewingKey};
|
||||
use super::{NullifierDerivingKey, PaymentAddress, ProofGenerationKey, SaplingIvk, ViewingKey};
|
||||
|
||||
/// A Sapling expanded spending key
|
||||
#[derive(Clone)]
|
||||
|
@ -104,7 +104,7 @@ impl FullViewingKey {
|
|||
FullViewingKey {
|
||||
vk: ViewingKey {
|
||||
ak: SPENDING_KEY_GENERATOR * expsk.ask,
|
||||
nk: PROOF_GENERATION_KEY_GENERATOR * expsk.nsk,
|
||||
nk: NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * expsk.nsk),
|
||||
},
|
||||
ovk: expsk.ovk,
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ impl FullViewingKey {
|
|||
));
|
||||
}
|
||||
let ak = ak.unwrap();
|
||||
let nk = nk.unwrap();
|
||||
let nk = NullifierDerivingKey(nk.unwrap());
|
||||
|
||||
let mut ovk = [0u8; 32];
|
||||
reader.read_exact(&mut ovk)?;
|
||||
|
@ -147,7 +147,7 @@ impl FullViewingKey {
|
|||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
writer.write_all(&self.vk.ak.to_bytes())?;
|
||||
writer.write_all(&self.vk.nk.to_bytes())?;
|
||||
writer.write_all(&self.vk.nk.0.to_bytes())?;
|
||||
writer.write_all(&self.ovk.0)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -232,11 +232,21 @@ impl DiversifiableFullViewingKey {
|
|||
Self { fvk, dk }
|
||||
}
|
||||
|
||||
/// Exposes the [`FullViewingKey`] component of this diversifiable full viewing key.
|
||||
/// Exposes the external [`FullViewingKey`] component of this diversifiable full viewing key.
|
||||
pub fn fvk(&self) -> &FullViewingKey {
|
||||
&self.fvk
|
||||
}
|
||||
|
||||
/// Derives a nullifier-deriving key for the provided scope.
|
||||
///
|
||||
/// This API is provided so that nullifiers for change notes can be correctly computed.
|
||||
pub fn to_nk(&self, scope: Scope) -> NullifierDerivingKey {
|
||||
match scope {
|
||||
Scope::External => self.fvk.vk.nk,
|
||||
Scope::Internal => self.derive_internal().fvk.vk.nk,
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives an incoming viewing key corresponding to this full viewing key.
|
||||
pub fn to_ivk(&self, scope: Scope) -> SaplingIvk {
|
||||
match scope {
|
||||
|
|
|
@ -1390,10 +1390,11 @@ mod tests {
|
|||
)],
|
||||
)[..]
|
||||
{
|
||||
[Some((decrypted_note, decrypted_to, decrypted_memo))] => {
|
||||
[Some(((decrypted_note, decrypted_to, decrypted_memo), i))] => {
|
||||
assert_eq!(decrypted_note, ¬e);
|
||||
assert_eq!(decrypted_to, &to);
|
||||
assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
|
||||
assert_eq!(*i, 0);
|
||||
}
|
||||
_ => panic!("Note decryption failed"),
|
||||
}
|
||||
|
@ -1406,9 +1407,10 @@ mod tests {
|
|||
)],
|
||||
)[..]
|
||||
{
|
||||
[Some((decrypted_note, decrypted_to))] => {
|
||||
[Some(((decrypted_note, decrypted_to), i))] => {
|
||||
assert_eq!(decrypted_note, ¬e);
|
||||
assert_eq!(decrypted_to, &to);
|
||||
assert_eq!(*i, 0);
|
||||
}
|
||||
_ => panic!("Note decryption failed"),
|
||||
}
|
||||
|
@ -1450,22 +1452,22 @@ mod tests {
|
|||
})
|
||||
.collect();
|
||||
|
||||
let res = batch::try_note_decryption(&[invalid_ivk.clone(), valid_ivk.clone()], &outputs);
|
||||
assert_eq!(res.len(), 20);
|
||||
// The batched trial decryptions with invalid_ivk failed.
|
||||
assert_eq!(&res[..10], &vec![None; 10][..]);
|
||||
for (result, (_, output)) in res[10..].iter().zip(outputs.iter()) {
|
||||
// Confirm that the outputs should indeed have failed with invalid_ivk
|
||||
assert_eq!(
|
||||
try_sapling_note_decryption(&TEST_NETWORK, height, &invalid_ivk, output),
|
||||
None
|
||||
);
|
||||
// Check that batched trial decryptions with invalid_ivk fails.
|
||||
let res = batch::try_note_decryption(&[invalid_ivk.clone()], &outputs);
|
||||
assert_eq!(res.len(), 10);
|
||||
assert_eq!(&res[..], &vec![None; 10][..]);
|
||||
|
||||
// Check that batched trial decryptions with valid_ivk succeeds.
|
||||
let res = batch::try_note_decryption(&[invalid_ivk, valid_ivk.clone()], &outputs);
|
||||
assert_eq!(res.len(), 10);
|
||||
for (result, (_, output)) in res.iter().zip(outputs.iter()) {
|
||||
// Confirm the successful batched trial decryptions gave the same result.
|
||||
// In all cases, the index of the valid ivk is returned.
|
||||
assert!(result.is_some());
|
||||
assert_eq!(
|
||||
result,
|
||||
&try_sapling_note_decryption(&TEST_NETWORK, height, &valid_ivk, output)
|
||||
.map(|r| (r, 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -356,7 +356,7 @@ impl<P: consensus::Parameters> SaplingBuilder<P> {
|
|||
let proof_generation_key = spend.extsk.expsk.proof_generation_key();
|
||||
|
||||
let nullifier = spend.note.nf(
|
||||
&proof_generation_key.to_viewing_key(),
|
||||
&proof_generation_key.to_viewing_key().nk,
|
||||
spend.merkle_path.position,
|
||||
);
|
||||
|
||||
|
|
|
@ -18,7 +18,10 @@ use std::io::{self, Read, Write};
|
|||
|
||||
use crate::{
|
||||
keys::{prf_expand, prf_expand_vec, OutgoingViewingKey},
|
||||
sapling::keys::{ExpandedSpendingKey, FullViewingKey},
|
||||
sapling::{
|
||||
keys::{ExpandedSpendingKey, FullViewingKey},
|
||||
NullifierDerivingKey,
|
||||
},
|
||||
};
|
||||
|
||||
pub const ZIP32_SAPLING_MASTER_PERSONALIZATION: &[u8; 16] = b"ZcashIP32Sapling";
|
||||
|
@ -281,7 +284,7 @@ pub fn sapling_derive_internal_fvk(
|
|||
let r = prf_expand(i.as_bytes(), &[0x18]);
|
||||
let r = r.as_bytes();
|
||||
// PROOF_GENERATION_KEY_GENERATOR = \mathcal{H}^Sapling
|
||||
let nk_internal = PROOF_GENERATION_KEY_GENERATOR * i_nsk + fvk.vk.nk;
|
||||
let nk_internal = NullifierDerivingKey(PROOF_GENERATION_KEY_GENERATOR * i_nsk + fvk.vk.nk.0);
|
||||
let dk_internal = DiversifierKey(r[..32].try_into().unwrap());
|
||||
let ovk_internal = OutgoingViewingKey(r[32..].try_into().unwrap());
|
||||
|
||||
|
@ -583,7 +586,9 @@ impl ExtendedFullViewingKey {
|
|||
let i_ask = jubjub::Fr::from_bytes_wide(prf_expand(i_l, &[0x13]).as_array());
|
||||
let i_nsk = jubjub::Fr::from_bytes_wide(prf_expand(i_l, &[0x14]).as_array());
|
||||
let ak = (SPENDING_KEY_GENERATOR * i_ask) + self.fvk.vk.ak;
|
||||
let nk = (PROOF_GENERATION_KEY_GENERATOR * i_nsk) + self.fvk.vk.nk;
|
||||
let nk = NullifierDerivingKey(
|
||||
(PROOF_GENERATION_KEY_GENERATOR * i_nsk) + self.fvk.vk.nk.0,
|
||||
);
|
||||
|
||||
FullViewingKey {
|
||||
vk: ViewingKey { ak, nk },
|
||||
|
@ -1545,7 +1550,7 @@ mod tests {
|
|||
|
||||
for (xfvk, tv) in xfvks.iter().zip(test_vectors.iter()) {
|
||||
assert_eq!(xfvk.fvk.vk.ak.to_bytes(), tv.ak);
|
||||
assert_eq!(xfvk.fvk.vk.nk.to_bytes(), tv.nk);
|
||||
assert_eq!(xfvk.fvk.vk.nk.0.to_bytes(), tv.nk);
|
||||
|
||||
assert_eq!(xfvk.fvk.ovk.0, tv.ovk);
|
||||
assert_eq!(xfvk.dk.0, tv.dk);
|
||||
|
@ -1589,7 +1594,7 @@ mod tests {
|
|||
|
||||
let internal_xfvk = xfvk.derive_internal();
|
||||
assert_eq!(internal_xfvk.fvk.vk.ak.to_bytes(), tv.ak);
|
||||
assert_eq!(internal_xfvk.fvk.vk.nk.to_bytes(), tv.internal_nk);
|
||||
assert_eq!(internal_xfvk.fvk.vk.nk.0.to_bytes(), tv.internal_nk);
|
||||
|
||||
assert_eq!(internal_xfvk.fvk.ovk.0, tv.internal_ovk);
|
||||
assert_eq!(internal_xfvk.dk.0, tv.internal_dk);
|
||||
|
|
|
@ -611,7 +611,7 @@ fn test_input_circuit_with_bls12_381() {
|
|||
}
|
||||
}
|
||||
|
||||
let expected_nf = note.nf(&viewing_key, position);
|
||||
let expected_nf = note.nf(&viewing_key.nk, position);
|
||||
let expected_nf = multipack::bytes_to_bits_le(&expected_nf.0);
|
||||
let expected_nf = multipack::compute_multipacking(&expected_nf);
|
||||
assert_eq!(expected_nf.len(), 2);
|
||||
|
@ -789,7 +789,7 @@ fn test_input_circuit_with_bls12_381_external_test_vectors() {
|
|||
}
|
||||
}
|
||||
|
||||
let expected_nf = note.nf(&viewing_key, position);
|
||||
let expected_nf = note.nf(&viewing_key.nk, position);
|
||||
let expected_nf = multipack::bytes_to_bits_le(&expected_nf.0);
|
||||
let expected_nf = multipack::compute_multipacking(&expected_nf);
|
||||
assert_eq!(expected_nf.len(), 2);
|
||||
|
|
|
@ -96,7 +96,7 @@ impl SaplingProvingContext {
|
|||
rseed,
|
||||
};
|
||||
|
||||
let nullifier = note.nf(&viewing_key, merkle_path.position);
|
||||
let nullifier = note.nf(&viewing_key.nk, merkle_path.position);
|
||||
|
||||
// We now have the full witness for our circuit
|
||||
let instance = Spend {
|
||||
|
|
Loading…
Reference in New Issue