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,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::Memo,
|
||||
primitives::{Note, Nullifier, PaymentAddress},
|
||||
primitives::{Nullifier, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
|
@ -20,7 +20,7 @@ use crate::{
|
|||
data_api::wallet::ANCHOR_OFFSET,
|
||||
decrypt::DecryptedOutput,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, SpendableNote, WalletShieldedOutput, WalletTx},
|
||||
wallet::{AccountId, SpendableNote, WalletTx},
|
||||
};
|
||||
|
||||
pub mod chain;
|
||||
|
@ -177,26 +177,47 @@ pub trait WalletRead {
|
|||
) -> 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
|
||||
/// wallet data.
|
||||
pub trait WalletWrite: WalletRead {
|
||||
/// Perform one or more write operations of this trait transactionally.
|
||||
/// Implementations of this method must ensure that all mutations to the
|
||||
/// 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(
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn advance_by_block(
|
||||
&mut self,
|
||||
block_height: BlockHeight,
|
||||
block_hash: BlockHash,
|
||||
block_time: u32,
|
||||
commitment_tree: &CommitmentTree<Node>,
|
||||
) -> Result<(), Self::Error>;
|
||||
block: &PrunedBlock,
|
||||
updated_witnesses: &[(Self::NoteRef, IncrementalWitness<Node>)],
|
||||
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, 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.
|
||||
///
|
||||
|
@ -212,80 +233,6 @@ pub trait WalletWrite: WalletRead {
|
|||
///
|
||||
/// There may be restrictions on how far it is possible to rewind.
|
||||
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
|
||||
|
@ -305,62 +252,6 @@ pub trait BlockSource {
|
|||
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")]
|
||||
pub mod testing {
|
||||
use std::collections::HashMap;
|
||||
|
@ -369,21 +260,21 @@ pub mod testing {
|
|||
block::BlockHash,
|
||||
consensus::BlockHeight,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::Memo,
|
||||
primitives::{Nullifier, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
transaction::{components::Amount, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
address::RecipientAddress,
|
||||
decrypt::DecryptedOutput,
|
||||
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 {}
|
||||
|
||||
|
@ -493,91 +384,31 @@ pub mod testing {
|
|||
}
|
||||
|
||||
impl WalletWrite for MockWalletDB {
|
||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, Self::Error>
|
||||
where
|
||||
F: FnOnce(&mut Self) -> Result<A, Self::Error>,
|
||||
{
|
||||
f(self)
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn advance_by_block(
|
||||
&mut self,
|
||||
_block: &PrunedBlock,
|
||||
_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,
|
||||
_block_height: BlockHeight,
|
||||
_block_hash: BlockHash,
|
||||
_block_time: u32,
|
||||
_commitment_tree: &CommitmentTree<Node>,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
_received_tx: &ReceivedTransaction,
|
||||
) -> Result<Self::TxRef, Self::Error> {
|
||||
Ok(TxId([0u8; 32]))
|
||||
}
|
||||
|
||||
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> {
|
||||
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,
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
merkle_tree::CommitmentTree,
|
||||
primitives::Nullifier,
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
data_api::{
|
||||
error::{ChainInvalid, Error},
|
||||
BlockSource, WalletWrite,
|
||||
BlockSource, PrunedBlock, WalletWrite,
|
||||
},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::WalletTx,
|
||||
wallet::{AccountId, WalletTx},
|
||||
welding_rig::scan_block,
|
||||
};
|
||||
|
||||
|
@ -261,10 +263,7 @@ where
|
|||
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let extfvks = data.get_extended_full_viewing_keys()?;
|
||||
let ivks: Vec<_> = extfvks
|
||||
.iter()
|
||||
.map(|(a, extfvk)| (*a, extfvk.fvk.vk.ivk()))
|
||||
.collect();
|
||||
let extfvks: Vec<(&AccountId, &ExtendedFullViewingKey)> = extfvks.iter().collect();
|
||||
|
||||
// Get the most recent CommitmentTree
|
||||
let mut tree = data
|
||||
|
@ -279,24 +278,24 @@ where
|
|||
|
||||
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
||||
let current_height = block.height();
|
||||
|
||||
// Scanned blocks MUST be height-sequential.
|
||||
if current_height != (last_height + 1) {
|
||||
return Err(
|
||||
ChainInvalid::block_height_discontinuity(last_height + 1, current_height).into(),
|
||||
);
|
||||
}
|
||||
last_height = current_height;
|
||||
|
||||
let block_hash = BlockHash::from_slice(&block.hash);
|
||||
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();
|
||||
|
||||
scan_block(
|
||||
params,
|
||||
block,
|
||||
&ivks,
|
||||
&extfvks,
|
||||
&nullifiers,
|
||||
&mut tree,
|
||||
&mut witness_refs[..],
|
||||
|
@ -309,7 +308,7 @@ where
|
|||
let cur_root = tree.root();
|
||||
for row in &witnesses {
|
||||
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 {
|
||||
|
@ -318,7 +317,7 @@ where
|
|||
return Err(Error::InvalidNewWitnessAnchor(
|
||||
output.index,
|
||||
tx.txid,
|
||||
last_height,
|
||||
current_height,
|
||||
output.witness.root(),
|
||||
)
|
||||
.into());
|
||||
|
@ -327,53 +326,32 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// database updates for each block are transactional
|
||||
data.transactionally(|up| {
|
||||
// Insert the block into the database.
|
||||
up.insert_block(current_height, block_hash, block_time, &tree)?;
|
||||
let new_witnesses = data.advance_by_block(
|
||||
&(PrunedBlock {
|
||||
block_height: current_height,
|
||||
block_hash,
|
||||
block_time,
|
||||
commitment_tree: &tree,
|
||||
transactions: &txs,
|
||||
}),
|
||||
&witnesses,
|
||||
)?;
|
||||
|
||||
for tx in txs {
|
||||
let tx_row = up.put_tx_meta(&tx, current_height)?;
|
||||
let spent_nf: Vec<Nullifier> = txs
|
||||
.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
|
||||
for spend in &tx.shielded_spends {
|
||||
up.mark_spent(tx_row, &spend.nf)?;
|
||||
}
|
||||
witnesses.extend(new_witnesses);
|
||||
|
||||
// remove spent nullifiers from the nullifier set
|
||||
nullifiers
|
||||
.retain(|(_, nf)| !tx.shielded_spends.iter().any(|spend| &spend.nf == nf));
|
||||
last_height = current_height;
|
||||
|
||||
for output in tx.shielded_outputs {
|
||||
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::{
|
||||
address::RecipientAddress,
|
||||
data_api::{error::Error, WalletWrite},
|
||||
data_api::{error::Error, ReceivedTransaction, SentTransaction, WalletWrite},
|
||||
decrypt_transaction,
|
||||
wallet::{AccountId, OvkPolicy},
|
||||
};
|
||||
|
@ -51,20 +51,12 @@ where
|
|||
if outputs.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.transactionally(|up| {
|
||||
let tx_ref = up.put_tx_data(tx, None)?;
|
||||
data.store_received_tx(&ReceivedTransaction {
|
||||
tx,
|
||||
outputs: &outputs,
|
||||
})?;
|
||||
|
||||
for output in outputs {
|
||||
if output.outgoing {
|
||||
up.put_sent_note(&output, tx_ref)?;
|
||||
} else {
|
||||
up.put_received_note(&output, &None, tx_ref)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,26 +235,13 @@ where
|
|||
None => panic!("Output 0 should exist in the transaction"),
|
||||
};
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
wallet_db.transactionally(|up| {
|
||||
let created = time::OffsetDateTime::now_utc();
|
||||
let tx_ref = up.put_tx_data(&tx, Some(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 &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)
|
||||
wallet_db.store_sent_tx(&SentTransaction {
|
||||
tx: &tx,
|
||||
created: time::OffsetDateTime::now_utc(),
|
||||
output_index: output_index as usize,
|
||||
account,
|
||||
recipient_address: to,
|
||||
value,
|
||||
memo,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,13 +30,13 @@ impl ConditionallySelectable for AccountId {
|
|||
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`Transaction`]: zcash_primitives::transaction::Transaction
|
||||
pub struct WalletTx {
|
||||
pub struct WalletTx<N> {
|
||||
pub txid: TxId,
|
||||
pub index: usize,
|
||||
pub num_spends: usize,
|
||||
pub num_outputs: usize,
|
||||
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.
|
||||
|
@ -51,7 +51,7 @@ pub struct WalletShieldedSpend {
|
|||
/// A subset of an [`OutputDescription`] relevant to wallets and light clients.
|
||||
///
|
||||
/// [`OutputDescription`]: zcash_primitives::transaction::components::OutputDescription
|
||||
pub struct WalletShieldedOutput {
|
||||
pub struct WalletShieldedOutput<N> {
|
||||
pub index: usize,
|
||||
pub cmu: bls12_381::Scalar,
|
||||
pub epk: jubjub::ExtendedPoint,
|
||||
|
@ -60,6 +60,7 @@ pub struct WalletShieldedOutput {
|
|||
pub to: PaymentAddress,
|
||||
pub is_change: bool,
|
||||
pub witness: IncrementalWitness<Node>,
|
||||
pub nf: N,
|
||||
}
|
||||
|
||||
pub struct SpendableNote {
|
||||
|
|
|
@ -7,35 +7,34 @@ use zcash_primitives::{
|
|||
consensus::{self, BlockHeight},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::try_sapling_compact_note_decryption,
|
||||
primitives::{Nullifier, SaplingIvk},
|
||||
primitives::{Note, Nullifier, PaymentAddress, SaplingIvk},
|
||||
sapling::Node,
|
||||
transaction::TxId,
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use crate::proto::compact_formats::{CompactBlock, CompactOutput};
|
||||
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
|
||||
/// 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
|
||||
/// with this output's commitment.
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: zcash_primitives::zip32::ExtendedFullViewingKey
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn scan_output<P: consensus::Parameters>(
|
||||
fn scan_output<P: consensus::Parameters, K: ScanningKey>(
|
||||
params: &P,
|
||||
height: BlockHeight,
|
||||
(index, output): (usize, CompactOutput),
|
||||
ivks: &[(AccountId, SaplingIvk)],
|
||||
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> {
|
||||
) -> Option<WalletShieldedOutput<K::Nf>> {
|
||||
let cmu = output.cmu().ok()?;
|
||||
let epk = output.epk().ok()?;
|
||||
let ct = output.ciphertext;
|
||||
|
@ -53,12 +52,11 @@ fn scan_output<P: consensus::Parameters>(
|
|||
}
|
||||
tree.append(node).unwrap();
|
||||
|
||||
for (account, ivk) in ivks.iter() {
|
||||
let (note, to) =
|
||||
match try_sapling_compact_note_decryption(params, height, &ivk, &epk, &cmu, &ct) {
|
||||
Some(ret) => ret,
|
||||
None => continue,
|
||||
};
|
||||
for (account, vk) in vks.iter() {
|
||||
let (note, to) = match vk.try_decryption(params, height, &epk, &cmu, &ct) {
|
||||
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,
|
||||
|
@ -68,38 +66,111 @@ fn scan_output<P: consensus::Parameters>(
|
|||
// - 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,
|
||||
epk,
|
||||
account: *account,
|
||||
account: **account,
|
||||
note,
|
||||
to,
|
||||
is_change,
|
||||
witness: IncrementalWitness::from_tree(tree),
|
||||
witness,
|
||||
nf,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
/// [`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
|
||||
/// 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
|
||||
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,
|
||||
block: CompactBlock,
|
||||
ivks: &[(AccountId, SaplingIvk)],
|
||||
vks: &[(&AccountId, &K)],
|
||||
nullifiers: &[(AccountId, Nullifier)],
|
||||
tree: &mut CommitmentTree<Node>,
|
||||
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
||||
) -> Vec<WalletTx> {
|
||||
let mut wtxs: Vec<WalletTx> = vec![];
|
||||
) -> Vec<WalletTx<K::Nf>> {
|
||||
let mut wtxs: Vec<WalletTx<K::Nf>> = vec![];
|
||||
let block_height = block.height();
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
// 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,
|
||||
block_height,
|
||||
to_scan,
|
||||
ivks,
|
||||
vks,
|
||||
&spent_from_accounts,
|
||||
tree,
|
||||
existing_witnesses,
|
||||
|
@ -206,7 +277,7 @@ mod tests {
|
|||
constants::SPENDING_KEY_GENERATOR,
|
||||
merkle_tree::CommitmentTree,
|
||||
note_encryption::{Memo, SaplingNoteEncryption},
|
||||
primitives::{Note, Nullifier},
|
||||
primitives::{Note, Nullifier, SaplingIvk},
|
||||
transaction::components::Amount,
|
||||
util::generate_random_rseed,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
|
@ -334,7 +405,7 @@ mod tests {
|
|||
let txs = scan_block(
|
||||
&Network::TestNetwork,
|
||||
cb,
|
||||
&[(AccountId(0), extfvk.fvk.vk.ivk())],
|
||||
&[(&AccountId(0), &extfvk)],
|
||||
&[],
|
||||
&mut tree,
|
||||
&mut [],
|
||||
|
@ -373,7 +444,7 @@ mod tests {
|
|||
let txs = scan_block(
|
||||
&Network::TestNetwork,
|
||||
cb,
|
||||
&[(AccountId(0), extfvk.fvk.vk.ivk())],
|
||||
&[(&AccountId(0), &extfvk)],
|
||||
&[],
|
||||
&mut tree,
|
||||
&mut [],
|
||||
|
@ -403,12 +474,13 @@ mod tests {
|
|||
|
||||
let cb = fake_compact_block(1u32.into(), nf, extfvk, Amount::from_u64(5).unwrap(), false);
|
||||
assert_eq!(cb.vtx.len(), 2);
|
||||
let vks: Vec<(&AccountId, &SaplingIvk)> = vec![];
|
||||
|
||||
let mut tree = CommitmentTree::empty();
|
||||
let txs = scan_block(
|
||||
&Network::TestNetwork,
|
||||
cb,
|
||||
&[],
|
||||
&vks[..],
|
||||
&[(account, nf)],
|
||||
&mut tree,
|
||||
&mut [],
|
||||
|
|
|
@ -34,20 +34,19 @@ use zcash_primitives::{
|
|||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::Memo,
|
||||
primitives::{Nullifier, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
transaction::{components::Amount, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress,
|
||||
data_api::{BlockSource, ShieldedOutput, WalletRead, WalletWrite},
|
||||
data_api::{
|
||||
BlockSource, PrunedBlock, ReceivedTransaction, SentTransaction, WalletRead, WalletWrite,
|
||||
},
|
||||
encoding::encode_payment_address,
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{AccountId, SpendableNote, WalletTx},
|
||||
DecryptedOutput,
|
||||
wallet::{AccountId, SpendableNote},
|
||||
};
|
||||
|
||||
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> {
|
||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, Self::Error>
|
||||
impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
|
||||
fn transactionally<F, A>(&mut self, f: F) -> Result<A, SqliteClientError>
|
||||
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)?;
|
||||
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,
|
||||
block_height: BlockHeight,
|
||||
block_hash: BlockHash,
|
||||
block_time: u32,
|
||||
commitment_tree: &CommitmentTree<Node>,
|
||||
) -> Result<(), Self::Error> {
|
||||
wallet::insert_block(self, block_height, block_hash, block_time, commitment_tree)
|
||||
block: &PrunedBlock,
|
||||
updated_witnesses: &[(Self::NoteRef, IncrementalWitness<Node>)],
|
||||
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error> {
|
||||
// database updates for each block are transactional
|
||||
self.transactionally(|up| {
|
||||
// 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> {
|
||||
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);
|
||||
|
|
|
@ -9,7 +9,7 @@ use zcash_primitives::{
|
|||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
note_encryption::Memo,
|
||||
primitives::{Nullifier, PaymentAddress},
|
||||
primitives::{Note, Nullifier, PaymentAddress},
|
||||
sapling::Node,
|
||||
transaction::{components::Amount, Transaction, TxId},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
|
@ -17,12 +17,12 @@ use zcash_primitives::{
|
|||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress,
|
||||
data_api::{error::Error, ShieldedOutput},
|
||||
data_api::error::Error,
|
||||
encoding::{
|
||||
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
|
||||
encode_payment_address,
|
||||
},
|
||||
wallet::{AccountId, WalletTx},
|
||||
wallet::{AccountId, WalletShieldedOutput, WalletTx},
|
||||
DecryptedOutput,
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,70 @@ use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDB};
|
|||
pub mod init;
|
||||
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.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -458,9 +522,9 @@ pub fn insert_block<'a, P>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn put_tx_meta<'a, P>(
|
||||
pub fn put_tx_meta<'a, P, N>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
tx: &WalletTx,
|
||||
tx: &WalletTx<N>,
|
||||
height: BlockHeight,
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
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>(
|
||||
stmts: &mut DataConnStmtCache<'a, P>,
|
||||
output: &T,
|
||||
nf_opt: &Option<Nullifier>,
|
||||
tx_ref: i64,
|
||||
) -> Result<NoteId, SqliteClientError> {
|
||||
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 tx = tx_ref;
|
||||
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)] = &[
|
||||
(&":account", &account),
|
||||
|
@ -645,7 +708,7 @@ pub fn put_sent_note<'a, P: consensus::Parameters>(
|
|||
&RecipientAddress::Shielded(output.to.clone()),
|
||||
Amount::from_u64(output.note.value)
|
||||
.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,
|
||||
to: &RecipientAddress,
|
||||
value: Amount,
|
||||
memo: Option<Memo>,
|
||||
memo: &Option<Memo>,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let to_str = to.encode(&stmts.wallet_db.params);
|
||||
let ivalue: i64 = value.into();
|
||||
|
@ -669,7 +732,7 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
|
|||
account.0,
|
||||
to_str,
|
||||
ivalue,
|
||||
memo.map(|m| m.as_bytes().to_vec()),
|
||||
memo.as_ref().map(|m| m.as_bytes().to_vec()),
|
||||
])?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::iter;
|
||||
|
||||
use crate::sapling::SAPLING_COMMITMENT_TREE_DEPTH;
|
||||
use crate::serialize::{Optional, Vector};
|
||||
|
@ -274,17 +273,9 @@ impl<Node: Hashable> IncrementalWitness<Node> {
|
|||
.as_ref()
|
||||
.map(|c| c.root_inner(self.cursor_depth, PathFiller::empty()));
|
||||
|
||||
let queue = if let Some(node) = cursor_root {
|
||||
self.filled
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(iter::once(node))
|
||||
.collect()
|
||||
} else {
|
||||
self.filled.iter().cloned().collect()
|
||||
};
|
||||
|
||||
PathFiller { queue }
|
||||
PathFiller {
|
||||
queue: self.filled.iter().cloned().chain(cursor_root).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the next "depth" of an unfilled subtree.
|
||||
|
|
|
@ -48,7 +48,7 @@ impl ProofGenerationKey {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ViewingKey {
|
||||
pub ak: jubjub::SubgroupPoint,
|
||||
pub nk: jubjub::SubgroupPoint,
|
||||
|
|
Loading…
Reference in New Issue