diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index e62ec9d943..cdacfc36ac 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -804,7 +804,7 @@ pub struct SentTransactionOutput { recipient: Recipient, value: NonNegativeAmount, memo: Option, - sapling_change_to: Option<(AccountId, sapling::Note)>, + change_to: Option<(AccountId, Note)>, } impl SentTransactionOutput { @@ -813,14 +813,14 @@ impl SentTransactionOutput { recipient: Recipient, value: NonNegativeAmount, memo: Option, - sapling_change_to: Option<(AccountId, sapling::Note)>, + change_to: Option<(AccountId, Note)>, ) -> Self { Self { output_index, recipient, value, memo, - sapling_change_to, + change_to, } } @@ -849,9 +849,9 @@ impl SentTransactionOutput { } /// Returns the account to which change (or wallet-internal value in the case of a shielding - /// transaction) was sent, along with the change note. - pub fn sapling_change_to(&self) -> Option<&(AccountId, sapling::Note)> { - self.sapling_change_to.as_ref() + /// transaction) was sent, along with the change or shielding note. + pub fn change_to(&self) -> Option<&(AccountId, Note)> { + self.change_to.as_ref() } } diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index 4509f65014..757aca4760 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -13,6 +13,7 @@ use zcash_primitives::{ zip32::AccountId, }; +use crate::address::UnifiedAddress; use crate::data_api::wallet::input_selection::InputSelectorError; use crate::PoolType; @@ -58,8 +59,12 @@ pub enum Error { /// It is forbidden to provide a memo when constructing a transparent output. MemoForbidden, - /// Attempted to create a spend to an unsupported pool type (currently, Orchard). - UnsupportedPoolType(PoolType), + /// Attempted to create a send change to an unsupported pool. + UnsupportedChangeType(PoolType), + + /// The Unified address provided as a transaction recipient did not contain any receiver types + /// to which the wallet knows how to send funds. + NoSupportedReceivers(Box), /// A note being spent does not correspond to either the internal or external /// full viewing key for an account. @@ -117,7 +122,8 @@ where Error::ScanRequired => write!(f, "Must scan blocks first"), Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e), Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."), - Error::UnsupportedPoolType(t) => write!(f, "Attempted to create spend to an unsupported pool type: {}", t), + Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t), + Error::NoSupportedReceivers(_) => write!(f, "A recipient's unified address not contain any receivers to which the wallet can send funds."), Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n), #[cfg(feature = "transparent-inputs")] diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 34eaca8130..73baae2ce5 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -41,8 +41,7 @@ use super::InputSource; #[cfg(feature = "transparent-inputs")] use { crate::wallet::WalletTransparentOutput, input_selection::ShieldingSelector, - sapling::keys::OutgoingViewingKey, std::convert::Infallible, - zcash_primitives::legacy::TransparentAddress, + std::convert::Infallible, zcash_primitives::legacy::TransparentAddress, }; /// Scans a [`Transaction`] for any information that can be decrypted by the accounts in @@ -547,32 +546,6 @@ where .map_err(Error::DataSource)? .ok_or(Error::KeyNotRecognized)?; - let dfvk = usk.sapling().to_diversifiable_full_viewing_key(); - - // Apply the outgoing viewing key policy. - let external_ovk = match ovk_policy { - OvkPolicy::Sender => Some(dfvk.to_ovk(Scope::External)), - OvkPolicy::Custom(ovk) => Some(ovk), - OvkPolicy::Discard => None, - }; - - let internal_ovk = || { - #[cfg(feature = "transparent-inputs")] - return if proposal.is_shielding() { - Some(OutgoingViewingKey( - usk.transparent() - .to_account_pubkey() - .internal_ovk() - .as_bytes(), - )) - } else { - Some(dfvk.to_ovk(Scope::Internal)) - }; - - #[cfg(not(feature = "transparent-inputs"))] - Some(dfvk.to_ovk(Scope::Internal)) - }; - let (sapling_anchor, sapling_inputs) = proposal.shielded_inputs().map_or_else( || Ok((sapling::Anchor::empty_tree(), vec![])), |inputs| { @@ -695,6 +668,62 @@ where utxos }; + #[cfg(feature = "orchard")] + let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into(); + + #[cfg(feature = "orchard")] + let orchard_external_ovk = match ovk_policy { + OvkPolicy::Sender => Some(orchard_fvk.to_ovk(orchard::keys::Scope::External)), + OvkPolicy::Custom(ovk) => Some(orchard::keys::OutgoingViewingKey::from(ovk)), + OvkPolicy::Discard => None, + }; + + #[cfg(feature = "orchard")] + let orchard_internal_ovk = || { + #[cfg(feature = "transparent-inputs")] + return if proposal.is_shielding() { + Some(orchard::keys::OutgoingViewingKey::from( + usk.transparent() + .to_account_pubkey() + .internal_ovk() + .as_bytes(), + )) + } else { + Some(orchard_fvk.to_ovk(orchard::keys::Scope::Internal)) + }; + + #[cfg(not(feature = "transparent-inputs"))] + Some(orchard_fvk.to_ovk(Scope::Internal)) + }; + + let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key(); + + // Apply the outgoing viewing key policy. + let sapling_external_ovk = match ovk_policy { + OvkPolicy::Sender => Some(sapling_dfvk.to_ovk(Scope::External)), + OvkPolicy::Custom(ovk) => Some(sapling::keys::OutgoingViewingKey(ovk)), + OvkPolicy::Discard => None, + }; + + let sapling_internal_ovk = || { + #[cfg(feature = "transparent-inputs")] + return if proposal.is_shielding() { + Some(sapling::keys::OutgoingViewingKey( + usk.transparent() + .to_account_pubkey() + .internal_ovk() + .as_bytes(), + )) + } else { + Some(sapling_dfvk.to_ovk(Scope::Internal)) + }; + + #[cfg(not(feature = "transparent-inputs"))] + Some(sapling_dfvk.to_ovk(Scope::Internal)) + }; + + #[cfg(feature = "orchard")] + let mut orchard_output_meta = vec![]; let mut sapling_output_meta = vec![]; let mut transparent_output_meta = vec![]; for payment in proposal.transaction_request().payments() { @@ -705,9 +734,28 @@ where .as_ref() .map_or_else(MemoBytes::empty, |m| m.clone()); + #[cfg(feature = "orchard")] + if let Some(orchard_receiver) = ua.orchard() { + builder.add_orchard_output( + orchard_external_ovk.clone(), + *orchard_receiver, + payment.amount.into(), + memo.clone(), + )?; + orchard_output_meta.push(( + Recipient::Unified( + ua.clone(), + PoolType::Shielded(ShieldedProtocol::Orchard), + ), + payment.amount, + Some(memo), + )); + continue; + } + if let Some(sapling_receiver) = ua.sapling() { builder.add_sapling_output( - external_ovk, + sapling_external_ovk, *sapling_receiver, payment.amount, memo.clone(), @@ -720,24 +768,33 @@ where payment.amount, Some(memo), )); - } else if let Some(taddr) = ua.transparent() { + + continue; + } + + if let Some(taddr) = ua.transparent() { if payment.memo.is_some() { return Err(Error::MemoForbidden); } else { builder.add_transparent_output(taddr, payment.amount)?; } - } else { - return Err(Error::UnsupportedPoolType(PoolType::Shielded( - ShieldedProtocol::Orchard, - ))); + + continue; } + + return Err(Error::NoSupportedReceivers(Box::new(ua.clone()))); } Address::Sapling(addr) => { let memo = payment .memo .as_ref() .map_or_else(MemoBytes::empty, |m| m.clone()); - builder.add_sapling_output(external_ovk, *addr, payment.amount, memo.clone())?; + builder.add_sapling_output( + sapling_external_ovk, + *addr, + payment.amount, + memo.clone(), + )?; sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo))); } Address::Transparent(to) => { @@ -758,8 +815,8 @@ where match change_value.output_pool() { ShieldedProtocol::Sapling => { builder.add_sapling_output( - internal_ovk(), - dfvk.change_address().1, + sapling_internal_ovk(), + sapling_dfvk.change_address().1, change_value.value(), memo.clone(), )?; @@ -774,12 +831,27 @@ where } ShieldedProtocol::Orchard => { #[cfg(not(feature = "orchard"))] - return Err(Error::UnsupportedPoolType(PoolType::Shielded( + return Err(Error::UnsupportedChangeType(PoolType::Shielded( ShieldedProtocol::Orchard, ))); #[cfg(feature = "orchard")] - unimplemented!("FIXME: implement Orchard change output creation.") + { + builder.add_orchard_output( + orchard_internal_ovk(), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + change_value.value().into(), + memo.clone(), + )?; + orchard_output_meta.push(( + Recipient::InternalAccount( + account, + PoolType::Shielded(ShieldedProtocol::Orchard), + ), + change_value.value(), + Some(memo), + )) + } } } } @@ -787,7 +859,41 @@ where // Build the transaction with the specified fee rule let build_result = builder.build(OsRng, spend_prover, output_prover, proposal.fee_rule())?; - let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + #[cfg(feature = "orchard")] + let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal); + #[cfg(feature = "orchard")] + let orchard_outputs = + orchard_output_meta + .into_iter() + .enumerate() + .map(|(i, (recipient, value, memo))| { + let output_index = build_result + .orchard_meta() + .output_action_index(i) + .expect("An action should exist in the transaction for each Orchard output."); + + let received_as = if let Recipient::InternalAccount( + account, + PoolType::Shielded(ShieldedProtocol::Orchard), + ) = recipient + { + build_result + .transaction() + .orchard_bundle() + .and_then(|bundle| { + bundle + .decrypt_output_with_key(output_index, &orchard_internal_ivk) + .map(|(note, _, _)| (account, Note::Orchard(note))) + }) + } else { + None + }; + + SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as) + }); + + let sapling_internal_ivk = + PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal)); let sapling_outputs = sapling_output_meta .into_iter() @@ -808,14 +914,14 @@ where .sapling_bundle() .and_then(|bundle| { try_sapling_note_decryption( - &internal_ivk, + &sapling_internal_ivk, &bundle.shielded_outputs()[output_index], consensus::sapling_zip212_enforcement( params, proposal.min_target_height(), ), ) - .map(|(note, _, _)| (account, note)) + .map(|(note, _, _)| (account, Note::Sapling(note))) }) } else { None @@ -847,12 +953,18 @@ where ) }); + let mut outputs = vec![]; + #[cfg(feature = "orchard")] + outputs.extend(orchard_outputs); + outputs.extend(sapling_outputs); + outputs.extend(transparent_outputs); + wallet_db .store_sent_tx(&SentTransaction { tx: build_result.transaction(), created: time::OffsetDateTime::now_utc(), account, - outputs: sapling_outputs.chain(transparent_outputs).collect(), + outputs, fee_amount: Amount::from(proposal.balance().fee_required()), #[cfg(feature = "transparent-inputs")] utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(), diff --git a/zcash_client_backend/src/fees/common.rs b/zcash_client_backend/src/fees/common.rs index aba4fe3d82..6d0102b0b0 100644 --- a/zcash_client_backend/src/fees/common.rs +++ b/zcash_client_backend/src/fees/common.rs @@ -96,9 +96,8 @@ where // Sapling outputs, so that we avoid pool-crossing. (ShieldedProtocol::Sapling, 1, 0) } else { - // For all other transactions, send change to Sapling. - // FIXME: Change this to Orchard once Orchard outputs are enabled. - (ShieldedProtocol::Sapling, 1, 0) + // For all other transactions, send change to Orchard. + (ShieldedProtocol::Orchard, 0, 1) }; #[cfg(not(feature = "orchard"))] let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1); diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 851bc31ad9..54d81f71ef 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -389,7 +389,7 @@ pub enum OvkPolicy { /// /// Transaction outputs will be decryptable by the recipients, and whoever controls /// the provided outgoing viewing key. - Custom(sapling::keys::OutgoingViewingKey), + Custom([u8; 32]), /// Use no outgoing viewing key. Transaction outputs will be decryptable by their /// recipients, but not by the sender. diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 95ea752dfc..58320de97e 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -43,7 +43,7 @@ jubjub.workspace = true secrecy.workspace = true # - Shielded protocols -orchard.workspace = true +orchard = { workspace = true, optional = true } sapling.workspace = true # - Note commitment trees @@ -84,7 +84,7 @@ multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"] ## Enables support for storing data related to the sending and receiving of ## Orchard funds. -orchard = ["zcash_client_backend/orchard"] +orchard = ["dep:orchard", "zcash_client_backend/orchard"] ## Exposes APIs that are useful for testing, such as `proptest` strategies. test-dependencies = [ diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 62ee3c6bc4..5f8fa71f25 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -718,21 +718,28 @@ impl WalletWrite for WalletDb output, )?; - if let Some((account, note)) = output.sapling_change_to() { - wallet::sapling::put_received_note( - wdb.conn.0, - &DecryptedOutput { - index: output.output_index(), - note: note.clone(), - account: *account, - memo: output - .memo() - .map_or_else(MemoBytes::empty, |memo| memo.clone()), - transfer_type: TransferType::WalletInternal, - }, - tx_ref, - None, - )?; + match output.change_to() { + Some((account, Note::Sapling(note))) => { + wallet::sapling::put_received_note( + wdb.conn.0, + &DecryptedOutput { + index: output.output_index(), + note: note.clone(), + account: *account, + memo: output + .memo() + .map_or_else(MemoBytes::empty, |memo| memo.clone()), + transfer_type: TransferType::WalletInternal, + }, + tx_ref, + None, + )?; + } + #[cfg(feature = "orchard")] + Some((_account, Note::Orchard(_note))) => { + todo!(); + } + None => (), } }