Merge pull request #353 from nuttycom/data_api/transactional_types
Make data access API write calls atomic.
This commit is contained in:
commit
3a8e72936f
|
@ -9,7 +9,7 @@ use zcash_primitives::{
|
||||||
consensus::BlockHeight,
|
consensus::BlockHeight,
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
note_encryption::Memo,
|
note_encryption::Memo,
|
||||||
primitives::{Note, Nullifier, PaymentAddress},
|
primitives::{Nullifier, PaymentAddress},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{components::Amount, Transaction, TxId},
|
||||||
zip32::ExtendedFullViewingKey,
|
zip32::ExtendedFullViewingKey,
|
||||||
|
@ -20,7 +20,7 @@ use crate::{
|
||||||
data_api::wallet::ANCHOR_OFFSET,
|
data_api::wallet::ANCHOR_OFFSET,
|
||||||
decrypt::DecryptedOutput,
|
decrypt::DecryptedOutput,
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
wallet::{AccountId, SpendableNote, WalletShieldedOutput, WalletTx},
|
wallet::{AccountId, SpendableNote, WalletTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod chain;
|
pub mod chain;
|
||||||
|
@ -177,26 +177,47 @@ pub trait WalletRead {
|
||||||
) -> Result<Vec<SpendableNote>, Self::Error>;
|
) -> Result<Vec<SpendableNote>, Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The subset of information that is relevant to this wallet that has been
|
||||||
|
/// decrypted and extracted from a [CompactBlock].
|
||||||
|
pub struct PrunedBlock<'a> {
|
||||||
|
pub block_height: BlockHeight,
|
||||||
|
pub block_hash: BlockHash,
|
||||||
|
pub block_time: u32,
|
||||||
|
pub commitment_tree: &'a CommitmentTree<Node>,
|
||||||
|
pub transactions: &'a Vec<WalletTx<Nullifier>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReceivedTransaction<'a> {
|
||||||
|
pub tx: &'a Transaction,
|
||||||
|
pub outputs: &'a Vec<DecryptedOutput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SentTransaction<'a> {
|
||||||
|
pub tx: &'a Transaction,
|
||||||
|
pub created: time::OffsetDateTime,
|
||||||
|
pub output_index: usize,
|
||||||
|
pub account: AccountId,
|
||||||
|
pub recipient_address: &'a RecipientAddress,
|
||||||
|
pub value: Amount,
|
||||||
|
pub memo: Option<Memo>,
|
||||||
|
}
|
||||||
|
|
||||||
/// This trait encapsulates the write capabilities required to update stored
|
/// This trait encapsulates the write capabilities required to update stored
|
||||||
/// wallet data.
|
/// wallet data.
|
||||||
pub trait WalletWrite: WalletRead {
|
pub trait WalletWrite: WalletRead {
|
||||||
/// Perform one or more write operations of this trait transactionally.
|
#[allow(clippy::type_complexity)]
|
||||||
/// Implementations of this method must ensure that all mutations to the
|
fn advance_by_block(
|
||||||
/// state of the data store made by the provided closure must be performed
|
|
||||||
/// atomically and modifications to state must be automatically rolled back
|
|
||||||
/// if the provided closure returns an error.
|
|
||||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, Self::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> Result<A, Self::Error>;
|
|
||||||
|
|
||||||
/// Add the data for a block to the data store.
|
|
||||||
fn insert_block(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
block_height: BlockHeight,
|
block: &PrunedBlock,
|
||||||
block_hash: BlockHash,
|
updated_witnesses: &[(Self::NoteRef, IncrementalWitness<Node>)],
|
||||||
block_time: u32,
|
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error>;
|
||||||
commitment_tree: &CommitmentTree<Node>,
|
|
||||||
) -> Result<(), Self::Error>;
|
fn store_received_tx(
|
||||||
|
&mut self,
|
||||||
|
received_tx: &ReceivedTransaction,
|
||||||
|
) -> Result<Self::TxRef, Self::Error>;
|
||||||
|
|
||||||
|
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error>;
|
||||||
|
|
||||||
/// Rewinds the wallet database to the specified height.
|
/// Rewinds the wallet database to the specified height.
|
||||||
///
|
///
|
||||||
|
@ -212,80 +233,6 @@ pub trait WalletWrite: WalletRead {
|
||||||
///
|
///
|
||||||
/// There may be restrictions on how far it is possible to rewind.
|
/// There may be restrictions on how far it is possible to rewind.
|
||||||
fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>;
|
fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
/// Add wallet-relevant metadata for a specific transaction to the data
|
|
||||||
/// store.
|
|
||||||
fn put_tx_meta(
|
|
||||||
&mut self,
|
|
||||||
tx: &WalletTx,
|
|
||||||
height: BlockHeight,
|
|
||||||
) -> Result<Self::TxRef, Self::Error>;
|
|
||||||
|
|
||||||
/// Add a full transaction contents to the data store.
|
|
||||||
fn put_tx_data(
|
|
||||||
&mut self,
|
|
||||||
tx: &Transaction,
|
|
||||||
created_at: Option<time::OffsetDateTime>,
|
|
||||||
) -> Result<Self::TxRef, Self::Error>;
|
|
||||||
|
|
||||||
/// Mark the specified transaction as spent and record the nullifier.
|
|
||||||
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &Nullifier) -> Result<(), Self::Error>;
|
|
||||||
|
|
||||||
/// Record a note as having been received, along with its nullifier and the transaction
|
|
||||||
/// within which the note was created.
|
|
||||||
///
|
|
||||||
/// Implementations of this method must be exclusively additive with respect to stored
|
|
||||||
/// data; passing `None` for the nullifier should not be interpreted as deleting nullifier
|
|
||||||
/// information from the underlying store.
|
|
||||||
///
|
|
||||||
/// Implementations of this method must ensure that attempting to record the same note
|
|
||||||
/// with a different nullifier to that already stored will return an error.
|
|
||||||
fn put_received_note<T: ShieldedOutput>(
|
|
||||||
&mut self,
|
|
||||||
output: &T,
|
|
||||||
nf: &Option<Nullifier>,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
) -> Result<Self::NoteRef, Self::Error>;
|
|
||||||
|
|
||||||
/// Add the incremental witness for the specified note to the database.
|
|
||||||
fn insert_witness(
|
|
||||||
&mut self,
|
|
||||||
note_id: Self::NoteRef,
|
|
||||||
witness: &IncrementalWitness<Node>,
|
|
||||||
height: BlockHeight,
|
|
||||||
) -> Result<(), Self::Error>;
|
|
||||||
|
|
||||||
/// Remove all incremental witness data before the specified block height.
|
|
||||||
// TODO: this is a backend-specific optimization that probably shouldn't be part of
|
|
||||||
// the public API
|
|
||||||
fn prune_witnesses(&mut self, from_height: BlockHeight) -> Result<(), Self::Error>;
|
|
||||||
|
|
||||||
/// Remove the spent marker from any received notes that had been spent in a
|
|
||||||
/// transaction constructed by the wallet, but which transaction had not been mined
|
|
||||||
/// by the specified block height.
|
|
||||||
// TODO: this is a backend-specific optimization that probably shouldn't be part of
|
|
||||||
// the public API
|
|
||||||
fn update_expired_notes(&mut self, from_height: BlockHeight) -> Result<(), Self::Error>;
|
|
||||||
|
|
||||||
/// Add the decrypted contents of a sent note to the database if it does not exist;
|
|
||||||
/// otherwise, update the note. This is useful in the case of a wallet restore where
|
|
||||||
/// the send of the note is being discovered via trial decryption.
|
|
||||||
fn put_sent_note(
|
|
||||||
&mut self,
|
|
||||||
output: &DecryptedOutput,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
) -> Result<(), Self::Error>;
|
|
||||||
|
|
||||||
/// Add the decrypted contents of a sent note to the database.
|
|
||||||
fn insert_sent_note(
|
|
||||||
&mut self,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
output_index: usize,
|
|
||||||
account: AccountId,
|
|
||||||
to: &RecipientAddress,
|
|
||||||
value: Amount,
|
|
||||||
memo: Option<Memo>,
|
|
||||||
) -> Result<(), Self::Error>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait provides sequential access to raw blockchain data via a callback-oriented
|
/// This trait provides sequential access to raw blockchain data via a callback-oriented
|
||||||
|
@ -305,62 +252,6 @@ pub trait BlockSource {
|
||||||
F: FnMut(CompactBlock) -> Result<(), Self::Error>;
|
F: FnMut(CompactBlock) -> Result<(), Self::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This trait provides a generalization over shielded output representations
|
|
||||||
/// that allows a wallet to avoid coupling to a specific one.
|
|
||||||
// TODO: it'd probably be better not to unify the definitions of
|
|
||||||
// `WalletShieldedOutput` and `DecryptedOutput` via a compositional
|
|
||||||
// approach, if possible.
|
|
||||||
pub trait ShieldedOutput {
|
|
||||||
fn index(&self) -> usize;
|
|
||||||
fn account(&self) -> AccountId;
|
|
||||||
fn to(&self) -> &PaymentAddress;
|
|
||||||
fn note(&self) -> &Note;
|
|
||||||
fn memo(&self) -> Option<&Memo>;
|
|
||||||
fn is_change(&self) -> Option<bool>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShieldedOutput for WalletShieldedOutput {
|
|
||||||
fn index(&self) -> usize {
|
|
||||||
self.index
|
|
||||||
}
|
|
||||||
fn account(&self) -> AccountId {
|
|
||||||
self.account
|
|
||||||
}
|
|
||||||
fn to(&self) -> &PaymentAddress {
|
|
||||||
&self.to
|
|
||||||
}
|
|
||||||
fn note(&self) -> &Note {
|
|
||||||
&self.note
|
|
||||||
}
|
|
||||||
fn memo(&self) -> Option<&Memo> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn is_change(&self) -> Option<bool> {
|
|
||||||
Some(self.is_change)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShieldedOutput for DecryptedOutput {
|
|
||||||
fn index(&self) -> usize {
|
|
||||||
self.index
|
|
||||||
}
|
|
||||||
fn account(&self) -> AccountId {
|
|
||||||
self.account
|
|
||||||
}
|
|
||||||
fn to(&self) -> &PaymentAddress {
|
|
||||||
&self.to
|
|
||||||
}
|
|
||||||
fn note(&self) -> &Note {
|
|
||||||
&self.note
|
|
||||||
}
|
|
||||||
fn memo(&self) -> Option<&Memo> {
|
|
||||||
Some(&self.memo)
|
|
||||||
}
|
|
||||||
fn is_change(&self) -> Option<bool> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "test-dependencies")]
|
#[cfg(feature = "test-dependencies")]
|
||||||
pub mod testing {
|
pub mod testing {
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
@ -369,21 +260,21 @@ pub mod testing {
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::BlockHeight,
|
consensus::BlockHeight,
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
note_encryption::Memo,
|
|
||||||
primitives::{Nullifier, PaymentAddress},
|
primitives::{Nullifier, PaymentAddress},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{components::Amount, TxId},
|
||||||
zip32::ExtendedFullViewingKey,
|
zip32::ExtendedFullViewingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
address::RecipientAddress,
|
|
||||||
decrypt::DecryptedOutput,
|
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
wallet::{AccountId, SpendableNote, WalletTx},
|
wallet::{AccountId, SpendableNote},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{error::Error, BlockSource, ShieldedOutput, WalletRead, WalletWrite};
|
use super::{
|
||||||
|
error::Error, BlockSource, PrunedBlock, ReceivedTransaction, SentTransaction, WalletRead,
|
||||||
|
WalletWrite,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct MockBlockSource {}
|
pub struct MockBlockSource {}
|
||||||
|
|
||||||
|
@ -493,91 +384,31 @@ pub mod testing {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletWrite for MockWalletDB {
|
impl WalletWrite for MockWalletDB {
|
||||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, Self::Error>
|
#[allow(clippy::type_complexity)]
|
||||||
where
|
fn advance_by_block(
|
||||||
F: FnOnce(&mut Self) -> Result<A, Self::Error>,
|
&mut self,
|
||||||
{
|
_block: &PrunedBlock,
|
||||||
f(self)
|
_updated_witnesses: &[(Self::NoteRef, IncrementalWitness<Node>)],
|
||||||
|
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error> {
|
||||||
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_block(
|
fn store_received_tx(
|
||||||
&mut self,
|
&mut self,
|
||||||
_block_height: BlockHeight,
|
_received_tx: &ReceivedTransaction,
|
||||||
_block_hash: BlockHash,
|
) -> Result<Self::TxRef, Self::Error> {
|
||||||
_block_time: u32,
|
Ok(TxId([0u8; 32]))
|
||||||
_commitment_tree: &CommitmentTree<Node>,
|
}
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
fn store_sent_tx(
|
||||||
|
&mut self,
|
||||||
|
_sent_tx: &SentTransaction,
|
||||||
|
) -> Result<Self::TxRef, Self::Error> {
|
||||||
|
Ok(TxId([0u8; 32]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewind_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> {
|
fn rewind_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put_tx_meta(
|
|
||||||
&mut self,
|
|
||||||
_tx: &WalletTx,
|
|
||||||
_height: BlockHeight,
|
|
||||||
) -> Result<Self::TxRef, Self::Error> {
|
|
||||||
Ok(TxId([0u8; 32]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_tx_data(
|
|
||||||
&mut self,
|
|
||||||
_tx: &Transaction,
|
|
||||||
_created_at: Option<time::OffsetDateTime>,
|
|
||||||
) -> Result<Self::TxRef, Self::Error> {
|
|
||||||
Ok(TxId([0u8; 32]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_spent(&mut self, _tx_ref: Self::TxRef, _nf: &Nullifier) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_received_note<T: ShieldedOutput>(
|
|
||||||
&mut self,
|
|
||||||
_output: &T,
|
|
||||||
_nf: &Option<Nullifier>,
|
|
||||||
_tx_ref: Self::TxRef,
|
|
||||||
) -> Result<Self::NoteRef, Self::Error> {
|
|
||||||
Ok(0u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_witness(
|
|
||||||
&mut self,
|
|
||||||
_note_id: Self::NoteRef,
|
|
||||||
_witness: &IncrementalWitness<Node>,
|
|
||||||
_height: BlockHeight,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prune_witnesses(&mut self, _from_height: BlockHeight) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_expired_notes(&mut self, _from_height: BlockHeight) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_sent_note(
|
|
||||||
&mut self,
|
|
||||||
_output: &DecryptedOutput,
|
|
||||||
_tx_ref: Self::TxRef,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_sent_note(
|
|
||||||
&mut self,
|
|
||||||
_tx_ref: Self::TxRef,
|
|
||||||
_output_index: usize,
|
|
||||||
_account: AccountId,
|
|
||||||
_to: &RecipientAddress,
|
|
||||||
_value: Amount,
|
|
||||||
_memo: Option<Memo>,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,15 +95,17 @@ use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||||
merkle_tree::CommitmentTree,
|
merkle_tree::CommitmentTree,
|
||||||
|
primitives::Nullifier,
|
||||||
|
zip32::ExtendedFullViewingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
data_api::{
|
data_api::{
|
||||||
error::{ChainInvalid, Error},
|
error::{ChainInvalid, Error},
|
||||||
BlockSource, WalletWrite,
|
BlockSource, PrunedBlock, WalletWrite,
|
||||||
},
|
},
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
wallet::WalletTx,
|
wallet::{AccountId, WalletTx},
|
||||||
welding_rig::scan_block,
|
welding_rig::scan_block,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -261,10 +263,7 @@ where
|
||||||
|
|
||||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||||
let extfvks = data.get_extended_full_viewing_keys()?;
|
let extfvks = data.get_extended_full_viewing_keys()?;
|
||||||
let ivks: Vec<_> = extfvks
|
let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect();
|
||||||
.iter()
|
|
||||||
.map(|(a, extfvk)| (*a, extfvk.fvk.vk.ivk()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Get the most recent CommitmentTree
|
// Get the most recent CommitmentTree
|
||||||
let mut tree = data
|
let mut tree = data
|
||||||
|
@ -279,24 +278,24 @@ where
|
||||||
|
|
||||||
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
||||||
let current_height = block.height();
|
let current_height = block.height();
|
||||||
|
|
||||||
// Scanned blocks MUST be height-sequential.
|
// Scanned blocks MUST be height-sequential.
|
||||||
if current_height != (last_height + 1) {
|
if current_height != (last_height + 1) {
|
||||||
return Err(
|
return Err(
|
||||||
ChainInvalid::block_height_discontinuity(last_height + 1, current_height).into(),
|
ChainInvalid::block_height_discontinuity(last_height + 1, current_height).into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
last_height = current_height;
|
|
||||||
|
|
||||||
let block_hash = BlockHash::from_slice(&block.hash);
|
let block_hash = BlockHash::from_slice(&block.hash);
|
||||||
let block_time = block.time;
|
let block_time = block.time;
|
||||||
|
|
||||||
let txs: Vec<WalletTx> = {
|
let txs: Vec<WalletTx<Nullifier>> = {
|
||||||
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
|
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
|
||||||
|
|
||||||
scan_block(
|
scan_block(
|
||||||
params,
|
params,
|
||||||
block,
|
block,
|
||||||
&ivks,
|
&extfvks,
|
||||||
&nullifiers,
|
&nullifiers,
|
||||||
&mut tree,
|
&mut tree,
|
||||||
&mut witness_refs[..],
|
&mut witness_refs[..],
|
||||||
|
@ -309,7 +308,7 @@ where
|
||||||
let cur_root = tree.root();
|
let cur_root = tree.root();
|
||||||
for row in &witnesses {
|
for row in &witnesses {
|
||||||
if row.1.root() != cur_root {
|
if row.1.root() != cur_root {
|
||||||
return Err(Error::InvalidWitnessAnchor(row.0, last_height).into());
|
return Err(Error::InvalidWitnessAnchor(row.0, current_height).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for tx in &txs {
|
for tx in &txs {
|
||||||
|
@ -318,7 +317,7 @@ where
|
||||||
return Err(Error::InvalidNewWitnessAnchor(
|
return Err(Error::InvalidNewWitnessAnchor(
|
||||||
output.index,
|
output.index,
|
||||||
tx.txid,
|
tx.txid,
|
||||||
last_height,
|
current_height,
|
||||||
output.witness.root(),
|
output.witness.root(),
|
||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
|
@ -327,53 +326,32 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// database updates for each block are transactional
|
let new_witnesses = data.advance_by_block(
|
||||||
data.transactionally(|up| {
|
&(PrunedBlock {
|
||||||
// Insert the block into the database.
|
block_height: current_height,
|
||||||
up.insert_block(current_height, block_hash, block_time, &tree)?;
|
block_hash,
|
||||||
|
block_time,
|
||||||
|
commitment_tree: &tree,
|
||||||
|
transactions: &txs,
|
||||||
|
}),
|
||||||
|
&witnesses,
|
||||||
|
)?;
|
||||||
|
|
||||||
for tx in txs {
|
let spent_nf: Vec<Nullifier> = txs
|
||||||
let tx_row = up.put_tx_meta(&tx, current_height)?;
|
.iter()
|
||||||
|
.flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf))
|
||||||
|
.collect();
|
||||||
|
nullifiers.retain(|(_, nf)| !spent_nf.contains(nf));
|
||||||
|
nullifiers.extend(
|
||||||
|
txs.iter()
|
||||||
|
.flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))),
|
||||||
|
);
|
||||||
|
|
||||||
// Mark notes as spent and remove them from the scanning cache
|
witnesses.extend(new_witnesses);
|
||||||
for spend in &tx.shielded_spends {
|
|
||||||
up.mark_spent(tx_row, &spend.nf)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove spent nullifiers from the nullifier set
|
last_height = current_height;
|
||||||
nullifiers
|
|
||||||
.retain(|(_, nf)| !tx.shielded_spends.iter().any(|spend| &spend.nf == nf));
|
|
||||||
|
|
||||||
for output in tx.shielded_outputs {
|
Ok(())
|
||||||
if let Some(extfvk) = &extfvks.get(&output.account) {
|
|
||||||
let nf = output
|
|
||||||
.note
|
|
||||||
.nf(&extfvk.fvk.vk, output.witness.position() as u64);
|
|
||||||
|
|
||||||
let received_note_id = up.put_received_note(&output, &Some(nf), tx_row)?;
|
|
||||||
|
|
||||||
// Save witness for note.
|
|
||||||
witnesses.push((received_note_id, output.witness));
|
|
||||||
|
|
||||||
// Cache nullifier for note (to detect subsequent spends in this scan).
|
|
||||||
nullifiers.push((output.account, nf));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert current witnesses into the database.
|
|
||||||
for (received_note_id, witness) in witnesses.iter() {
|
|
||||||
up.insert_witness(*received_note_id, witness, last_height)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prune the stored witnesses (we only expect rollbacks of at most 100 blocks).
|
|
||||||
up.prune_witnesses(last_height - 100)?;
|
|
||||||
|
|
||||||
// Update now-expired transactions that didn't get mined.
|
|
||||||
up.update_expired_notes(last_height)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -15,7 +15,7 @@ use zcash_primitives::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
address::RecipientAddress,
|
address::RecipientAddress,
|
||||||
data_api::{error::Error, WalletWrite},
|
data_api::{error::Error, ReceivedTransaction, SentTransaction, WalletWrite},
|
||||||
decrypt_transaction,
|
decrypt_transaction,
|
||||||
wallet::{AccountId, OvkPolicy},
|
wallet::{AccountId, OvkPolicy},
|
||||||
};
|
};
|
||||||
|
@ -51,20 +51,12 @@ where
|
||||||
if outputs.is_empty() {
|
if outputs.is_empty() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
// Update the database atomically, to ensure the result is internally consistent.
|
data.store_received_tx(&ReceivedTransaction {
|
||||||
data.transactionally(|up| {
|
tx,
|
||||||
let tx_ref = up.put_tx_data(tx, None)?;
|
outputs: &outputs,
|
||||||
|
})?;
|
||||||
|
|
||||||
for output in outputs {
|
Ok(())
|
||||||
if output.outgoing {
|
|
||||||
up.put_sent_note(&output, tx_ref)?;
|
|
||||||
} else {
|
|
||||||
up.put_received_note(&output, &None, tx_ref)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,26 +235,13 @@ where
|
||||||
None => panic!("Output 0 should exist in the transaction"),
|
None => panic!("Output 0 should exist in the transaction"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the database atomically, to ensure the result is internally consistent.
|
wallet_db.store_sent_tx(&SentTransaction {
|
||||||
wallet_db.transactionally(|up| {
|
tx: &tx,
|
||||||
let created = time::OffsetDateTime::now_utc();
|
created: time::OffsetDateTime::now_utc(),
|
||||||
let tx_ref = up.put_tx_data(&tx, Some(created))?;
|
output_index: output_index as usize,
|
||||||
|
account,
|
||||||
// Mark notes as spent.
|
recipient_address: to,
|
||||||
//
|
value,
|
||||||
// This locks the notes so they aren't selected again by a subsequent call to
|
memo,
|
||||||
// create_spend_to_address() before this transaction has been mined (at which point the notes
|
|
||||||
// get re-marked as spent).
|
|
||||||
//
|
|
||||||
// Assumes that create_spend_to_address() will never be called in parallel, which is a
|
|
||||||
// reasonable assumption for a light client such as a mobile phone.
|
|
||||||
for spend in &tx.shielded_spends {
|
|
||||||
up.mark_spent(tx_ref, &spend.nullifier)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
up.insert_sent_note(tx_ref, output_index as usize, account, to, value, memo)?;
|
|
||||||
|
|
||||||
// Return the row number of the transaction, so the caller can fetch it for sending.
|
|
||||||
Ok(tx_ref)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ impl ConditionallySelectable for AccountId {
|
||||||
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
||||||
///
|
///
|
||||||
/// [`Transaction`]: zcash_primitives::transaction::Transaction
|
/// [`Transaction`]: zcash_primitives::transaction::Transaction
|
||||||
pub struct WalletTx {
|
pub struct WalletTx<N> {
|
||||||
pub txid: TxId,
|
pub txid: TxId,
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub num_spends: usize,
|
pub num_spends: usize,
|
||||||
pub num_outputs: usize,
|
pub num_outputs: usize,
|
||||||
pub shielded_spends: Vec<WalletShieldedSpend>,
|
pub shielded_spends: Vec<WalletShieldedSpend>,
|
||||||
pub shielded_outputs: Vec<WalletShieldedOutput>,
|
pub shielded_outputs: Vec<WalletShieldedOutput<N>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
/// A subset of a [`SpendDescription`] relevant to wallets and light clients.
|
||||||
|
@ -51,7 +51,7 @@ pub struct WalletShieldedSpend {
|
||||||
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
|
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
|
||||||
///
|
///
|
||||||
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
|
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
|
||||||
pub struct WalletShieldedOutput {
|
pub struct WalletShieldedOutput<N> {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub cmu: bls12_381::Scalar,
|
pub cmu: bls12_381::Scalar,
|
||||||
pub epk: jubjub::ExtendedPoint,
|
pub epk: jubjub::ExtendedPoint,
|
||||||
|
@ -60,6 +60,7 @@ pub struct WalletShieldedOutput {
|
||||||
pub to: PaymentAddress,
|
pub to: PaymentAddress,
|
||||||
pub is_change: bool,
|
pub is_change: bool,
|
||||||
pub witness: IncrementalWitness<Node>,
|
pub witness: IncrementalWitness<Node>,
|
||||||
|
pub nf: N,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SpendableNote {
|
pub struct SpendableNote {
|
||||||
|
|
|
@ -7,35 +7,34 @@ use zcash_primitives::{
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
note_encryption::try_sapling_compact_note_decryption,
|
note_encryption::try_sapling_compact_note_decryption,
|
||||||
primitives::{Nullifier, SaplingIvk},
|
primitives::{Note, Nullifier, PaymentAddress, SaplingIvk},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::TxId,
|
transaction::TxId,
|
||||||
|
zip32::ExtendedFullViewingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::proto::compact_formats::{CompactBlock, CompactOutput};
|
use crate::proto::compact_formats::{CompactBlock, CompactOutput};
|
||||||
use crate::wallet::{AccountId, WalletShieldedOutput, WalletShieldedSpend, WalletTx};
|
use crate::wallet::{AccountId, WalletShieldedOutput, WalletShieldedSpend, WalletTx};
|
||||||
|
|
||||||
/// Scans a [`CompactOutput`] with a set of [`ExtendedFullViewingKey`]s.
|
/// Scans a [`CompactOutput`] with a set of [`ScanningKey`]s.
|
||||||
///
|
///
|
||||||
/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this
|
/// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this
|
||||||
/// output belongs to any of the given [`ExtendedFullViewingKey`]s.
|
/// output belongs to any of the given [`ScanningKey`]s.
|
||||||
///
|
///
|
||||||
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented
|
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented
|
||||||
/// with this output's commitment.
|
/// with this output's commitment.
|
||||||
///
|
|
||||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn scan_output<P: consensus::Parameters>(
|
fn scan_output<P: consensus::Parameters, K: ScanningKey>(
|
||||||
params: &P,
|
params: &P,
|
||||||
height: BlockHeight,
|
height: BlockHeight,
|
||||||
(index, output): (usize, CompactOutput),
|
(index, output): (usize, CompactOutput),
|
||||||
ivks: &[(AccountId, SaplingIvk)],
|
vks: &[(&AccountId, &K)],
|
||||||
spent_from_accounts: &HashSet<AccountId>,
|
spent_from_accounts: &HashSet<AccountId>,
|
||||||
tree: &mut CommitmentTree<Node>,
|
tree: &mut CommitmentTree<Node>,
|
||||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||||
block_witnesses: &mut [&mut IncrementalWitness<Node>],
|
block_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||||
new_witnesses: &mut [&mut IncrementalWitness<Node>],
|
new_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||||
) -> Option<WalletShieldedOutput> {
|
) -> Option<WalletShieldedOutput<K::Nf>> {
|
||||||
let cmu = output.cmu().ok()?;
|
let cmu = output.cmu().ok()?;
|
||||||
let epk = output.epk().ok()?;
|
let epk = output.epk().ok()?;
|
||||||
let ct = output.ciphertext;
|
let ct = output.ciphertext;
|
||||||
|
@ -53,12 +52,11 @@ fn scan_output<P: consensus::Parameters>(
|
||||||
}
|
}
|
||||||
tree.append(node).unwrap();
|
tree.append(node).unwrap();
|
||||||
|
|
||||||
for (account, ivk) in ivks.iter() {
|
for (account, vk) in vks.iter() {
|
||||||
let (note, to) =
|
let (note, to) = match vk.try_decryption(params, height, &epk, &cmu, &ct) {
|
||||||
match try_sapling_compact_note_decryption(params, height, &ivk, &epk, &cmu, &ct) {
|
Some(ret) => ret,
|
||||||
Some(ret) => ret,
|
None => continue,
|
||||||
None => continue,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// A note is marked as "change" if the account that received it
|
// A note is marked as "change" if the account that received it
|
||||||
// also spent notes in the same transaction. This will catch,
|
// also spent notes in the same transaction. This will catch,
|
||||||
|
@ -68,38 +66,111 @@ fn scan_output<P: consensus::Parameters>(
|
||||||
// - Notes sent from one account to itself.
|
// - Notes sent from one account to itself.
|
||||||
let is_change = spent_from_accounts.contains(&account);
|
let is_change = spent_from_accounts.contains(&account);
|
||||||
|
|
||||||
|
let witness = IncrementalWitness::from_tree(tree);
|
||||||
|
let nf = vk.nf(¬e, &witness);
|
||||||
|
|
||||||
return Some(WalletShieldedOutput {
|
return Some(WalletShieldedOutput {
|
||||||
index,
|
index,
|
||||||
cmu,
|
cmu,
|
||||||
epk,
|
epk,
|
||||||
account: *account,
|
account: **account,
|
||||||
note,
|
note,
|
||||||
to,
|
to,
|
||||||
is_change,
|
is_change,
|
||||||
witness: IncrementalWitness::from_tree(tree),
|
witness,
|
||||||
|
nf,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Scans a [`CompactBlock`] with a set of [`ExtendedFullViewingKey`]s.
|
/// A key that can be used to perform trial decryption and nullifier
|
||||||
|
/// computation for a Sapling [`CompactOutput`]
|
||||||
|
///
|
||||||
|
/// [`CompactOutput`]: crate::proto::compact_formats::CompactOutput
|
||||||
|
pub trait ScanningKey {
|
||||||
|
type Nf;
|
||||||
|
|
||||||
|
fn try_decryption<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
params: &P,
|
||||||
|
height: BlockHeight,
|
||||||
|
epk: &jubjub::ExtendedPoint,
|
||||||
|
cmu: &bls12_381::Scalar,
|
||||||
|
ct: &[u8],
|
||||||
|
) -> Option<(Note, PaymentAddress)>;
|
||||||
|
|
||||||
|
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScanningKey for ExtendedFullViewingKey {
|
||||||
|
type Nf = Nullifier;
|
||||||
|
|
||||||
|
fn try_decryption<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
params: &P,
|
||||||
|
height: BlockHeight,
|
||||||
|
epk: &jubjub::ExtendedPoint,
|
||||||
|
cmu: &bls12_381::Scalar,
|
||||||
|
ct: &[u8],
|
||||||
|
) -> Option<(Note, PaymentAddress)> {
|
||||||
|
try_sapling_compact_note_decryption(params, height, &self.fvk.vk.ivk(), &epk, &cmu, &ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nf(&self, note: &Note, witness: &IncrementalWitness<Node>) -> Self::Nf {
|
||||||
|
note.nf(&self.fvk.vk, witness.position() as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScanningKey for SaplingIvk {
|
||||||
|
type Nf = ();
|
||||||
|
|
||||||
|
fn try_decryption<P: consensus::Parameters>(
|
||||||
|
&self,
|
||||||
|
params: &P,
|
||||||
|
height: BlockHeight,
|
||||||
|
epk: &jubjub::ExtendedPoint,
|
||||||
|
cmu: &bls12_381::Scalar,
|
||||||
|
ct: &[u8],
|
||||||
|
) -> Option<(Note, PaymentAddress)> {
|
||||||
|
try_sapling_compact_note_decryption(params, height, self, &epk, &cmu, &ct)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nf(&self, _note: &Note, _witness: &IncrementalWitness<Node>) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scans a [`CompactBlock`] with a set of [`ScanningKey`]s.
|
||||||
///
|
///
|
||||||
/// Returns a vector of [`WalletTx`]s belonging to any of the given
|
/// Returns a vector of [`WalletTx`]s belonging to any of the given
|
||||||
/// [`ExtendedFullViewingKey`]s, and the corresponding new [`IncrementalWitness`]es.
|
/// [`ScanningKey`]s. If scanning with a full viewing key, the nullifiers
|
||||||
|
/// of the resulting [`WalletShieldedOutput`]s will also be computed.
|
||||||
///
|
///
|
||||||
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
|
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
|
||||||
/// incremented appropriately.
|
/// incremented appropriately.
|
||||||
///
|
///
|
||||||
|
/// The implementation of [`ScanningKey`] may either support or omit the computation of
|
||||||
|
/// the nullifiers for received notes; the implementation for [`ExtendedFullViewingKey`]
|
||||||
|
/// will derive the nullifiers for received notes and return them as part of the resulting
|
||||||
|
/// [`WalletShieldedOutput`]s, whereas since the implementation for [`SaplingIvk`] cannot
|
||||||
|
/// do so and it will return the unit value in those outputs instead.
|
||||||
|
///
|
||||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||||
pub fn scan_block<P: consensus::Parameters>(
|
/// [`SaplingIvk`]: zcash_primitives::SaplingIvk
|
||||||
|
/// [`CompactBlock`]: crate::proto::compact_formats::CompactBlock
|
||||||
|
/// [`ScanningKey`]: self::ScanningKey
|
||||||
|
/// [`CommitmentTree`]: zcash_primitives::merkle_tree::CommitmentTree
|
||||||
|
/// [`IncrementalWitness`]: zcash_primitives::merkle_tree::IncrementalWitness
|
||||||
|
/// [`WalletShieldedOutput`]: crate::wallet::WalletShieldedOutput
|
||||||
|
pub fn scan_block<P: consensus::Parameters, K: ScanningKey>(
|
||||||
params: &P,
|
params: &P,
|
||||||
block: CompactBlock,
|
block: CompactBlock,
|
||||||
ivks: &[(AccountId, SaplingIvk)],
|
vks: &[(&AccountId, &K)],
|
||||||
nullifiers: &[(AccountId, Nullifier)],
|
nullifiers: &[(AccountId, Nullifier)],
|
||||||
tree: &mut CommitmentTree<Node>,
|
tree: &mut CommitmentTree<Node>,
|
||||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||||
) -> Vec<WalletTx> {
|
) -> Vec<WalletTx<K::Nf>> {
|
||||||
let mut wtxs: Vec<WalletTx> = vec![];
|
let mut wtxs: Vec<WalletTx<K::Nf>> = vec![];
|
||||||
let block_height = block.height();
|
let block_height = block.height();
|
||||||
|
|
||||||
for tx in block.vtx.into_iter() {
|
for tx in block.vtx.into_iter() {
|
||||||
|
@ -140,7 +211,7 @@ pub fn scan_block<P: consensus::Parameters>(
|
||||||
shielded_spends.iter().map(|spend| spend.account).collect();
|
shielded_spends.iter().map(|spend| spend.account).collect();
|
||||||
|
|
||||||
// Check for incoming notes while incrementing tree and witnesses
|
// Check for incoming notes while incrementing tree and witnesses
|
||||||
let mut shielded_outputs: Vec<WalletShieldedOutput> = vec![];
|
let mut shielded_outputs: Vec<WalletShieldedOutput<K::Nf>> = vec![];
|
||||||
{
|
{
|
||||||
// Grab mutable references to new witnesses from previous transactions
|
// Grab mutable references to new witnesses from previous transactions
|
||||||
// in this block so that we can update them. Scoped so we don't hold
|
// in this block so that we can update them. Scoped so we don't hold
|
||||||
|
@ -167,7 +238,7 @@ pub fn scan_block<P: consensus::Parameters>(
|
||||||
params,
|
params,
|
||||||
block_height,
|
block_height,
|
||||||
to_scan,
|
to_scan,
|
||||||
ivks,
|
vks,
|
||||||
&spent_from_accounts,
|
&spent_from_accounts,
|
||||||
tree,
|
tree,
|
||||||
existing_witnesses,
|
existing_witnesses,
|
||||||
|
@ -206,7 +277,7 @@ mod tests {
|
||||||
constants::SPENDING_KEY_GENERATOR,
|
constants::SPENDING_KEY_GENERATOR,
|
||||||
merkle_tree::CommitmentTree,
|
merkle_tree::CommitmentTree,
|
||||||
note_encryption::{Memo, SaplingNoteEncryption},
|
note_encryption::{Memo, SaplingNoteEncryption},
|
||||||
primitives::{Note, Nullifier},
|
primitives::{Note, Nullifier, SaplingIvk},
|
||||||
transaction::components::Amount,
|
transaction::components::Amount,
|
||||||
util::generate_random_rseed,
|
util::generate_random_rseed,
|
||||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||||
|
@ -334,7 +405,7 @@ mod tests {
|
||||||
let txs = scan_block(
|
let txs = scan_block(
|
||||||
&Network::TestNetwork,
|
&Network::TestNetwork,
|
||||||
cb,
|
cb,
|
||||||
&[(AccountId(0), extfvk.fvk.vk.ivk())],
|
&[(&AccountId(0), &extfvk)],
|
||||||
&[],
|
&[],
|
||||||
&mut tree,
|
&mut tree,
|
||||||
&mut [],
|
&mut [],
|
||||||
|
@ -373,7 +444,7 @@ mod tests {
|
||||||
let txs = scan_block(
|
let txs = scan_block(
|
||||||
&Network::TestNetwork,
|
&Network::TestNetwork,
|
||||||
cb,
|
cb,
|
||||||
&[(AccountId(0), extfvk.fvk.vk.ivk())],
|
&[(&AccountId(0), &extfvk)],
|
||||||
&[],
|
&[],
|
||||||
&mut tree,
|
&mut tree,
|
||||||
&mut [],
|
&mut [],
|
||||||
|
@ -403,12 +474,13 @@ mod tests {
|
||||||
|
|
||||||
let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false);
|
let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false);
|
||||||
assert_eq!(cb.vtx.len(), 2);
|
assert_eq!(cb.vtx.len(), 2);
|
||||||
|
let vks: Vec<(&AccountId, &SaplingIvk)> = vec![];
|
||||||
|
|
||||||
let mut tree = CommitmentTree::empty();
|
let mut tree = CommitmentTree::empty();
|
||||||
let txs = scan_block(
|
let txs = scan_block(
|
||||||
&Network::TestNetwork,
|
&Network::TestNetwork,
|
||||||
cb,
|
cb,
|
||||||
&[],
|
&vks[..],
|
||||||
&[(account, nf)],
|
&[(account, nf)],
|
||||||
&mut tree,
|
&mut tree,
|
||||||
&mut [],
|
&mut [],
|
||||||
|
|
|
@ -34,20 +34,19 @@ use zcash_primitives::{
|
||||||
block::BlockHash,
|
block::BlockHash,
|
||||||
consensus::{self, BlockHeight},
|
consensus::{self, BlockHeight},
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
note_encryption::Memo,
|
|
||||||
primitives::{Nullifier, PaymentAddress},
|
primitives::{Nullifier, PaymentAddress},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{components::Amount, TxId},
|
||||||
zip32::ExtendedFullViewingKey,
|
zip32::ExtendedFullViewingKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::RecipientAddress,
|
data_api::{
|
||||||
data_api::{BlockSource, ShieldedOutput, WalletRead, WalletWrite},
|
BlockSource, PrunedBlock, ReceivedTransaction, SentTransaction, WalletRead, WalletWrite,
|
||||||
|
},
|
||||||
encoding::encode_payment_address,
|
encoding::encode_payment_address,
|
||||||
proto::compact_formats::CompactBlock,
|
proto::compact_formats::CompactBlock,
|
||||||
wallet::{AccountId, SpendableNote, WalletTx},
|
wallet::{AccountId, SpendableNote},
|
||||||
DecryptedOutput,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::SqliteClientError;
|
use crate::error::SqliteClientError;
|
||||||
|
@ -360,10 +359,10 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
|
||||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, Self::Error>
|
fn transactionally<F, A>(&mut self, f: F) -> Result<A, SqliteClientError>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Self) -> Result<A, Self::Error>,
|
F: FnOnce(&mut Self) -> Result<A, SqliteClientError>,
|
||||||
{
|
{
|
||||||
self.wallet_db.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
self.wallet_db.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||||
match f(self) {
|
match f(self) {
|
||||||
|
@ -386,93 +385,117 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn insert_block(
|
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn advance_by_block(
|
||||||
&mut self,
|
&mut self,
|
||||||
block_height: BlockHeight,
|
block: &PrunedBlock,
|
||||||
block_hash: BlockHash,
|
updated_witnesses: &[(Self::NoteRef, IncrementalWitness<Node>)],
|
||||||
block_time: u32,
|
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error> {
|
||||||
commitment_tree: &CommitmentTree<Node>,
|
// database updates for each block are transactional
|
||||||
) -> Result<(), Self::Error> {
|
self.transactionally(|up| {
|
||||||
wallet::insert_block(self, block_height, block_hash, block_time, commitment_tree)
|
// Insert the block into the database.
|
||||||
|
wallet::insert_block(
|
||||||
|
up,
|
||||||
|
block.block_height,
|
||||||
|
block.block_hash,
|
||||||
|
block.block_time,
|
||||||
|
&block.commitment_tree,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut new_witnesses = vec![];
|
||||||
|
for tx in block.transactions {
|
||||||
|
let tx_row = wallet::put_tx_meta(up, &tx, block.block_height)?;
|
||||||
|
|
||||||
|
// Mark notes as spent and remove them from the scanning cache
|
||||||
|
for spend in &tx.shielded_spends {
|
||||||
|
wallet::mark_spent(up, tx_row, &spend.nf)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for output in &tx.shielded_outputs {
|
||||||
|
let received_note_id = wallet::put_received_note(up, output, tx_row)?;
|
||||||
|
|
||||||
|
// Save witness for note.
|
||||||
|
new_witnesses.push((received_note_id, output.witness.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert current new_witnesses into the database.
|
||||||
|
for (received_note_id, witness) in updated_witnesses.iter().chain(new_witnesses.iter())
|
||||||
|
{
|
||||||
|
if let NoteId::ReceivedNoteId(rnid) = *received_note_id {
|
||||||
|
wallet::insert_witness(up, rnid, witness, block.block_height)?;
|
||||||
|
} else {
|
||||||
|
return Err(SqliteClientError::InvalidNoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune the stored witnesses (we only expect rollbacks of at most 100 blocks).
|
||||||
|
wallet::prune_witnesses(up, block.block_height - 100)?;
|
||||||
|
|
||||||
|
// Update now-expired transactions that didn't get mined.
|
||||||
|
wallet::update_expired_notes(up, block.block_height)?;
|
||||||
|
|
||||||
|
Ok(new_witnesses)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_received_tx(
|
||||||
|
&mut self,
|
||||||
|
received_tx: &ReceivedTransaction,
|
||||||
|
) -> Result<Self::TxRef, Self::Error> {
|
||||||
|
self.transactionally(|up| {
|
||||||
|
let tx_ref = wallet::put_tx_data(up, received_tx.tx, None)?;
|
||||||
|
|
||||||
|
for output in received_tx.outputs {
|
||||||
|
if output.outgoing {
|
||||||
|
wallet::put_sent_note(up, output, tx_ref)?;
|
||||||
|
} else {
|
||||||
|
wallet::put_received_note(up, output, tx_ref)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx_ref)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error> {
|
||||||
|
// Update the database atomically, to ensure the result is internally consistent.
|
||||||
|
self.transactionally(|up| {
|
||||||
|
let tx_ref = wallet::put_tx_data(up, &sent_tx.tx, Some(sent_tx.created))?;
|
||||||
|
|
||||||
|
// Mark notes as spent.
|
||||||
|
//
|
||||||
|
// This locks the notes so they aren't selected again by a subsequent call to
|
||||||
|
// create_spend_to_address() before this transaction has been mined (at which point the notes
|
||||||
|
// get re-marked as spent).
|
||||||
|
//
|
||||||
|
// Assumes that create_spend_to_address() will never be called in parallel, which is a
|
||||||
|
// reasonable assumption for a light client such as a mobile phone.
|
||||||
|
for spend in &sent_tx.tx.shielded_spends {
|
||||||
|
wallet::mark_spent(up, tx_ref, &spend.nullifier)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet::insert_sent_note(
|
||||||
|
up,
|
||||||
|
tx_ref,
|
||||||
|
sent_tx.output_index,
|
||||||
|
sent_tx.account,
|
||||||
|
sent_tx.recipient_address,
|
||||||
|
sent_tx.value,
|
||||||
|
&sent_tx.memo,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||||
|
Ok(tx_ref)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> {
|
fn rewind_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> {
|
||||||
wallet::rewind_to_height(self.wallet_db, block_height)
|
wallet::rewind_to_height(self.wallet_db, block_height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn put_tx_meta(
|
|
||||||
&mut self,
|
|
||||||
tx: &WalletTx,
|
|
||||||
height: BlockHeight,
|
|
||||||
) -> Result<Self::TxRef, Self::Error> {
|
|
||||||
wallet::put_tx_meta(self, tx, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_tx_data(
|
|
||||||
&mut self,
|
|
||||||
tx: &Transaction,
|
|
||||||
created_at: Option<time::OffsetDateTime>,
|
|
||||||
) -> Result<Self::TxRef, Self::Error> {
|
|
||||||
wallet::put_tx_data(self, tx, created_at)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &Nullifier) -> Result<(), Self::Error> {
|
|
||||||
wallet::mark_spent(self, tx_ref, nf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumptions:
|
|
||||||
// - A transaction will not contain more than 2^63 shielded outputs.
|
|
||||||
// - A note value will never exceed 2^63 zatoshis.
|
|
||||||
fn put_received_note<T: ShieldedOutput>(
|
|
||||||
&mut self,
|
|
||||||
output: &T,
|
|
||||||
nf_opt: &Option<Nullifier>,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
) -> Result<Self::NoteRef, Self::Error> {
|
|
||||||
wallet::put_received_note(self, output, nf_opt, tx_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_witness(
|
|
||||||
&mut self,
|
|
||||||
note_id: Self::NoteRef,
|
|
||||||
witness: &IncrementalWitness<Node>,
|
|
||||||
height: BlockHeight,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
if let NoteId::ReceivedNoteId(rnid) = note_id {
|
|
||||||
wallet::insert_witness(self, rnid, witness, height)
|
|
||||||
} else {
|
|
||||||
Err(SqliteClientError::InvalidNoteId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prune_witnesses(&mut self, below_height: BlockHeight) -> Result<(), Self::Error> {
|
|
||||||
wallet::prune_witnesses(self, below_height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_expired_notes(&mut self, height: BlockHeight) -> Result<(), Self::Error> {
|
|
||||||
wallet::update_expired_notes(self, height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_sent_note(
|
|
||||||
&mut self,
|
|
||||||
output: &DecryptedOutput,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
wallet::put_sent_note(self, output, tx_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_sent_note(
|
|
||||||
&mut self,
|
|
||||||
tx_ref: Self::TxRef,
|
|
||||||
output_index: usize,
|
|
||||||
account: AccountId,
|
|
||||||
to: &RecipientAddress,
|
|
||||||
value: Amount,
|
|
||||||
memo: Option<Memo>,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
wallet::insert_sent_note(self, tx_ref, output_index, account, to, value, memo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BlockDB(Connection);
|
pub struct BlockDB(Connection);
|
||||||
|
|
|
@ -9,7 +9,7 @@ use zcash_primitives::{
|
||||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||||
note_encryption::Memo,
|
note_encryption::Memo,
|
||||||
primitives::{Nullifier, PaymentAddress},
|
primitives::{Note, Nullifier, PaymentAddress},
|
||||||
sapling::Node,
|
sapling::Node,
|
||||||
transaction::{components::Amount, Transaction, TxId},
|
transaction::{components::Amount, Transaction, TxId},
|
||||||
zip32::ExtendedFullViewingKey,
|
zip32::ExtendedFullViewingKey,
|
||||||
|
@ -17,12 +17,12 @@ use zcash_primitives::{
|
||||||
|
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
address::RecipientAddress,
|
address::RecipientAddress,
|
||||||
data_api::{error::Error, ShieldedOutput},
|
data_api::error::Error,
|
||||||
encoding::{
|
encoding::{
|
||||||
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
||||||
encode_payment_address,
|
encode_payment_address,
|
||||||
},
|
},
|
||||||
wallet::{AccountId, WalletTx},
|
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||||
DecryptedOutput,
|
DecryptedOutput,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,6 +31,70 @@ use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDB};
|
||||||
pub mod init;
|
pub mod init;
|
||||||
pub mod transact;
|
pub mod transact;
|
||||||
|
|
||||||
|
/// This trait provides a generalization over shielded output representations
|
||||||
|
/// that allows a wallet to avoid coupling to a specific one.
|
||||||
|
// TODO: it'd probably be better not to unify the definitions of
|
||||||
|
// `WalletShieldedOutput` and `DecryptedOutput` via a compositional
|
||||||
|
// approach, if possible.
|
||||||
|
pub trait ShieldedOutput {
|
||||||
|
fn index(&self) -> usize;
|
||||||
|
fn account(&self) -> AccountId;
|
||||||
|
fn to(&self) -> &PaymentAddress;
|
||||||
|
fn note(&self) -> &Note;
|
||||||
|
fn memo(&self) -> Option<&Memo>;
|
||||||
|
fn is_change(&self) -> Option<bool>;
|
||||||
|
fn nullifier(&self) -> Option<Nullifier>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShieldedOutput for WalletShieldedOutput<Nullifier> {
|
||||||
|
fn index(&self) -> usize {
|
||||||
|
self.index
|
||||||
|
}
|
||||||
|
fn account(&self) -> AccountId {
|
||||||
|
self.account
|
||||||
|
}
|
||||||
|
fn to(&self) -> &PaymentAddress {
|
||||||
|
&self.to
|
||||||
|
}
|
||||||
|
fn note(&self) -> &Note {
|
||||||
|
&self.note
|
||||||
|
}
|
||||||
|
fn memo(&self) -> Option<&Memo> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn is_change(&self) -> Option<bool> {
|
||||||
|
Some(self.is_change)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nullifier(&self) -> Option<Nullifier> {
|
||||||
|
Some(self.nf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShieldedOutput for DecryptedOutput {
|
||||||
|
fn index(&self) -> usize {
|
||||||
|
self.index
|
||||||
|
}
|
||||||
|
fn account(&self) -> AccountId {
|
||||||
|
self.account
|
||||||
|
}
|
||||||
|
fn to(&self) -> &PaymentAddress {
|
||||||
|
&self.to
|
||||||
|
}
|
||||||
|
fn note(&self) -> &Note {
|
||||||
|
&self.note
|
||||||
|
}
|
||||||
|
fn memo(&self) -> Option<&Memo> {
|
||||||
|
Some(&self.memo)
|
||||||
|
}
|
||||||
|
fn is_change(&self) -> Option<bool> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn nullifier(&self) -> Option<Nullifier> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the address for the account.
|
/// Returns the address for the account.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -458,9 +522,9 @@ pub fn insert_block<'a, P>(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn put_tx_meta<'a, P>(
|
pub fn put_tx_meta<'a, P, N>(
|
||||||
stmts: &mut DataConnStmtCache<'a, P>,
|
stmts: &mut DataConnStmtCache<'a, P>,
|
||||||
tx: &WalletTx,
|
tx: &WalletTx<N>,
|
||||||
height: BlockHeight,
|
height: BlockHeight,
|
||||||
) -> Result<i64, SqliteClientError> {
|
) -> Result<i64, SqliteClientError> {
|
||||||
let txid = tx.txid.0.to_vec();
|
let txid = tx.txid.0.to_vec();
|
||||||
|
@ -534,7 +598,6 @@ pub fn mark_spent<'a, P>(
|
||||||
pub fn put_received_note<'a, P, T: ShieldedOutput>(
|
pub fn put_received_note<'a, P, T: ShieldedOutput>(
|
||||||
stmts: &mut DataConnStmtCache<'a, P>,
|
stmts: &mut DataConnStmtCache<'a, P>,
|
||||||
output: &T,
|
output: &T,
|
||||||
nf_opt: &Option<Nullifier>,
|
|
||||||
tx_ref: i64,
|
tx_ref: i64,
|
||||||
) -> Result<NoteId, SqliteClientError> {
|
) -> Result<NoteId, SqliteClientError> {
|
||||||
let rcm = output.note().rcm().to_repr();
|
let rcm = output.note().rcm().to_repr();
|
||||||
|
@ -546,7 +609,7 @@ pub fn put_received_note<'a, P, T: ShieldedOutput>(
|
||||||
let is_change = output.is_change();
|
let is_change = output.is_change();
|
||||||
let tx = tx_ref;
|
let tx = tx_ref;
|
||||||
let output_index = output.index() as i64;
|
let output_index = output.index() as i64;
|
||||||
let nf_bytes = nf_opt.map(|nf| nf.0.to_vec());
|
let nf_bytes = output.nullifier().map(|nf| nf.0.to_vec());
|
||||||
|
|
||||||
let sql_args: &[(&str, &dyn ToSql)] = &[
|
let sql_args: &[(&str, &dyn ToSql)] = &[
|
||||||
(&":account", &account),
|
(&":account", &account),
|
||||||
|
@ -645,7 +708,7 @@ pub fn put_sent_note<'a, P: consensus::Parameters>(
|
||||||
&RecipientAddress::Shielded(output.to.clone()),
|
&RecipientAddress::Shielded(output.to.clone()),
|
||||||
Amount::from_u64(output.note.value)
|
Amount::from_u64(output.note.value)
|
||||||
.map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?,
|
.map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?,
|
||||||
Some(output.memo.clone()),
|
&Some(output.memo.clone()),
|
||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -659,7 +722,7 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
|
||||||
account: AccountId,
|
account: AccountId,
|
||||||
to: &RecipientAddress,
|
to: &RecipientAddress,
|
||||||
value: Amount,
|
value: Amount,
|
||||||
memo: Option<Memo>,
|
memo: &Option<Memo>,
|
||||||
) -> Result<(), SqliteClientError> {
|
) -> Result<(), SqliteClientError> {
|
||||||
let to_str = to.encode(&stmts.wallet_db.params);
|
let to_str = to.encode(&stmts.wallet_db.params);
|
||||||
let ivalue: i64 = value.into();
|
let ivalue: i64 = value.into();
|
||||||
|
@ -669,7 +732,7 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
|
||||||
account.0,
|
account.0,
|
||||||
to_str,
|
to_str,
|
||||||
ivalue,
|
ivalue,
|
||||||
memo.map(|m| m.as_bytes().to_vec()),
|
memo.as_ref().map(|m| m.as_bytes().to_vec()),
|
||||||
])?;
|
])?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
use byteorder::{LittleEndian, ReadBytesExt};
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::iter;
|
|
||||||
|
|
||||||
use crate::sapling::SAPLING_COMMITMENT_TREE_DEPTH;
|
use crate::sapling::SAPLING_COMMITMENT_TREE_DEPTH;
|
||||||
use crate::serialize::{Optional, Vector};
|
use crate::serialize::{Optional, Vector};
|
||||||
|
@ -274,17 +273,9 @@ impl<Node: Hashable> IncrementalWitness<Node> {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.root_inner(self.cursor_depth, PathFiller::empty()));
|
.map(|c| c.root_inner(self.cursor_depth, PathFiller::empty()));
|
||||||
|
|
||||||
let queue = if let Some(node) = cursor_root {
|
PathFiller {
|
||||||
self.filled
|
queue: self.filled.iter().cloned().chain(cursor_root).collect(),
|
||||||
.iter()
|
}
|
||||||
.cloned()
|
|
||||||
.chain(iter::once(node))
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
self.filled.iter().cloned().collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
PathFiller { queue }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the next "depth" of an unfilled subtree.
|
/// Finds the next "depth" of an unfilled subtree.
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl ProofGenerationKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ViewingKey {
|
pub struct ViewingKey {
|
||||||
pub ak: jubjub::SubgroupPoint,
|
pub ak: jubjub::SubgroupPoint,
|
||||||
pub nk: jubjub::SubgroupPoint,
|
pub nk: jubjub::SubgroupPoint,
|
||||||
|
|
Loading…
Reference in New Issue