From ed6016857e1d4100a38362c9b5d54d81bf711ae9 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 7 Jun 2022 11:35:53 +0000 Subject: [PATCH] zcash_client_backend: Add `RecipientAddress::Unified` --- zcash_client_backend/CHANGELOG.md | 3 + zcash_client_backend/Cargo.toml | 7 +- zcash_client_backend/src/address.rs | 178 +++++++++++++++++++- zcash_client_backend/src/data_api/wallet.rs | 13 +- zcash_client_backend/src/zip321.rs | 22 ++- zcash_client_sqlite/src/lib.rs | 11 ++ 6 files changed, 226 insertions(+), 8 deletions(-) diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index be24954fa..af2455155 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -25,6 +25,8 @@ and this library adheres to Rust's notion of of a `zcash_client_backend::zip321::TransactionRequest` value. This facilitates the implementation of ZIP 321 support in wallets and provides substantially greater flexibility in transaction creation. +- `zcash_client_backend::address`: + - `RecipientAddress::Unified` - `zcash_client_backend::proto`: - `actions` field on `compact_formats::CompactTx` - `compact_formats::CompactOrchardAction` @@ -35,6 +37,7 @@ and this library adheres to Rust's notion of - New experimental APIs that should be considered unstable, and are likely to be modified and/or moved to a different module in a future release: + - `zcash_client_backend::address::UnifiedAddress` - `zcash_client_backend::keys::{UnifiedSpendingKey`, `UnifiedFullViewingKey`} - `zcash_client_backend::encoding::AddressCodec` - `zcash_client_backend::encoding::encode_payment_address` diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 9d16ebfea..9666101e0 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -24,6 +24,7 @@ hdwallet = { version = "0.3.1", optional = true } jubjub = "0.9" log = "0.4" nom = "7" +orchard = "0.1" percent-encoding = "2.1.0" proptest = { version = "1.0.0", optional = true } protobuf = "~2.27.1" # MSRV 1.52.1 @@ -48,7 +49,11 @@ zcash_proofs = { version = "0.6", path = "../zcash_proofs" } [features] transparent-inputs = ["ripemd", "hdwallet", "sha2", "secp256k1", "zcash_primitives/transparent-inputs"] -test-dependencies = ["proptest", "zcash_primitives/test-dependencies"] +test-dependencies = [ + "proptest", + "orchard/test-dependencies", + "zcash_primitives/test-dependencies", +] [lib] bench = false diff --git a/zcash_client_backend/src/address.rs b/zcash_client_backend/src/address.rs index 69d2f0870..cfd8960f8 100644 --- a/zcash_client_backend/src/address.rs +++ b/zcash_client_backend/src/address.rs @@ -1,6 +1,11 @@ //! Structs for handling supported address types. -use zcash_address::{ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress}; +use std::convert::TryFrom; + +use zcash_address::{ + unified::{self, Container, Encoding}, + ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, +}; use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress}; fn params_to_network(params: &P) -> Network { @@ -13,12 +18,134 @@ fn params_to_network(params: &P) -> Network { } } +/// A Unified Address. +#[derive(Clone, Debug, PartialEq)] +pub struct UnifiedAddress { + orchard: Option, + sapling: Option, + transparent: Option, + unknown: Vec<(u32, Vec)>, +} + +impl TryFrom for UnifiedAddress { + type Error = &'static str; + + fn try_from(ua: unified::Address) -> Result { + let mut orchard = None; + let mut sapling = None; + let mut transparent = None; + + // We can use as-parsed order here for efficiency, because we're breaking out the + // receivers we support from the unknown receivers. + let unknown = ua + .items_as_parsed() + .iter() + .filter_map(|receiver| match receiver { + unified::Receiver::Orchard(data) => { + Option::from(orchard::Address::from_raw_address_bytes(data)) + .ok_or("Invalid Orchard receiver in Unified Address") + .map(|addr| { + orchard = Some(addr); + None + }) + .transpose() + } + unified::Receiver::Sapling(data) => PaymentAddress::from_bytes(data) + .ok_or("Invalid Sapling receiver in Unified Address") + .map(|pa| { + sapling = Some(pa); + None + }) + .transpose(), + unified::Receiver::P2pkh(data) => { + transparent = Some(TransparentAddress::PublicKey(*data)); + None + } + unified::Receiver::P2sh(data) => { + transparent = Some(TransparentAddress::Script(*data)); + None + } + unified::Receiver::Unknown { typecode, data } => { + Some(Ok((*typecode, data.clone()))) + } + }) + .collect::>()?; + + Ok(Self { + orchard, + sapling, + transparent, + unknown, + }) + } +} + +impl UnifiedAddress { + /// Constructs a Unified Address from a given set of receivers. + /// + /// Returns `None` if the receivers would produce an invalid Unified Address (namely, + /// if no shielded receiver is provided). + pub fn from_receivers( + orchard: Option, + sapling: Option, + transparent: Option, + ) -> Option { + if orchard.is_some() || sapling.is_some() { + Some(Self { + orchard, + sapling, + transparent, + unknown: vec![], + }) + } else { + // UAs require at least one shielded receiver. + None + } + } + + /// Returns the Sapling receiver within this Unified Address, if any. + pub fn sapling(&self) -> Option<&PaymentAddress> { + self.sapling.as_ref() + } + + fn to_address(&self, net: Network) -> ZcashAddress { + let ua = unified::Address::try_from_items( + self.unknown + .iter() + .map(|(typecode, data)| unified::Receiver::Unknown { + typecode: *typecode, + data: data.clone(), + }) + .chain(self.transparent.as_ref().map(|taddr| match taddr { + TransparentAddress::PublicKey(data) => unified::Receiver::P2pkh(*data), + TransparentAddress::Script(data) => unified::Receiver::P2sh(*data), + })) + .chain( + self.sapling + .as_ref() + .map(|pa| pa.to_bytes()) + .map(unified::Receiver::Sapling), + ) + .chain( + self.orchard + .as_ref() + .map(|addr| addr.to_raw_address_bytes()) + .map(unified::Receiver::Orchard), + ) + .collect(), + ) + .expect("UnifiedAddress should only be constructed safely"); + ZcashAddress::from_unified(net, ua) + } +} + /// An address that funds can be sent to. // TODO: rename to ParsedAddress #[derive(Debug, PartialEq, Clone)] pub enum RecipientAddress { Shielded(PaymentAddress), Transparent(TransparentAddress), + Unified(UnifiedAddress), } impl From for RecipientAddress { @@ -33,6 +160,12 @@ impl From for RecipientAddress { } } +impl From for RecipientAddress { + fn from(addr: UnifiedAddress) -> Self { + RecipientAddress::Unified(addr) + } +} + impl TryFromRawAddress for RecipientAddress { type Error = &'static str; @@ -41,6 +174,14 @@ impl TryFromRawAddress for RecipientAddress { Ok(pa.into()) } + fn try_from_raw_unified( + ua: zcash_address::unified::Address, + ) -> Result> { + UnifiedAddress::try_from(ua) + .map_err(ConversionError::User) + .map(RecipientAddress::from) + } + fn try_from_raw_transparent_p2pkh( data: [u8; 20], ) -> Result> { @@ -69,7 +210,42 @@ impl RecipientAddress { } TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data), }, + RecipientAddress::Unified(ua) => ua.to_address(net), } .to_string() } } + +#[cfg(test)] +mod tests { + use zcash_primitives::{consensus::MAIN_NETWORK, zip32::ExtendedFullViewingKey}; + + use super::{RecipientAddress, UnifiedAddress}; + use crate::keys::sapling; + + #[test] + fn ua_round_trip() { + let orchard = { + let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap(); + let fvk = orchard::keys::FullViewingKey::from(&sk); + Some(fvk.address_at(0u32, orchard::keys::Scope::External)) + }; + + let sapling = { + let extsk = sapling::spending_key(&[0; 32], 0, 0.into()); + let extfvk = ExtendedFullViewingKey::from(&extsk); + Some(extfvk.default_address().1) + }; + + let transparent = { None }; + + let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap(); + + let addr = RecipientAddress::Unified(ua); + let addr_str = addr.encode(&MAIN_NETWORK); + assert_eq!( + RecipientAddress::decode(&MAIN_NETWORK, &addr_str), + Some(addr) + ); + } +} diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 0ba8fe107..fee32c2dd 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -334,6 +334,16 @@ where for payment in request.payments() { match &payment.recipient_address { + RecipientAddress::Unified(ua) => builder + .add_sapling_output( + ovk, + ua.sapling() + .expect("TODO: Add Orchard support to builder") + .clone(), + payment.amount, + payment.memo.clone().unwrap_or_else(MemoBytes::empty), + ) + .map_err(Error::Builder), RecipientAddress::Shielded(to) => builder .add_sapling_output( ovk, @@ -359,7 +369,8 @@ where let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| { let idx = match &payment.recipient_address { // Sapling outputs are shuffled, so we need to look up where the output ended up. - RecipientAddress::Shielded(_) => + // TODO: When we add Orchard support, we will need to trial-decrypt to find them. + RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."), RecipientAddress::Transparent(addr) => { let script = addr.script(); diff --git a/zcash_client_backend/src/zip321.rs b/zcash_client_backend/src/zip321.rs index 2d575844d..c14a5dcdb 100644 --- a/zcash_client_backend/src/zip321.rs +++ b/zcash_client_backend/src/zip321.rs @@ -452,7 +452,9 @@ mod parse { match v { Param::Amount(a) => payment.amount = a, Param::Memo(m) => match payment.recipient_address { - RecipientAddress::Shielded(_) => payment.memo = Some(m), + RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => { + payment.memo = Some(m) + } RecipientAddress::Transparent(_) => { return Err(Zip321Error::TransparentMemo(i)) } @@ -646,14 +648,24 @@ pub mod testing { transaction::components::amount::testing::arb_nonnegative_amount, }; - use crate::address::RecipientAddress; + use crate::address::{RecipientAddress, UnifiedAddress}; use super::{MemoBytes, Payment, TransactionRequest}; + prop_compose! { + fn arb_unified_addr()( + sapling in arb_shielded_addr(), + transparent in option::of(arb_transparent_addr()), + ) -> UnifiedAddress { + UnifiedAddress::from_receivers(None, Some(sapling), transparent).unwrap() + } + } + pub fn arb_addr() -> impl Strategy { prop_oneof![ arb_shielded_addr().prop_map(RecipientAddress::Shielded), arb_transparent_addr().prop_map(RecipientAddress::Transparent), + arb_unified_addr().prop_map(RecipientAddress::Unified), ] } @@ -676,15 +688,15 @@ pub mod testing { other_params in btree_map(VALID_PARAMNAME, any::(), 0..3), ) -> Payment { - let is_sapling = match recipient_address { + let is_shielded = match recipient_address { RecipientAddress::Transparent(_) => false, - RecipientAddress::Shielded(_) => true, + RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => true, }; Payment { recipient_address, amount, - memo: memo.filter(|_| is_sapling), + memo: memo.filter(|_| is_shielded), label, message, other_params: other_params.into_iter().collect(), diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index c8b939dea..64a2752d9 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -642,6 +642,17 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { for output in &sent_tx.outputs { match output.recipient_address { + // TODO: Store the entire UA, not just the Sapling component. + // This will require more info about the output index. + RecipientAddress::Unified(ua) => wallet::insert_sent_note( + up, + tx_ref, + output.output_index, + sent_tx.account, + ua.sapling().expect("TODO: Add Orchard support"), + output.value, + output.memo.as_ref(), + )?, RecipientAddress::Shielded(addr) => wallet::insert_sent_note( up, tx_ref,