From 64c75149416d18da91ac9a6ab4e136e8f13671fc Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 19 Jan 2021 08:23:58 -0700 Subject: [PATCH] WIP: Create an in-memory wallet backend. --- Cargo.toml | 2 +- components/zcash_protocol/src/lib.rs | 4 +- components/zcash_protocol/src/memo.rs | 9 +- rust-toolchain.toml | 2 +- zcash_client_backend/src/data_api.rs | 3 + .../src/data_api/mem_wallet.rs | 459 ++++++++++++++++++ zcash_client_backend/src/wallet.rs | 2 +- zcash_client_backend/src/zip321.rs | 4 +- zcash_client_sqlite/src/lib.rs | 6 +- zcash_client_sqlite/src/wallet.rs | 6 +- .../src/wallet/commitment_tree.rs | 3 +- zcash_primitives/src/transaction/sighash.rs | 3 +- zcash_proofs/src/circuit/sprout/input.rs | 2 +- 13 files changed, 480 insertions(+), 25 deletions(-) create mode 100644 zcash_client_backend/src/data_api/mem_wallet.rs diff --git a/Cargo.toml b/Cargo.toml index ab50918ed..a5ec47ca6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ [workspace.package] edition = "2021" -rust-version = "1.65" +rust-version = "1.69" repository = "https://github.com/zcash/librustzcash" license = "MIT OR Apache-2.0" categories = ["cryptography::cryptocurrencies"] diff --git a/components/zcash_protocol/src/lib.rs b/components/zcash_protocol/src/lib.rs index 2976a0264..3bbc61a97 100644 --- a/components/zcash_protocol/src/lib.rs +++ b/components/zcash_protocol/src/lib.rs @@ -25,7 +25,7 @@ pub mod memo; pub mod value; /// A Zcash shielded transfer protocol. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ShieldedProtocol { /// The Sapling protocol Sapling, @@ -34,7 +34,7 @@ pub enum ShieldedProtocol { } /// A value pool in the Zcash protocol. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum PoolType { /// The transparent value pool Transparent, diff --git a/components/zcash_protocol/src/memo.rs b/components/zcash_protocol/src/memo.rs index dafb1bcbe..10258a52d 100644 --- a/components/zcash_protocol/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -144,9 +144,10 @@ impl Deref for TextMemo { } /// An unencrypted memo received alongside a shielded note in a Zcash transaction. -#[derive(Clone)] +#[derive(Clone, Default)] pub enum Memo { /// An empty memo field. + #[default] Empty, /// A memo field containing a UTF-8 string. Text(TextMemo), @@ -171,12 +172,6 @@ impl fmt::Debug for Memo { } } -impl Default for Memo { - fn default() -> Self { - Memo::Empty - } -} - impl PartialEq for Memo { fn eq(&self, rhs: &Memo) -> bool { match (self, rhs) { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5ecda6e49..190bd174a 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.65.0" +channel = "1.69.0" components = [ "clippy", "rustfmt" ] diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 53ac896b1..59908b74f 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -104,6 +104,9 @@ pub mod error; pub mod scanning; pub mod wallet; +#[cfg(any(test, feature = "test-dependencies"))] +pub mod mem_wallet; + /// The height of subtree roots in the Sapling note commitment tree. /// /// This conforms to the structure of subtree data returned by diff --git a/zcash_client_backend/src/data_api/mem_wallet.rs b/zcash_client_backend/src/data_api/mem_wallet.rs new file mode 100644 index 000000000..8ceab026f --- /dev/null +++ b/zcash_client_backend/src/data_api/mem_wallet.rs @@ -0,0 +1,459 @@ +#![allow(unused)] +use incrementalmerkletree::Address; +use secrecy::{ExposeSecret, SecretVec}; +use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; +use std::{ + cmp::Ordering, + collections::{BTreeMap, HashMap, HashSet}, + convert::Infallible, + num::NonZeroU32, +}; +use zcash_keys::keys::{AddressGenerationError, DerivationError}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; + +use zcash_primitives::{ + block::BlockHash, + consensus::{BlockHeight, Network}, + transaction::{Transaction, TxId}, + zip32::AccountId, +}; +use zcash_protocol::{ + memo::{self, Memo, MemoBytes}, + value::Zatoshis, +}; + +use crate::{ + address::UnifiedAddress, + keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, + wallet::{NoteId, WalletTransparentOutput, WalletTx}, +}; + +use super::{ + chain::CommitmentTreeRoot, scanning::ScanRange, AccountBirthday, BlockMetadata, + DecryptedTransaction, NullifierQuery, ScannedBlock, SentTransaction, WalletCommitmentTrees, + WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, +}; + +#[cfg(feature = "transparent-inputs")] +use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress}; + +#[cfg(feature = "orchard")] +use super::ORCHARD_SHARD_HEIGHT; + +struct MemoryWalletBlock { + height: BlockHeight, + hash: BlockHash, + block_time: u32, + // Just the transactions that involve an account in this wallet + transactions: HashMap>, + memos: HashMap, +} + +impl PartialEq for MemoryWalletBlock { + fn eq(&self, other: &Self) -> bool { + (self.height, self.block_time) == (other.height, other.block_time) + } +} + +impl Eq for MemoryWalletBlock {} + +impl PartialOrd for MemoryWalletBlock { + fn partial_cmp(&self, other: &Self) -> Option { + Some((self.height, self.block_time).cmp(&(other.height, other.block_time))) + } +} + +impl Ord for MemoryWalletBlock { + fn cmp(&self, other: &Self) -> Ordering { + (self.height, self.block_time).cmp(&(other.height, other.block_time)) + } +} + +pub struct MemoryWalletAccount { + seed_fingerprint: SeedFingerprint, + account_id: AccountId, + ufvk: UnifiedFullViewingKey, + birthday: AccountBirthday, + addresses: BTreeMap, + notes: HashSet, +} + +pub struct MemoryWalletDb { + network: Network, + accounts: BTreeMap, + blocks: BTreeMap, + tx_idx: HashMap, + sapling_spends: HashMap, + #[cfg(feature = "orchard")] + orchard_spends: HashMap, + sapling_tree: ShardTree< + MemoryShardStore, + { SAPLING_SHARD_HEIGHT * 2 }, + SAPLING_SHARD_HEIGHT, + >, + #[cfg(feature = "orchard")] + orchard_tree: ShardTree< + MemoryShardStore, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, +} + +#[derive(Debug)] +pub enum Error { + AccountUnknown(u32), + MemoDecryption(memo::Error), + KeyDerivation(DerivationError), + AddressGeneration(AddressGenerationError), +} + +impl From for Error { + fn from(value: DerivationError) -> Self { + Error::KeyDerivation(value) + } +} + +impl From for Error { + fn from(value: AddressGenerationError) -> Self { + Error::AddressGeneration(value) + } +} + +impl From for Error { + fn from(value: memo::Error) -> Self { + Error::MemoDecryption(value) + } +} + +impl WalletRead for MemoryWalletDb { + type Error = Error; + type AccountId = u32; + type Account = (u32, UnifiedFullViewingKey); + + fn get_account_ids(&self) -> Result, Self::Error> { + Ok(Vec::new()) + } + + fn get_account( + &self, + _account_id: Self::AccountId, + ) -> Result, Self::Error> { + todo!() + } + + fn get_derived_account( + &self, + _seed: &SeedFingerprint, + _account_id: zip32::AccountId, + ) -> Result, Self::Error> { + todo!() + } + + fn validate_seed( + &self, + _account_id: Self::AccountId, + _seed: &SecretVec, + ) -> Result { + todo!() + } + + fn get_account_for_ufvk( + &self, + ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + let ufvk_req = + UnifiedAddressRequest::all().expect("At least one protocol should be enabled"); + Ok(self.accounts.iter().find_map(|(id, acct)| { + if acct.ufvk.default_address(ufvk_req).unwrap() + == ufvk.default_address(ufvk_req).unwrap() + { + Some((*id, acct.ufvk.clone())) + } else { + None + } + })) + } + + fn get_current_address( + &self, + account: Self::AccountId, + ) -> Result, Self::Error> { + self.accounts + .get(&account) + .map(|account| { + account + .ufvk + .default_address( + UnifiedAddressRequest::all() + .expect("At least one protocol should be enabled."), + ) + .map(|(addr, _)| addr) + }) + .transpose() + .map_err(|e| e.into()) + } + + fn get_account_birthday(&self, _account: Self::AccountId) -> Result { + Err(Error::AccountUnknown(_account)) + } + + fn get_wallet_birthday(&self) -> Result, Self::Error> { + todo!() + } + + fn get_wallet_summary( + &self, + _min_confirmations: u32, + ) -> Result>, Self::Error> { + todo!() + } + + fn chain_height(&self) -> Result, Self::Error> { + todo!() + } + + fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { + Ok(self.blocks.iter().find_map(|b| { + if b.0 == &block_height { + Some(b.1.hash) + } else { + None + } + })) + } + + fn block_metadata(&self, _height: BlockHeight) -> Result, Self::Error> { + todo!() + } + + fn block_fully_scanned(&self) -> Result, Self::Error> { + todo!() + } + + fn get_max_height_hash(&self) -> Result, Self::Error> { + todo!() + } + + fn block_max_scanned(&self) -> Result, Self::Error> { + todo!() + } + + fn suggest_scan_ranges(&self) -> Result, Self::Error> { + Ok(vec![]) + } + + fn get_target_and_anchor_heights( + &self, + _min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + todo!() + } + + fn get_min_unspent_height(&self) -> Result, Self::Error> { + todo!() + } + + fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + todo!() + } + + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + fn get_memo(&self, id_note: NoteId) -> Result, Self::Error> { + self.tx_idx + .get(id_note.txid()) + .and_then(|height| self.blocks.get(height)) + .and_then(|block| block.memos.get(&id_note)) + .map(Memo::try_from) + .transpose() + .map_err(Error::from) + } + + fn get_transaction(&self, _id_tx: TxId) -> Result, Self::Error> { + todo!() + } + + fn get_sapling_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( + &self, + _account: Self::AccountId, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( + &self, + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } +} + +impl WalletWrite for MemoryWalletDb { + type UtxoRef = u32; + + fn create_account( + &mut self, + seed: &SecretVec, + birthday: AccountBirthday, + ) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> { + let seed_fingerprint = + SeedFingerprint::from_seed(seed.expose_secret()).expect("Valid seed."); + let account_id = self.accounts.last_key_value().map_or(0, |(id, _)| id + 1); + let account_index = AccountId::try_from(account_id).unwrap(); + let usk = + UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account_index)?; + let ufvk = usk.to_unified_full_viewing_key(); + self.accounts.insert( + account_id, + MemoryWalletAccount { + seed_fingerprint, + account_id: account_index, + ufvk, + birthday, + addresses: BTreeMap::new(), + notes: HashSet::new(), + }, + ); + + Ok((account_id, usk)) + } + + fn get_next_available_address( + &mut self, + _account: Self::AccountId, + _request: UnifiedAddressRequest, + ) -> Result, Self::Error> { + todo!() + } + + fn update_chain_tip(&mut self, _tip_height: BlockHeight) -> Result<(), Self::Error> { + todo!() + } + + fn put_blocks( + &mut self, + _from_state: &super::chain::ChainState, + _blocks: Vec>, + ) -> Result<(), Self::Error> { + todo!() + } + + /// Adds a transparent UTXO received by the wallet to the data store. + fn put_received_transparent_utxo( + &mut self, + _output: &WalletTransparentOutput, + ) -> Result { + Ok(0) + } + + fn store_decrypted_tx( + &mut self, + _received_tx: DecryptedTransaction, + ) -> Result<(), Self::Error> { + todo!() + } + + fn store_sent_tx( + &mut self, + _sent_tx: &SentTransaction, + ) -> Result<(), Self::Error> { + todo!() + } + + fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> { + todo!() + } +} + +impl WalletCommitmentTrees for MemoryWalletDb { + type Error = Infallible; + type SaplingShardStore<'a> = MemoryShardStore; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + callback(&mut self.sapling_tree) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + self.with_sapling_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = Address::from_parts(SAPLING_SHARD_HEIGHT.into(), start_index + i); + t.insert(root_addr, *root.root_hash())?; + } + Ok::<_, ShardTreeError>(()) + })?; + + Ok(()) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = MemoryShardStore; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + callback(&mut self.orchard_tree) + } + + /// Adds a sequence of note commitment tree subtree roots to the data store. + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + self.with_orchard_tree_mut(|t| { + for (root, i) in roots.iter().zip(0u64..) { + let root_addr = Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), start_index + i); + t.insert(root_addr, *root.root_hash())?; + } + Ok::<_, ShardTreeError>(()) + })?; + + Ok(()) + } +} diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index e849d5d70..133ed234f 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -27,7 +27,7 @@ use crate::fees::orchard as orchard_fees; use zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope}; /// A unique identifier for a shielded transaction output -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NoteId { txid: TxId, protocol: ShieldedProtocol, diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs index bd020bb3e..28f505e15 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/zcash_client_backend/src/zip321.rs @@ -672,8 +672,8 @@ mod parse { )(input) } - fn to_indexed_param<'a, P: consensus::Parameters>( - params: &'a P, + fn to_indexed_param( + params: &P, ((name, iopt), value): ((&str, Option<&str>), &str), ) -> Result { let param = match name { diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 15d83fc2b..5277472d2 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -50,7 +50,7 @@ use zcash_primitives::{ consensus::{self, BlockHeight}, memo::{Memo, MemoBytes}, transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, - zip32::{self, DiversifierIndex, Scope}, + zip32::{self, DiversifierIndex}, }; use zip32::fingerprint::SeedFingerprint; @@ -654,7 +654,7 @@ impl WalletWrite for WalletDb let spent_in = output .nf() .map(|nf| { - wallet::query_nullifier_map::<_, Scope>( + wallet::query_nullifier_map( wdb.conn.0, ShieldedProtocol::Sapling, nf, @@ -672,7 +672,7 @@ impl WalletWrite for WalletDb let spent_in = output .nf() .map(|nf| { - wallet::query_nullifier_map::<_, Scope>( + wallet::query_nullifier_map( wdb.conn.0, ShieldedProtocol::Orchard, &nf.to_bytes(), diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index c4964257e..0c47270bf 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -2626,7 +2626,7 @@ pub(crate) fn insert_nullifier_map>( /// Returns the row of the `transactions` table corresponding to the transaction in which /// this nullifier is revealed, if any. -pub(crate) fn query_nullifier_map, S>( +pub(crate) fn query_nullifier_map>( conn: &rusqlite::Transaction<'_>, spend_pool: ShieldedProtocol, nf: &N, @@ -2799,7 +2799,7 @@ mod tests { height_1, &[] ).as_deref(), - Ok(&[ref ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_1) + Ok([ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_1) ); // Change the mined height of the UTXO and upsert; we should get back @@ -2822,7 +2822,7 @@ mod tests { st.wallet() .get_unspent_transparent_outputs(taddr, height_2, &[]) .as_deref(), - Ok(&[ref ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_2) + Ok([ret]) if (ret.outpoint(), ret.txout(), ret.height()) == (utxo.outpoint(), utxo.txout(), height_2) ); assert_matches!( diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 8dde47a53..6dfcd0d5f 100644 --- a/zcash_client_sqlite/src/wallet/commitment_tree.rs +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -1209,7 +1209,6 @@ mod tests { // introduce some roots let roots = (0u32..4) - .into_iter() .map(|idx| { CommitmentTreeRoot::from_parts( BlockHeight::from((idx + 1) * 3), @@ -1228,7 +1227,7 @@ mod tests { let checkpoint_height = BlockHeight::from(3); tree.batch_insert( Position::from(24), - ('a'..='h').into_iter().map(|c| { + ('a'..='h').map(|c| { ( c.to_string(), match c { diff --git a/zcash_primitives/src/transaction/sighash.rs b/zcash_primitives/src/transaction/sighash.rs index 702ed1757..4be0a7454 100644 --- a/zcash_primitives/src/transaction/sighash.rs +++ b/zcash_primitives/src/transaction/sighash.rs @@ -76,13 +76,12 @@ pub trait TransparentAuthorizingContext: transparent::Authorization { /// set of precomputed hashes produced in the construction of the /// transaction ID. pub fn signature_hash< - 'a, TA: TransparentAuthorizingContext, SA: sapling::bundle::Authorization, A: Authorization, >( tx: &TransactionData, - signable_input: &SignableInput<'a>, + signable_input: &SignableInput<'_>, txid_parts: &TxDigests, ) -> SignatureHash { SignatureHash(match tx.version { diff --git a/zcash_proofs/src/circuit/sprout/input.rs b/zcash_proofs/src/circuit/sprout/input.rs index 39a2d4170..cadcfcbd8 100644 --- a/zcash_proofs/src/circuit/sprout/input.rs +++ b/zcash_proofs/src/circuit/sprout/input.rs @@ -66,7 +66,7 @@ impl InputNote { let lhs = cur; let rhs = witness_u256( cs.namespace(|| "sibling"), - layer.as_ref().map(|&(ref sibling, _)| &sibling[..]), + layer.as_ref().map(|(sibling, _)| &sibling[..]), )?; // Conditionally swap if cur is right