diff --git a/components/zcash_protocol/CHANGELOG.md b/components/zcash_protocol/CHANGELOG.md index 5854c937a7..2d20e03cce 100644 --- a/components/zcash_protocol/CHANGELOG.md +++ b/components/zcash_protocol/CHANGELOG.md @@ -7,6 +7,10 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_protocol::memo`: + - `impl TryFrom<&MemoBytes> for Memo` + ## [0.1.0] - 2024-03-06 The entries below are relative to the `zcash_primitives` crate as of the tag `zcash_primitives-0.14.0`. diff --git a/components/zcash_protocol/src/memo.rs b/components/zcash_protocol/src/memo.rs index c6e97b66f7..dafb1bcbe1 100644 --- a/components/zcash_protocol/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -197,13 +197,25 @@ impl TryFrom for Memo { /// Returns an error if the provided slice does not represent a valid `Memo` (for /// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical). fn try_from(bytes: MemoBytes) -> Result { + Self::try_from(&bytes) + } +} + +impl TryFrom<&MemoBytes> for Memo { + type Error = Error; + + /// Parses a `Memo` from its ZIP 302 serialization. + /// + /// Returns an error if the provided slice does not represent a valid `Memo` (for + /// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical). + fn try_from(bytes: &MemoBytes) -> Result { match bytes.0[0] { 0xF6 if bytes.0.iter().skip(1).all(|&b| b == 0) => Ok(Memo::Empty), 0xFF => Ok(Memo::Arbitrary(Box::new(bytes.0[1..].try_into().unwrap()))), b if b <= 0xF4 => str::from_utf8(bytes.as_slice()) .map(|r| Memo::Text(TextMemo(r.to_owned()))) .map_err(Error::InvalidUtf8), - _ => Ok(Memo::Future(bytes)), + _ => Ok(Memo::Future(bytes.clone())), } } } diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 863b59ebf8..a8a1535b0a 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -16,8 +16,10 @@ and this library adheres to Rust's notion of - `AccountBalance::with_orchard_balance_mut` - `AccountBirthday::orchard_frontier` - `BlockMetadata::orchard_tree_size` + - `DecryptedTransaction::{new, tx(), outputs()}` - `ScannedBlock::orchard` - `ScannedBlockCommitments::orchard` + - `SentTransaction::new` - `ORCHARD_SHARD_HEIGHT` - `BlockMetadata::orchard_tree_size` - `chain::ScanSummary::{spent_orchard_note_count, received_orchard_note_count}` @@ -51,6 +53,14 @@ and this library adheres to Rust's notion of - Changes to the `WalletRead` trait: - Added `get_account` - Added `get_orchard_nullifiers` + - Changes to the `InputSource` trait: + - `select_spendable_notes` now takes its `target_value` argument as a + `NonNegativeAmount`. Also, the values of the returned map are also + `NonNegativeAmount`s instead of `Amount`s. + - Fields of `DecryptedTransaction` are now private. Use `DecryptedTransaction::new` + and the newly provided accessors instead. + - Fields of `SentTransaction` are now private. Use `SentTransaction::new` + and the newly provided accessors instead. - `ShieldedProtocol` has a new `Orchard` variant. - `WalletCommitmentTrees` - `type OrchardShardStore` @@ -58,6 +68,13 @@ and this library adheres to Rust's notion of - `fn put_orchard_subtree_roots` - Added method `WalletRead::validate_seed` - Removed `Error::AccountNotFound` variant. +- `zcash_client_backend::decrypt`: + - Fields of `DecryptedOutput` are now private. Use `DecryptedOutput::new` + and the newly provided accessors instead. + - `decrypt_transaction` now returns a `DecryptedOutput` + instead of a `DecryptedOutput` and will decrypt Orchard + outputs when the `orchard` feature is enabled. In addition, the type + constraint on its `` parameter has been strengthened to `Copy`. - `zcash_client_backend::fees`: - Arguments to `ChangeStrategy::compute_balance` have changed. - `zcash_client_backend::zip321::render::amount_str` now takes a @@ -70,6 +87,8 @@ and this library adheres to Rust's notion of ### Removed - `zcash_client_backend::PoolType::is_receiver`: use `zcash_keys::Address::has_receiver` instead. +- `zcash_client_backend::DecryptedTransaction::sapling_outputs` use + the `DecryptedTransaction::outputs` method instead. ### Fixed - This release fixes an error in amount parsing in `zip321` that previously diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 948c9781d2..5b3fa51fef 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -81,7 +81,7 @@ use zcash_primitives::{ consensus::BlockHeight, memo::{Memo, MemoBytes}, transaction::{ - components::amount::{Amount, BalanceError, NonNegativeAmount}, + components::amount::{BalanceError, NonNegativeAmount}, Transaction, TxId, }, }; @@ -445,7 +445,7 @@ pub trait InputSource { fn select_spendable_notes( &self, account: Self::AccountId, - target_value: Amount, + target_value: NonNegativeAmount, sources: &[ShieldedProtocol], anchor_height: BlockHeight, exclude: &[Self::NoteRef], @@ -666,7 +666,7 @@ pub trait WalletRead { &self, _account: Self::AccountId, _max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(HashMap::new()) } @@ -876,13 +876,29 @@ impl ScannedBlock { } /// A transaction that was detected during scanning of the blockchain, -/// including its decrypted Sapling outputs. +/// including its decrypted Sapling and/or Orchard outputs. /// /// The purpose of this struct is to permit atomic updates of the /// wallet database when transactions are successfully decrypted. pub struct DecryptedTransaction<'a, AccountId> { - pub tx: &'a Transaction, - pub sapling_outputs: &'a Vec>, + tx: &'a Transaction, + outputs: &'a Vec>, +} + +impl<'a, AccountId> DecryptedTransaction<'a, AccountId> { + /// Constructs a new [`DecryptedTransaction`] from its constituent parts. + pub fn new(tx: &'a Transaction, outputs: &'a Vec>) -> Self { + Self { tx, outputs } + } + + /// Returns the raw transaction data. + pub fn tx(&self) -> &Transaction { + self.tx + } + /// Returns the Sapling outputs that were decrypted from the transaction. + pub fn outputs(&self) -> &Vec> { + self.outputs + } } /// A transaction that was constructed and sent by the wallet. @@ -891,13 +907,61 @@ pub struct DecryptedTransaction<'a, AccountId> { /// wallet database when transactions are created and submitted /// to the network. pub struct SentTransaction<'a, AccountId> { - pub tx: &'a Transaction, - pub created: time::OffsetDateTime, - pub account: AccountId, - pub outputs: Vec>, - pub fee_amount: Amount, + tx: &'a Transaction, + created: time::OffsetDateTime, + account: AccountId, + outputs: Vec>, + fee_amount: NonNegativeAmount, #[cfg(feature = "transparent-inputs")] - pub utxos_spent: Vec, + utxos_spent: Vec, +} + +impl<'a, AccountId> SentTransaction<'a, AccountId> { + /// Constructs a new [`SentTransaction`] from its constituent parts. + pub fn new( + tx: &'a Transaction, + created: time::OffsetDateTime, + account: AccountId, + outputs: Vec>, + fee_amount: NonNegativeAmount, + #[cfg(feature = "transparent-inputs")] utxos_spent: Vec, + ) -> Self { + Self { + tx, + created, + account, + outputs, + fee_amount, + #[cfg(feature = "transparent-inputs")] + utxos_spent, + } + } + + /// Returns the transaction that was sent. + pub fn tx(&self) -> &Transaction { + self.tx + } + /// Returns the timestamp of the transaction's creation. + pub fn created(&self) -> time::OffsetDateTime { + self.created + } + /// Returns the id for the account that created the outputs. + pub fn account_id(&self) -> &AccountId { + &self.account + } + /// Returns the outputs of the transaction. + pub fn outputs(&self) -> &[SentTransactionOutput] { + self.outputs.as_ref() + } + /// Returns the fee paid by the transaction. + pub fn fee_amount(&self) -> NonNegativeAmount { + self.fee_amount + } + /// Returns the list of UTXOs spent in the created transaction. + #[cfg(feature = "transparent-inputs")] + pub fn utxos_spent(&self) -> &[OutPoint] { + self.utxos_spent.as_ref() + } } /// An output of a transaction generated by the wallet. @@ -1281,7 +1345,7 @@ pub mod testing { block::BlockHash, consensus::{BlockHeight, Network}, memo::Memo, - transaction::{components::Amount, Transaction, TxId}, + transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, }; use crate::{ @@ -1346,7 +1410,7 @@ pub mod testing { fn select_spendable_notes( &self, _account: Self::AccountId, - _target_value: Amount, + _target_value: NonNegativeAmount, _sources: &[ShieldedProtocol], _anchor_height: BlockHeight, _exclude: &[Self::NoteRef], @@ -1498,7 +1562,7 @@ pub mod testing { &self, _account: Self::AccountId, _max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(HashMap::new()) } diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index a8925b05ba..ce2f7ce3c7 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -47,10 +47,7 @@ use zcash_primitives::{ memo::MemoBytes, transaction::{ builder::{BuildConfig, BuildResult, Builder}, - components::{ - amount::{Amount, NonNegativeAmount}, - sapling::zip212_enforcement, - }, + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, Transaction, TxId, }, @@ -166,10 +163,10 @@ where .or_else(|| params.activation_height(NetworkUpgrade::Sapling)) .expect("Sapling activation height must be known."); - data.store_decrypted_tx(DecryptedTransaction { + data.store_decrypted_tx(DecryptedTransaction::new( tx, - sapling_outputs: &decrypt_transaction(params, height, tx, &ufvks), - })?; + &decrypt_transaction(params, height, tx, &ufvks), + ))?; Ok(()) } @@ -1244,7 +1241,7 @@ where created: time::OffsetDateTime::now_utc(), account, outputs, - fee_amount: Amount::from(proposal_step.balance().fee_required()), + fee_amount: proposal_step.balance().fee_required(), #[cfg(feature = "transparent-inputs")] utxos_spent, }) diff --git a/zcash_client_backend/src/data_api/wallet/input_selection.rs b/zcash_client_backend/src/data_api/wallet/input_selection.rs index ebf891ca1c..1627698bab 100644 --- a/zcash_client_backend/src/data_api/wallet/input_selection.rs +++ b/zcash_client_backend/src/data_api/wallet/input_selection.rs @@ -462,7 +462,7 @@ where shielded_inputs = wallet_db .select_spendable_notes( account, - amount_required.into(), + amount_required, selectable_pools, anchor_height, &exclude, diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index 91a8e2a8ab..7de15c05ad 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -1,8 +1,7 @@ use std::collections::HashMap; -use sapling::note_encryption::{ - try_sapling_note_decryption, try_sapling_output_recovery, PreparedIncomingViewingKey, -}; +use sapling::note_encryption::{PreparedIncomingViewingKey, SaplingDomain}; +use zcash_note_encryption::{try_note_decryption, try_output_recovery_with_ovk}; use zcash_primitives::{ consensus::{self, BlockHeight}, memo::MemoBytes, @@ -11,7 +10,10 @@ use zcash_primitives::{ zip32::Scope, }; -use crate::keys::UnifiedFullViewingKey; +use crate::{keys::UnifiedFullViewingKey, wallet::Note}; + +#[cfg(feature = "orchard")] +use orchard::note_encryption::OrchardDomain; /// An enumeration of the possible relationships a TXO can have to the wallet. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -29,79 +31,176 @@ pub enum TransferType { /// A decrypted shielded output. pub struct DecryptedOutput { - /// The index of the output within [`shielded_outputs`]. - /// - /// [`shielded_outputs`]: zcash_primitives::transaction::TransactionData - pub index: usize, + index: usize, + note: Note, + account: AccountId, + memo: MemoBytes, + transfer_type: TransferType, +} + +impl DecryptedOutput { + pub fn new( + index: usize, + note: Note, + account: AccountId, + memo: MemoBytes, + transfer_type: TransferType, + ) -> Self { + Self { + index, + note, + account, + memo, + transfer_type, + } + } + + /// The index of the output within the shielded outputs of the Sapling bundle or the actions of + /// the Orchard bundle, depending upon the type of [`Self::note`]. + pub fn index(&self) -> usize { + self.index + } + /// The note within the output. - pub note: Note, + pub fn note(&self) -> &Note { + &self.note + } + /// The account that decrypted the note. - pub account: AccountId, + pub fn account(&self) -> &AccountId { + &self.account + } + /// The memo bytes included with the note. - pub memo: MemoBytes, - /// True if this output was recovered using an [`OutgoingViewingKey`], meaning that - /// this is a logical output of the transaction. - /// - /// [`OutgoingViewingKey`]: sapling::keys::OutgoingViewingKey - pub transfer_type: TransferType, + pub fn memo(&self) -> &MemoBytes { + &self.memo + } + + /// Returns a [`TransferType`] value that is determined based upon what type of key was used to + /// decrypt the transaction. + pub fn transfer_type(&self) -> TransferType { + self.transfer_type + } + pub fn map_note(&self, f: impl FnOnce(&Note) -> B) -> DecryptedOutput { + DecryptedOutput { + index: self.index, + note: f(&self.note), + account: self.account, + memo: self.memo.clone(), + transfer_type: self.transfer_type, + } + } } /// Scans a [`Transaction`] for any information that can be decrypted by the set of /// [`UnifiedFullViewingKey`]s. -pub fn decrypt_transaction( +pub fn decrypt_transaction( params: &P, height: BlockHeight, tx: &Transaction, ufvks: &HashMap, -) -> Vec> { +) -> Vec> { let zip212_enforcement = zip212_enforcement(params, height); - tx.sapling_bundle() - .iter() - .flat_map(|bundle| { - ufvks - .iter() - .flat_map(move |(account, ufvk)| { - ufvk.sapling() - .into_iter() - .map(|dfvk| (account.to_owned(), dfvk)) - }) - .flat_map(move |(account, dfvk)| { - let ivk_external = - PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External)); - let ivk_internal = - PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); - let ovk = dfvk.fvk().ovk; - - bundle - .shielded_outputs() - .iter() - .enumerate() - .flat_map(move |(index, output)| { - let account = account.clone(); - try_sapling_note_decryption(&ivk_external, output, zip212_enforcement) - .map(|ret| (ret, TransferType::Incoming)) - .or_else(|| { - try_sapling_note_decryption( - &ivk_internal, - output, - zip212_enforcement, - ) + let sapling_bundle = tx.sapling_bundle(); + let iter = sapling_bundle.iter().flat_map(|bundle| { + ufvks + .iter() + .flat_map(|(account, ufvk)| ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk))) + .flat_map(|(account, dfvk)| { + let sapling_domain = SaplingDomain::new(zip212_enforcement); + let ivk_external = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External)); + let ivk_internal = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + let ovk = dfvk.fvk().ovk; + + bundle + .shielded_outputs() + .iter() + .enumerate() + .flat_map(move |(index, output)| { + try_note_decryption(&sapling_domain, &ivk_external, output) + .map(|ret| (ret, TransferType::Incoming)) + .or_else(|| { + try_note_decryption(&sapling_domain, &ivk_internal, output) + .map(|ret| (ret, TransferType::WalletInternal)) + }) + .or_else(|| { + try_output_recovery_with_ovk( + &sapling_domain, + &ovk, + output, + output.cv(), + output.out_ciphertext(), + ) + .map(|ret| (ret, TransferType::Outgoing)) + }) + .into_iter() + .map(move |((note, _, memo), transfer_type)| { + DecryptedOutput::new( + index, + Note::Sapling(note), + account, + MemoBytes::from_bytes(&memo).expect("correct length"), + transfer_type, + ) + }) + }) + }) + }); + + #[cfg(feature = "orchard")] + let orchard_bundle = tx.orchard_bundle(); + #[cfg(feature = "orchard")] + let iter = iter.chain(orchard_bundle.iter().flat_map(|bundle| { + ufvks + .iter() + .flat_map(move |(account, ufvk)| { + ufvk.orchard() + .into_iter() + .map(|fvk| (account.to_owned(), fvk)) + }) + .flat_map(move |(account, fvk)| { + let ivk_external = + orchard::keys::PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::External)); + let ivk_internal = + orchard::keys::PreparedIncomingViewingKey::new(&fvk.to_ivk(Scope::Internal)); + let ovk = fvk.to_ovk(Scope::External); + + bundle + .actions() + .iter() + .enumerate() + .flat_map(move |(index, action)| { + let domain = OrchardDomain::for_nullifier(*action.nullifier()); + let account = account; + try_note_decryption(&domain, &ivk_external, action) + .map(|ret| (ret, TransferType::Incoming)) + .or_else(|| { + try_note_decryption(&domain, &ivk_internal, action) .map(|ret| (ret, TransferType::WalletInternal)) - }) - .or_else(|| { - try_sapling_output_recovery(&ovk, output, zip212_enforcement) - .map(|ret| (ret, TransferType::Outgoing)) - }) - .into_iter() - .map(move |((note, _, memo), transfer_type)| DecryptedOutput { + }) + .or_else(|| { + try_output_recovery_with_ovk( + &domain, + &ovk, + action, + action.cv_net(), + &action.encrypted_note().out_ciphertext, + ) + .map(|ret| (ret, TransferType::Outgoing)) + }) + .into_iter() + .map(move |((note, _, memo), transfer_type)| { + DecryptedOutput::new( index, - note, - account: account.clone(), - memo: MemoBytes::from_bytes(&memo).expect("correct length"), + Note::Orchard(note), + account, + MemoBytes::from_bytes(&memo).expect("correct length"), transfer_type, - }) - }) - }) - }) - .collect() + ) + }) + }) + }) + })); + + iter.collect() } diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 0351e2378a..4340c4872f 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -45,17 +45,6 @@ use std::{ path::Path, }; use subtle::ConditionallySelectable; -use zcash_keys::keys::HdSeedFingerprint; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight}, - memo::{Memo, MemoBytes}, - transaction::{ - components::amount::{Amount, NonNegativeAmount}, - Transaction, TxId, - }, - zip32::{self, DiversifierIndex, Scope}, -}; use zcash_client_backend::{ address::UnifiedAddress, @@ -75,11 +64,19 @@ use zcash_client_backend::{ wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput}, DecryptedOutput, ShieldedProtocol, TransferType, }; +use zcash_keys::keys::HdSeedFingerprint; +use zcash_primitives::{ + block::BlockHash, + memo::{Memo, MemoBytes}, + transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, +}; +use zcash_protocol::consensus::{self, BlockHeight}; +use zip32::{DiversifierIndex, Scope}; use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; #[cfg(feature = "orchard")] -use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; +use zcash_client_backend::{data_api::ORCHARD_SHARD_HEIGHT, PoolType}; #[cfg(feature = "transparent-inputs")] use { @@ -219,7 +216,7 @@ impl, P: consensus::Parameters> InputSource for fn select_spendable_notes( &self, account: AccountId, - target_value: Amount, + target_value: NonNegativeAmount, _sources: &[ShieldedProtocol], anchor_height: BlockHeight, exclude: &[Self::NoteRef], @@ -429,7 +426,7 @@ impl, P: consensus::Parameters> WalletRead for W &self, account: AccountId, max_height: BlockHeight, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { wallet::get_transparent_balances(self.conn.borrow(), &self.params, account, max_height) } @@ -703,76 +700,100 @@ impl WalletWrite for WalletDb d_tx: DecryptedTransaction, ) -> Result<(), Self::Error> { self.transactionally(|wdb| { - let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None)?; + let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx(), None, None)?; let mut spending_account_id: Option = None; - for output in d_tx.sapling_outputs { - match output.transfer_type { + for output in d_tx.outputs() { + match output.transfer_type() { TransferType::Outgoing | TransferType::WalletInternal => { - let value = output.note.value(); - let recipient = if output.transfer_type == TransferType::Outgoing { - Recipient::Sapling(output.note.recipient()) + let recipient = if output.transfer_type() == TransferType::Outgoing { + match output.note() { + Note::Sapling(n) => { + //TODO: Recover the UA, if possible. + Recipient::Sapling(n.recipient()) + } + #[cfg(feature = "orchard")] + Note::Orchard(n) => { + // TODO: Recover the actual UA, if possible. + Recipient::Unified( + UnifiedAddress::from_receivers( + Some(n.recipient()), + None, + None + ).expect("UA has an Orchard receiver by construction."), + PoolType::Shielded(ShieldedProtocol::Orchard) + ) + } + } } else { Recipient::InternalAccount( - output.account, - Note::Sapling(output.note.clone()), + *output.account(), + output.note().clone(), ) }; wallet::put_sent_output( wdb.conn.0, &wdb.params, - output.account, + *output.account(), tx_ref, - output.index, + output.index(), &recipient, - NonNegativeAmount::from_u64(value.inner()).map_err(|_| { - SqliteClientError::CorruptedData( - "Note value is not a valid Zcash amount.".to_string(), - ) - })?, - Some(&output.memo), + output.note().value(), + Some(output.memo()), )?; if matches!(recipient, Recipient::InternalAccount(_, _)) { - wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; + match output.note() { + Note::Sapling(n) => { + wallet::sapling::put_received_note(wdb.conn.0, &output.map_note(|_| n.clone()), tx_ref, None)?; + } + #[cfg(feature = "orchard")] + Note::Orchard(_) => todo!(), + } } } TransferType::Incoming => { match spending_account_id { Some(id) => { - if id != output.account { + if id != *output.account() { panic!("Unable to determine a unique account identifier for z->t spend."); } } None => { - spending_account_id = Some(output.account); + spending_account_id = Some(*output.account()); } } - wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?; + match output.note() { + Note::Sapling(n) => { + wallet::sapling::put_received_note(wdb.conn.0, &output.map_note(|_| n.clone()), tx_ref, None)?; + } + #[cfg(feature = "orchard")] + Note::Orchard(_) => todo!(), + } } } } // If any of the utxos spent in the transaction are ours, mark them as spent. #[cfg(feature = "transparent-inputs")] - for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) { + for txin in d_tx.tx().transparent_bundle().iter().flat_map(|b| b.vin.iter()) { wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, &txin.prevout)?; } // If we have some transparent outputs: - if d_tx.tx.transparent_bundle().iter().any(|b| !b.vout.is_empty()) { + if d_tx.tx().transparent_bundle().iter().any(|b| !b.vout.is_empty()) { let nullifiers = wdb.get_sapling_nullifiers(NullifierQuery::All)?; // If the transaction contains shielded spends from our wallet, we will store z->t // transactions we observe in the same way they would be stored by // create_spend_to_address. if let Some((account_id, _)) = nullifiers.iter().find( |(_, nf)| - d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter()) + d_tx.tx().sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter()) .any(|input| nf == input.nullifier()) ) { - for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() { + for (output_index, txout) in d_tx.tx().transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() { if let Some(address) = txout.recipient_address() { wallet::put_sent_output( wdb.conn.0, @@ -797,9 +818,9 @@ impl WalletWrite for WalletDb self.transactionally(|wdb| { let tx_ref = wallet::put_tx_data( wdb.conn.0, - sent_tx.tx, - Some(sent_tx.fee_amount), - Some(sent_tx.created), + sent_tx.tx(), + Some(sent_tx.fee_amount()), + Some(sent_tx.created()), )?; // Mark notes as spent. @@ -810,7 +831,7 @@ impl WalletWrite for WalletDb // // 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. - if let Some(bundle) = sent_tx.tx.sapling_bundle() { + if let Some(bundle) = sent_tx.tx().sapling_bundle() { for spend in bundle.shielded_spends() { wallet::sapling::mark_sapling_note_spent( wdb.conn.0, @@ -821,16 +842,16 @@ impl WalletWrite for WalletDb } #[cfg(feature = "transparent-inputs")] - for utxo_outpoint in &sent_tx.utxos_spent { + for utxo_outpoint in sent_tx.utxos_spent() { wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?; } - for output in &sent_tx.outputs { + for output in sent_tx.outputs() { wallet::insert_sent_output( wdb.conn.0, &wdb.params, tx_ref, - sent_tx.account, + *sent_tx.account_id(), output, )?; @@ -838,15 +859,15 @@ impl WalletWrite for WalletDb Recipient::InternalAccount(account, Note::Sapling(note)) => { wallet::sapling::put_received_note( wdb.conn.0, - &DecryptedOutput { - index: output.output_index(), - note: note.clone(), - account: *account, - memo: output + &DecryptedOutput::new( + output.output_index(), + note.clone(), + *account, + output .memo() .map_or_else(MemoBytes::empty, |memo| memo.clone()), - transfer_type: TransferType::WalletInternal, - }, + TransferType::WalletInternal, + ), tx_ref, None, )?; @@ -855,15 +876,15 @@ impl WalletWrite for WalletDb Recipient::InternalAccount(account, Note::Orchard(note)) => { wallet::orchard::put_received_note( wdb.conn.0, - &DecryptedOutput { - index: output.output_index(), - note: note.clone(), - account: *account, - memo: output + &DecryptedOutput::new( + output.output_index(), + *note, + *account, + output .memo() .map_or_else(MemoBytes::empty, |memo| memo.clone()), - transfer_type: TransferType::WalletInternal, - }, + TransferType::WalletInternal, + ), tx_ref, None, )?; diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 2cc3172330..e1f63c5a80 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -1607,7 +1607,7 @@ pub(crate) fn get_transparent_balances( params: &P, account: AccountId, max_height: BlockHeight, -) -> Result, SqliteClientError> { +) -> Result, SqliteClientError> { let chain_tip_height = scan_queue_extrema(conn)?.map(|range| *range.end()); let stable_height = chain_tip_height .unwrap_or(max_height) @@ -1633,7 +1633,7 @@ pub(crate) fn get_transparent_balances( while let Some(row) = rows.next()? { let taddr_str: String = row.get(0)?; let taddr = TransparentAddress::decode(params, &taddr_str)?; - let value = Amount::from_i64(row.get(1)?).unwrap(); + let value = NonNegativeAmount::from_nonnegative_i64(row.get(1)?)?; res.insert(taddr, value); } @@ -1754,7 +1754,7 @@ pub(crate) fn put_tx_meta( pub(crate) fn put_tx_data( conn: &rusqlite::Connection, tx: &Transaction, - fee: Option, + fee: Option, created_at: Option, ) -> Result { let mut stmt_upsert_tx_data = conn.prepare_cached( @@ -1776,7 +1776,7 @@ pub(crate) fn put_tx_data( ":created_at": created_at, ":expiry_height": u32::from(tx.expiry_height()), ":raw": raw_tx, - ":fee": fee.map(i64::from), + ":fee": fee.map(u64::from), ]; stmt_upsert_tx_data @@ -2213,7 +2213,7 @@ mod tests { zcash_primitives::{ consensus::BlockHeight, transaction::{ - components::{Amount, OutPoint, TxOut}, + components::{OutPoint, TxOut}, fees::fixed::FeeRule as FixedFeeRule, }, }, @@ -2320,7 +2320,7 @@ mod tests { assert_matches!( st.wallet().get_transparent_balances(account_id, height_2), - Ok(h) if h.get(taddr) == Some(&value.into()) + Ok(h) if h.get(taddr) == Some(&value) ); // Artificially delete the address from the addresses table so that @@ -2400,8 +2400,8 @@ mod tests { .unwrap() .get(taddr) .cloned() - .unwrap_or(Amount::zero()), - Amount::from(expected), + .unwrap_or(NonNegativeAmount::ZERO), + expected, ); assert_eq!( st.wallet() diff --git a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs index 961f270d8d..3b9cec5391 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/receiving_key_scopes.rs @@ -295,8 +295,7 @@ mod tests { decrypt_transaction, proto::compact_formats::{CompactBlock, CompactTx}, scanning::{scan_block, Nullifiers, ScanningKeys}, - wallet::Recipient, - PoolType, ShieldedProtocol, TransferType, + TransferType, }; use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; use zcash_primitives::{ @@ -495,49 +494,57 @@ mod tests { // We can't use `decrypt_and_store_transaction` because we haven't migrated yet. // Replicate its relevant innards here. - let d_tx = DecryptedTransaction { + let outputs = decrypt_transaction( + ¶ms, + height, tx, - sapling_outputs: &decrypt_transaction( - ¶ms, - height, - tx, - &[(account_id, ufvk0)].into_iter().collect(), - ), - }; + &[(account_id, ufvk0)].into_iter().collect(), + ); + let d_tx = DecryptedTransaction::new(tx, &outputs); db_data .transactionally::<_, _, rusqlite::Error>(|wdb| { - let tx_ref = crate::wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None).unwrap(); + let tx_ref = crate::wallet::put_tx_data(wdb.conn.0, d_tx.tx(), None, None).unwrap(); let mut spending_account_id: Option = None; - for output in d_tx.sapling_outputs { - match output.transfer_type { + for output in d_tx.outputs() { + match output.transfer_type() { TransferType::Outgoing | TransferType::WalletInternal => { - let recipient = if output.transfer_type == TransferType::Outgoing { - Recipient::Sapling(output.note.recipient()) - } else { - Recipient::InternalAccount( - output.account, - PoolType::Shielded(ShieldedProtocol::Sapling), - ) - }; - // Don't need to bother with sent outputs for this test. - if matches!(recipient, Recipient::InternalAccount(_, _)) { - put_received_note_before_migration( - wdb.conn.0, output, tx_ref, None, - ) - .unwrap(); + if output.transfer_type() != TransferType::Outgoing{ + match output.note() { + zcash_client_backend::wallet::Note::Sapling(n) => { + put_received_note_before_migration( + wdb.conn.0, &output.map_note(|_| n.clone()), tx_ref, None, + ) + .unwrap(); + } + #[cfg(feature = "orchard")] + zcash_client_backend::wallet::Note::Orchard(_) => { + unimplemented!("Orchard was not supported at the time of this migration."); + } + } } } TransferType::Incoming => { match spending_account_id { - Some(id) => assert_eq!(id, output.account), + Some(id) => assert_eq!(id, *output.account()), None => { - spending_account_id = Some(output.account); + spending_account_id = Some(*output.account()); + } + } + match output.note() { + zcash_client_backend::wallet::Note::Sapling(n) => { + put_received_note_before_migration( + wdb.conn.0, &output.map_note(|_| n.clone()), tx_ref, None, + ) + .unwrap(); + } + + #[cfg(feature = "orchard")] + zcash_client_backend::wallet::Note::Orchard(_) => { + unimplemented!("Orchard was not supported at the time of this migration."); } } - put_received_note_before_migration(wdb.conn.0, output, tx_ref, None) - .unwrap(); } } } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs index bc49a09c2b..232b7f6ccd 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/sapling_memo_consistency.rs @@ -103,8 +103,8 @@ impl RusqliteMigration for Migration

{ for d_out in decrypted_outputs { stmt_update_sent_memo.execute(named_params![ ":id_tx": id_tx, - ":output_index": d_out.index, - ":memo": memo_repr(Some(&d_out.memo)) + ":output_index": d_out.index(), + ":memo": memo_repr(Some(d_out.memo())) ])?; } } diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 31eecdc434..a5c308678c 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -52,19 +52,19 @@ impl ReceivedOrchardOutput for WalletOrchardOutput { impl ReceivedOrchardOutput for DecryptedOutput { fn index(&self) -> usize { - self.index + self.index() } fn account_id(&self) -> AccountId { - self.account + *self.account() } fn note(&self) -> &orchard::note::Note { - &self.note + self.note() } fn memo(&self) -> Option<&MemoBytes> { - Some(&self.memo) + Some(self.memo()) } fn is_change(&self) -> bool { - self.transfer_type == TransferType::WalletInternal + self.transfer_type() == TransferType::WalletInternal } fn nullifier(&self) -> Option<&orchard::note::Nullifier> { None @@ -73,7 +73,7 @@ impl ReceivedOrchardOutput for DecryptedOutput { None } fn recipient_key_scope(&self) -> Option { - if self.transfer_type == TransferType::WalletInternal { + if self.transfer_type() == TransferType::WalletInternal { Some(Scope::Internal) } else { Some(Scope::External) diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 58e909b5ca..db5b4b6052 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -12,10 +12,10 @@ use zcash_client_backend::{ wallet::{Note, ReceivedNote, WalletSaplingOutput}, DecryptedOutput, TransferType, }; -use zcash_primitives::{ +use zcash_primitives::transaction::{components::amount::NonNegativeAmount, TxId}; +use zcash_protocol::{ consensus::{self, BlockHeight}, memo::MemoBytes, - transaction::{components::Amount, TxId}, }; use zip32::Scope; @@ -64,19 +64,19 @@ impl ReceivedSaplingOutput for WalletSaplingOutput { impl ReceivedSaplingOutput for DecryptedOutput { fn index(&self) -> usize { - self.index + self.index() } fn account_id(&self) -> AccountId { - self.account + *self.account() } fn note(&self) -> &sapling::Note { - &self.note + self.note() } fn memo(&self) -> Option<&MemoBytes> { - Some(&self.memo) + Some(self.memo()) } fn is_change(&self) -> bool { - self.transfer_type == TransferType::WalletInternal + self.transfer_type() == TransferType::WalletInternal } fn nullifier(&self) -> Option<&sapling::Nullifier> { None @@ -85,7 +85,7 @@ impl ReceivedSaplingOutput for DecryptedOutput { None } fn recipient_key_scope(&self) -> Option { - if self.transfer_type == TransferType::WalletInternal { + if self.transfer_type() == TransferType::WalletInternal { Some(Scope::Internal) } else { Some(Scope::External) @@ -232,7 +232,7 @@ pub(crate) fn select_spendable_sapling_notes( conn: &Connection, params: &P, account: AccountId, - target_value: Amount, + target_value: NonNegativeAmount, anchor_height: BlockHeight, exclude: &[ReceivedNoteId], ) -> Result>, SqliteClientError> { @@ -306,7 +306,7 @@ pub(crate) fn select_spendable_sapling_notes( named_params![ ":account": account.0, ":anchor_height": &u32::from(anchor_height), - ":target_value": &i64::from(target_value), + ":target_value": &u64::from(target_value), ":exclude": &excluded_ptr, ":wallet_birthday": u32::from(birthday_height) ], @@ -470,7 +470,7 @@ pub(crate) mod tests { legacy::TransparentAddress, memo::{Memo, MemoBytes}, transaction::{ - components::{amount::NonNegativeAmount, sapling::zip212_enforcement, Amount}, + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, fees::{ fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule, }, @@ -603,10 +603,10 @@ pub(crate) mod tests { let mut found_tx_change_memo = false; let mut found_tx_empty_memo = false; for output in decrypted_outputs { - if output.memo == change_memo.clone().into() { + if Memo::try_from(output.memo()).unwrap() == change_memo { found_tx_change_memo = true } - if output.memo == Memo::Empty.into() { + if Memo::try_from(output.memo()).unwrap() == Memo::Empty { found_tx_empty_memo = true } } @@ -1733,7 +1733,7 @@ pub(crate) mod tests { &st.wallet().conn, &st.wallet().params, account.0, - Amount::const_from_i64(300000), + NonNegativeAmount::const_from_u64(300000), received_tx_height + 10, &[], ) @@ -1749,7 +1749,7 @@ pub(crate) mod tests { &st.wallet().conn, &st.wallet().params, account.0, - Amount::const_from_i64(300000), + NonNegativeAmount::const_from_u64(300000), received_tx_height + 10, &[], ) @@ -1803,7 +1803,7 @@ pub(crate) mod tests { &st.wallet().conn, &st.wallet().params, account, - Amount::const_from_i64(300000), + NonNegativeAmount::const_from_u64(300000), birthday.height() + 5, &[], )