diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 4c4d79034..55321a578 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -17,6 +17,8 @@ and this library adheres to Rust's notion of - `BlockMetadata::orchard_tree_size` - `ScannedBlock::orchard` - `ScannedBlockCommitments::orchard` + - `ORCHARD_SHARD_HEIGHT` + - `BlockMetadata::orchard_tree_size` - `zcash_client_backend::fees::orchard` - `zcash_client_backend::fees::ChangeValue::orchard` - `zcash_client_backend::wallet`: @@ -29,6 +31,10 @@ and this library adheres to Rust's notion of - Changes to the `WalletRead` trait: - Added `get_orchard_nullifiers` - `ShieldedProtocol` has a new `Orchard` variant. + - `WalletCommitmentTrees` has new members when the `orchard` feature is enabled: + - `type OrchardShardStore` + - `fn with_orchard_tree_mut` + - `fn put_orchard_subtree_roots` - `zcash_client_backend::fees`: - Arguments to `ChangeStrategy::compute_balance` have changed. diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index cb32cbe05..33c86da8c 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -8,7 +8,7 @@ use std::{ }; use incrementalmerkletree::{frontier::Frontier, Retention}; -use sapling::{Node, NOTE_COMMITMENT_TREE_DEPTH}; +use sapling; use secrecy::SecretVec; use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree}; use zcash_primitives::{ @@ -51,6 +51,13 @@ pub mod wallet; /// `lightwalletd` when using the `GetSubtreeRoots` GRPC call. pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2; +/// The height of subtree roots in the Orchard note commitment tree. +/// +/// This conforms to the structure of subtree data returned by +/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call. +#[cfg(feature = "orchard")] +pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2; + /// An enumeration of constraints that can be applied when querying for nullifiers for notes /// belonging to the wallet. pub enum NullifierQuery { @@ -870,7 +877,7 @@ impl SentTransactionOutput { #[derive(Clone, Debug)] pub struct AccountBirthday { height: BlockHeight, - sapling_frontier: Frontier, + sapling_frontier: Frontier, recover_until: Option, } @@ -911,7 +918,7 @@ impl AccountBirthday { #[cfg(feature = "test-dependencies")] pub fn from_parts( height: BlockHeight, - sapling_frontier: Frontier, + sapling_frontier: Frontier, recover_until: Option, ) -> Self { Self { @@ -944,7 +951,9 @@ impl AccountBirthday { /// Returns the Sapling note commitment tree frontier as of the end of the block at /// [`Self::height`]. - pub fn sapling_frontier(&self) -> &Frontier { + pub fn sapling_frontier( + &self, + ) -> &Frontier { &self.sapling_frontier } @@ -1081,13 +1090,15 @@ pub trait WalletWrite: WalletRead { /// also provide operations related to Orchard note commitment trees in the future. pub trait WalletCommitmentTrees { type Error; + /// The type of the backing [`ShardStore`] for the Sapling note commitment tree. type SaplingShardStore<'a>: ShardStore< H = sapling::Node, CheckpointId = BlockHeight, Error = Self::Error, >; - /// + /// Evaluates the given callback function with a reference to the Sapling + /// note commitment tree maintained by the wallet. fn with_sapling_tree_mut(&mut self, callback: F) -> Result where for<'a> F: FnMut( @@ -1099,12 +1110,48 @@ pub trait WalletCommitmentTrees { ) -> Result, E: From>; - /// Adds a sequence of note commitment tree subtree roots to the data store. + /// Adds a sequence of Sapling note commitment tree subtree roots to the data store. + /// + /// Each such value should be the Merkle root of a subtree of the Sapling note commitment tree + /// containing 2^[`SAPLING_SHARD_HEIGHT`] note commitments. fn put_sapling_subtree_roots( &mut self, start_index: u64, roots: &[CommitmentTreeRoot], ) -> Result<(), ShardTreeError>; + + /// The type of the backing [`ShardStore`] for the Orchard note commitment tree. + #[cfg(feature = "orchard")] + type OrchardShardStore<'a>: ShardStore< + H = orchard::tree::MerkleHashOrchard, + CheckpointId = BlockHeight, + Error = Self::Error, + >; + + /// Evaluates the given callback function with a reference to the Orchard + /// note commitment tree maintained by the wallet. + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>; + + /// Adds a sequence of Orchard note commitment tree subtree roots to the data store. + /// + /// Each such value should be the Merkle root of a subtree of the Orchard note commitment tree + /// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments. + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError>; } #[cfg(feature = "test-dependencies")] @@ -1138,6 +1185,9 @@ pub mod testing { #[cfg(feature = "transparent-inputs")] use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress}; + #[cfg(feature = "orchard")] + use super::ORCHARD_SHARD_HEIGHT; + pub struct MockWalletDb { pub network: Network, pub sapling_tree: ShardTree< @@ -1145,6 +1195,12 @@ pub mod testing { { SAPLING_SHARD_HEIGHT * 2 }, SAPLING_SHARD_HEIGHT, >, + #[cfg(feature = "orchard")] + pub orchard_tree: ShardTree< + MemoryShardStore, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, } impl MockWalletDb { @@ -1152,6 +1208,8 @@ pub mod testing { Self { network, sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100), + #[cfg(feature = "orchard")] + orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100), } } } @@ -1406,5 +1464,43 @@ pub mod testing { 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/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index b699c0ebc..910991b38 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -682,28 +682,24 @@ where let sapling_inputs = inputs .notes() .iter() - .map(|selected| { - match selected.note() { - Note::Sapling(note) => { - let key = match selected.spending_key_scope() { - Scope::External => usk.sapling().clone(), - Scope::Internal => usk.sapling().derive_internal(), - }; + .filter_map(|selected| match selected.note() { + Note::Sapling(note) => { + let key = match selected.spending_key_scope() { + Scope::External => usk.sapling().clone(), + Scope::Internal => usk.sapling().derive_internal(), + }; - let merkle_path = sapling_tree.witness_at_checkpoint_id_caching( + sapling_tree + .witness_at_checkpoint_id_caching( selected.note_commitment_tree_position(), &inputs.anchor_height(), - )?; - - Ok((key, note, merkle_path)) - } - #[cfg(feature = "orchard")] - Note::Orchard(_) => { - // FIXME: Implement this once `Proposal` has been refactored to - // include Orchard notes. - panic!("Orchard spends are not yet supported"); - } + ) + .map(|merkle_path| Some((key, note, merkle_path))) + .map_err(Error::from) + .transpose() } + #[cfg(feature = "orchard")] + Note::Orchard(_) => None, }) .collect::, Error<_, _, _, _>>>()?; @@ -712,6 +708,39 @@ where }, )?; + #[cfg(feature = "orchard")] + let (orchard_anchor, orchard_inputs) = proposal_step.shielded_inputs().map_or_else( + || Ok((Some(orchard::Anchor::empty_tree()), vec![])), + |inputs| { + wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| { + let anchor = orchard_tree + .root_at_checkpoint_id(&inputs.anchor_height())? + .into(); + + let orchard_inputs = inputs + .notes() + .iter() + .filter_map(|selected| match selected.note() { + #[cfg(feature = "orchard")] + Note::Orchard(note) => orchard_tree + .witness_at_checkpoint_id_caching( + selected.note_commitment_tree_position(), + &inputs.anchor_height(), + ) + .map(|merkle_path| Some((note, merkle_path))) + .map_err(Error::from) + .transpose(), + Note::Sapling(_) => None, + }) + .collect::, Error<_, _, _, _>>>()?; + + Ok((Some(anchor), orchard_inputs)) + }) + }, + )?; + #[cfg(not(feature = "orchard"))] + let orchard_anchor = None; + // Create the transaction. The type of the proposal ensures that there // are no possible transparent inputs, so we ignore those let mut builder = Builder::new( @@ -719,12 +748,17 @@ where min_target_height, BuildConfig::Standard { sapling_anchor: Some(sapling_anchor), - orchard_anchor: None, + orchard_anchor, }, ); - for (key, note, merkle_path) in sapling_inputs.into_iter() { - builder.add_sapling_spend(&key, note.clone(), merkle_path)?; + for (sapling_key, sapling_note, merkle_path) in sapling_inputs.into_iter() { + builder.add_sapling_spend(&sapling_key, sapling_note.clone(), merkle_path)?; + } + + #[cfg(feature = "orchard")] + for (orchard_note, merkle_path) in orchard_inputs.into_iter() { + builder.add_orchard_spend(usk.orchard(), *orchard_note, merkle_path.into())?; } #[cfg(feature = "transparent-inputs")] diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index e1445dfa1..d15b346dd 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -74,6 +74,9 @@ use zcash_client_backend::{ use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore}; +#[cfg(feature = "orchard")] +use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT; + #[cfg(feature = "transparent-inputs")] use { zcash_client_backend::wallet::TransparentAddressMetadata, @@ -805,6 +808,37 @@ impl WalletCommitmentTrees for WalletDb = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, _callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + todo!() + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + _start_index: u64, + _roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + todo!() + } } impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb, P> { @@ -845,6 +879,37 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb = SqliteShardStore< + &'a rusqlite::Transaction<'a>, + orchard::tree::MerkleHashOrchard, + ORCHARD_SHARD_HEIGHT, + >; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, _callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + todo!() + } + + #[cfg(feature = "orchard")] + fn put_orchard_subtree_roots( + &mut self, + _start_index: u64, + _roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + todo!() + } } /// A handle for the SQLite block source.