Merge pull request #1168 from nuttycom/zcash_keys_sapling_feature

zcash_keys: Add `sapling` and `transparent` feature flags.
This commit is contained in:
Kris Nuttycombe 2024-02-26 19:16:07 -07:00 committed by GitHub
commit 5ed788dc79
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 418 additions and 233 deletions

View File

@ -27,7 +27,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
zcash_address.workspace = true zcash_address.workspace = true
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_keys.workspace = true zcash_keys = { workspace = true, features = ["sapling"] }
zcash_note_encryption.workspace = true zcash_note_encryption.workspace = true
zcash_primitives.workspace = true zcash_primitives.workspace = true
zip32.workspace = true zip32.workspace = true

View File

@ -140,7 +140,7 @@ where
/// }; /// };
/// use zcash_proofs::prover::LocalTxProver; /// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{ /// use zcash_client_backend::{
/// keys::UnifiedSpendingKey, /// keys::{UnifiedSpendingKey, UnifiedAddressRequest},
/// data_api::{wallet::create_spend_to_address, error::Error, testing}, /// data_api::{wallet::create_spend_to_address, error::Error, testing},
/// wallet::OvkPolicy, /// wallet::OvkPolicy,
/// }; /// };
@ -166,8 +166,9 @@ where
/// }; /// };
/// ///
/// let account = AccountId::from(0); /// let account = AccountId::from(0);
/// let req = UnifiedAddressRequest::new(false, true, true);
/// let usk = UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0; 32][..], account).unwrap(); /// let usk = UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0; 32][..], account).unwrap();
/// let to = usk.to_unified_full_viewing_key().default_address().0.into(); /// let to = usk.to_unified_full_viewing_key().default_address(req).0.into();
/// ///
/// let mut db_read = testing::MockWalletDb { /// let mut db_read = testing::MockWalletDb {
/// network: Network::TestNetwork /// network: Network::TestNetwork

View File

@ -22,7 +22,7 @@ rustdoc-args = ["--cfg", "docsrs"]
zcash_address.workspace = true zcash_address.workspace = true
zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] } zcash_client_backend = { workspace = true, features = ["unstable-serialization", "unstable-spanning-tree"] }
zcash_encoding.workspace = true zcash_encoding.workspace = true
zcash_keys = { workspace = true, features = ["orchard"] } zcash_keys = { workspace = true, features = ["orchard", "sapling"] }
zcash_primitives.workspace = true zcash_primitives.workspace = true
# Dependencies exposed in a public API: # Dependencies exposed in a public API:
@ -90,7 +90,7 @@ multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"]
## Enables support for storing data related to the sending and receiving of ## Enables support for storing data related to the sending and receiving of
## Orchard funds. ## Orchard funds.
orchard = ["dep:orchard", "zcash_client_backend/orchard"] orchard = ["dep:orchard", "zcash_client_backend/orchard", "zcash_keys/orchard"]
## Exposes APIs that are useful for testing, such as `proptest` strategies. ## Exposes APIs that are useful for testing, such as `proptest` strategies.
test-dependencies = [ test-dependencies = [
@ -100,8 +100,12 @@ test-dependencies = [
"incrementalmerkletree/test-dependencies", "incrementalmerkletree/test-dependencies",
] ]
## Enables receiving transparent funds and shielding them. ## Enables receiving transparent funds and sending to transparent recipients
transparent-inputs = ["dep:hdwallet", "zcash_client_backend/transparent-inputs"] transparent-inputs = [
"dep:hdwallet",
"zcash_keys/transparent-inputs",
"zcash_client_backend/transparent-inputs"
]
#! ### Experimental features #! ### Experimental features

View File

@ -48,7 +48,7 @@ subtle.workspace = true
bls12_381.workspace = true bls12_381.workspace = true
group.workspace = true group.workspace = true
orchard = { workspace = true, optional = true } orchard = { workspace = true, optional = true }
sapling.workspace = true sapling = { workspace = true, optional = true }
# - Test dependencies # - Test dependencies
proptest = { workspace = true, optional = true } proptest = { workspace = true, optional = true }
@ -75,6 +75,9 @@ transparent-inputs = ["dep:hdwallet", "zcash_primitives/transparent-inputs"]
## Enables use of Orchard key parts and addresses ## Enables use of Orchard key parts and addresses
orchard = ["dep:orchard"] orchard = ["dep:orchard"]
## Enables use of Sapling key parts and addresses
sapling = ["dep:sapling"]
## Exposes APIs that are useful for testing, such as `proptest` strategies. ## Exposes APIs that are useful for testing, such as `proptest` strategies.
test-dependencies = [ test-dependencies = [
"dep:proptest", "dep:proptest",

View File

@ -1,17 +1,20 @@
//! Structs for handling supported address types. //! Structs for handling supported address types.
use sapling::PaymentAddress;
use zcash_address::{ use zcash_address::{
unified::{self, Container, Encoding, Typecode}, unified::{self, Container, Encoding, Typecode},
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress, ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
}; };
use zcash_primitives::{consensus, legacy::TransparentAddress}; use zcash_primitives::{consensus, legacy::TransparentAddress};
#[cfg(feature = "sapling")]
use sapling::PaymentAddress;
/// A Unified Address. /// A Unified Address.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnifiedAddress { pub struct UnifiedAddress {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Option<orchard::Address>, orchard: Option<orchard::Address>,
#[cfg(feature = "sapling")]
sapling: Option<PaymentAddress>, sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>, transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>, unknown: Vec<(u32, Vec<u8>)>,
@ -23,53 +26,62 @@ impl TryFrom<unified::Address> for UnifiedAddress {
fn try_from(ua: unified::Address) -> Result<Self, Self::Error> { fn try_from(ua: unified::Address) -> Result<Self, Self::Error> {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let mut orchard = None; let mut orchard = None;
#[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
let mut transparent = None; let mut transparent = None;
let mut unknown: Vec<(u32, Vec<u8>)> = vec![];
// We can use as-parsed order here for efficiency, because we're breaking out the // We can use as-parsed order here for efficiency, because we're breaking out the
// receivers we support from the unknown receivers. // receivers we support from the unknown receivers.
let unknown = ua for item in ua.items_as_parsed() {
.items_as_parsed() match item {
.iter()
.filter_map(|receiver| match receiver {
#[cfg(feature = "orchard")]
unified::Receiver::Orchard(data) => { unified::Receiver::Orchard(data) => {
Option::from(orchard::Address::from_raw_address_bytes(data)) #[cfg(feature = "orchard")]
.ok_or("Invalid Orchard receiver in Unified Address") {
.map(|addr| { orchard = Some(
orchard = Some(addr); Option::from(orchard::Address::from_raw_address_bytes(data))
None .ok_or("Invalid Orchard receiver in Unified Address")?,
}) );
.transpose() }
#[cfg(not(feature = "orchard"))]
{
unknown.push((unified::Typecode::Orchard.into(), data.to_vec()));
}
} }
#[cfg(not(feature = "orchard"))]
unified::Receiver::Orchard(data) => { unified::Receiver::Sapling(data) => {
Some(Ok((unified::Typecode::Orchard.into(), data.to_vec()))) #[cfg(feature = "sapling")]
{
sapling = Some(
PaymentAddress::from_bytes(data)
.ok_or("Invalid Sapling receiver in Unified Address")?,
);
}
#[cfg(not(feature = "sapling"))]
{
unknown.push((unified::Typecode::Sapling.into(), data.to_vec()));
}
} }
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) => { unified::Receiver::P2pkh(data) => {
transparent = Some(TransparentAddress::PublicKeyHash(*data)); transparent = Some(TransparentAddress::PublicKeyHash(*data));
None
} }
unified::Receiver::P2sh(data) => { unified::Receiver::P2sh(data) => {
transparent = Some(TransparentAddress::ScriptHash(*data)); transparent = Some(TransparentAddress::ScriptHash(*data));
None
} }
unified::Receiver::Unknown { typecode, data } => { unified::Receiver::Unknown { typecode, data } => {
Some(Ok((*typecode, data.clone()))) unknown.push((*typecode, data.clone()));
} }
}) }
.collect::<Result<_, _>>()?; }
Ok(Self { Ok(Self {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
#[cfg(feature = "sapling")]
sapling, sapling,
transparent, transparent,
unknown, unknown,
@ -84,18 +96,25 @@ impl UnifiedAddress {
/// if no shielded receiver is provided). /// if no shielded receiver is provided).
pub fn from_receivers( pub fn from_receivers(
#[cfg(feature = "orchard")] orchard: Option<orchard::Address>, #[cfg(feature = "orchard")] orchard: Option<orchard::Address>,
sapling: Option<PaymentAddress>, #[cfg(feature = "sapling")] sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>, transparent: Option<TransparentAddress>,
// TODO: Add handling for address metadata items.
) -> Option<Self> { ) -> Option<Self> {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let has_orchard = orchard.is_some(); let has_orchard = orchard.is_some();
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
let has_orchard = false; let has_orchard = false;
if has_orchard || sapling.is_some() { #[cfg(feature = "sapling")]
let has_sapling = sapling.is_some();
#[cfg(not(feature = "sapling"))]
let has_sapling = false;
if has_orchard || has_sapling {
Some(Self { Some(Self {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
#[cfg(feature = "sapling")]
sapling, sapling,
transparent, transparent,
unknown: vec![], unknown: vec![],
@ -128,6 +147,7 @@ impl UnifiedAddress {
} }
/// Returns the Sapling receiver within this Unified Address, if any. /// Returns the Sapling receiver within this Unified Address, if any.
#[cfg(feature = "sapling")]
pub fn sapling(&self) -> Option<&PaymentAddress> { pub fn sapling(&self) -> Option<&PaymentAddress> {
self.sapling.as_ref() self.sapling.as_ref()
} }
@ -148,36 +168,37 @@ impl UnifiedAddress {
} }
fn to_address(&self, net: Network) -> ZcashAddress { fn to_address(&self, net: Network) -> ZcashAddress {
#[cfg(feature = "orchard")] let items = self
let orchard_receiver = self .unknown
.orchard .iter()
.as_ref() .map(|(typecode, data)| unified::Receiver::Unknown {
.map(|addr| addr.to_raw_address_bytes()) typecode: *typecode,
.map(unified::Receiver::Orchard); data: data.clone(),
#[cfg(not(feature = "orchard"))] });
let orchard_receiver = None;
let ua = unified::Address::try_from_items( #[cfg(feature = "orchard")]
self.unknown let items = items.chain(
.iter() self.orchard
.map(|(typecode, data)| unified::Receiver::Unknown { .as_ref()
typecode: *typecode, .map(|addr| addr.to_raw_address_bytes())
data: data.clone(), .map(unified::Receiver::Orchard),
}) );
.chain(self.transparent.as_ref().map(|taddr| match taddr {
TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data), #[cfg(feature = "sapling")]
TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data), let items = items.chain(
})) self.sapling
.chain( .as_ref()
self.sapling .map(|pa| pa.to_bytes())
.as_ref() .map(unified::Receiver::Sapling),
.map(|pa| pa.to_bytes()) );
.map(unified::Receiver::Sapling),
) let items = items.chain(self.transparent.as_ref().map(|taddr| match taddr {
.chain(orchard_receiver) TransparentAddress::PublicKeyHash(data) => unified::Receiver::P2pkh(*data),
.collect(), TransparentAddress::ScriptHash(data) => unified::Receiver::P2sh(*data),
) }));
.expect("UnifiedAddress should only be constructed safely");
let ua = unified::Address::try_from_items(items.collect())
.expect("UnifiedAddress should only be constructed safely");
ZcashAddress::from_unified(net, ua) ZcashAddress::from_unified(net, ua)
} }
@ -209,11 +230,13 @@ impl UnifiedAddress {
/// An address that funds can be sent to. /// An address that funds can be sent to.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum Address { pub enum Address {
#[cfg(feature = "sapling")]
Sapling(PaymentAddress), Sapling(PaymentAddress),
Transparent(TransparentAddress), Transparent(TransparentAddress),
Unified(UnifiedAddress), Unified(UnifiedAddress),
} }
#[cfg(feature = "sapling")]
impl From<PaymentAddress> for Address { impl From<PaymentAddress> for Address {
fn from(addr: PaymentAddress) -> Self { fn from(addr: PaymentAddress) -> Self {
Address::Sapling(addr) Address::Sapling(addr)
@ -235,6 +258,7 @@ impl From<UnifiedAddress> for Address {
impl TryFromRawAddress for Address { impl TryFromRawAddress for Address {
type Error = &'static str; type Error = &'static str;
#[cfg(feature = "sapling")]
fn try_from_raw_sapling(data: [u8; 43]) -> Result<Self, ConversionError<Self::Error>> { fn try_from_raw_sapling(data: [u8; 43]) -> Result<Self, ConversionError<Self::Error>> {
let pa = PaymentAddress::from_bytes(&data).ok_or("Invalid Sapling payment address")?; let pa = PaymentAddress::from_bytes(&data).ok_or("Invalid Sapling payment address")?;
Ok(pa.into()) Ok(pa.into())
@ -270,6 +294,7 @@ impl Address {
let net = params.address_network().expect("Unrecognized network"); let net = params.address_network().expect("Unrecognized network");
match self { match self {
#[cfg(feature = "sapling")]
Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()), Address::Sapling(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()),
Address::Transparent(addr) => match addr { Address::Transparent(addr) => match addr {
TransparentAddress::PublicKeyHash(data) => { TransparentAddress::PublicKeyHash(data) => {
@ -288,13 +313,16 @@ impl Address {
#[cfg(any(test, feature = "test-dependencies"))] #[cfg(any(test, feature = "test-dependencies"))]
pub mod testing { pub mod testing {
use proptest::prelude::*; use proptest::prelude::*;
use sapling::testing::arb_payment_address; use zcash_primitives::consensus::Network;
use zcash_primitives::{consensus::Network, legacy::testing::arb_transparent_addr};
use crate::keys::{testing::arb_unified_spending_key, UnifiedAddressRequest}; use crate::keys::{testing::arb_unified_spending_key, UnifiedAddressRequest};
use super::{Address, UnifiedAddress}; use super::{Address, UnifiedAddress};
#[cfg(feature = "sapling")]
use sapling::testing::arb_payment_address;
use zcash_primitives::legacy::testing::arb_transparent_addr;
pub fn arb_unified_addr( pub fn arb_unified_addr(
params: Network, params: Network,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
@ -302,6 +330,7 @@ pub mod testing {
arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0) arb_unified_spending_key(params).prop_map(move |k| k.default_address(request).0)
} }
#[cfg(feature = "sapling")]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> { pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
prop_oneof![ prop_oneof![
arb_payment_address().prop_map(Address::Sapling), arb_payment_address().prop_map(Address::Sapling),
@ -309,17 +338,34 @@ pub mod testing {
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified), arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
] ]
} }
#[cfg(not(feature = "sapling"))]
pub fn arb_addr(request: UnifiedAddressRequest) -> impl Strategy<Value = Address> {
return prop_oneof![
arb_transparent_addr().prop_map(Address::Transparent),
arb_unified_addr(Network::TestNetwork, request).prop_map(Address::Unified),
];
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use zcash_address::test_vectors; use zcash_address::test_vectors;
use zcash_primitives::{consensus::MAIN_NETWORK, zip32::AccountId}; use zcash_primitives::consensus::MAIN_NETWORK;
use super::{Address, UnifiedAddress}; use super::Address;
#[cfg(feature = "sapling")]
use crate::keys::sapling; use crate::keys::sapling;
#[cfg(any(feature = "orchard", feature = "sapling"))]
use zcash_primitives::zip32::AccountId;
#[cfg(any(feature = "orchard", feature = "sapling"))]
use super::UnifiedAddress;
#[test] #[test]
#[cfg(any(feature = "orchard", feature = "sapling"))]
fn ua_round_trip() { fn ua_round_trip() {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let orchard = { let orchard = {
@ -329,37 +375,49 @@ mod tests {
Some(fvk.address_at(0u32, orchard::keys::Scope::External)) Some(fvk.address_at(0u32, orchard::keys::Scope::External))
}; };
#[cfg(feature = "sapling")]
let sapling = { let sapling = {
let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO); let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO);
let dfvk = extsk.to_diversifiable_full_viewing_key(); let dfvk = extsk.to_diversifiable_full_viewing_key();
Some(dfvk.default_address().1) Some(dfvk.default_address().1)
}; };
let transparent = { None }; let transparent = None;
#[cfg(feature = "orchard")] #[cfg(all(feature = "orchard", feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap(); let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap();
#[cfg(not(feature = "orchard"))] #[cfg(all(not(feature = "orchard"), feature = "sapling"))]
let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap(); let ua = UnifiedAddress::from_receivers(sapling, transparent).unwrap();
#[cfg(all(feature = "orchard", not(feature = "sapling")))]
let ua = UnifiedAddress::from_receivers(orchard, transparent).unwrap();
let addr = Address::Unified(ua); let addr = Address::Unified(ua);
let addr_str = addr.encode(&MAIN_NETWORK); let addr_str = addr.encode(&MAIN_NETWORK);
assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr)); assert_eq!(Address::decode(&MAIN_NETWORK, &addr_str), Some(addr));
} }
#[test]
#[cfg(not(any(feature = "orchard", feature = "sapling")))]
fn ua_round_trip() {
let transparent = None;
assert_eq!(UnifiedAddress::from_receivers(transparent), None)
}
#[test] #[test]
fn ua_parsing() { fn ua_parsing() {
for tv in test_vectors::UNIFIED { for tv in test_vectors::UNIFIED {
match Address::decode(&MAIN_NETWORK, tv.unified_addr) { match Address::decode(&MAIN_NETWORK, tv.unified_addr) {
Some(Address::Unified(ua)) => { Some(Address::Unified(_ua)) => {
assert_eq!( assert_eq!(
ua.transparent().is_some(), _ua.transparent().is_some(),
tv.p2pkh_bytes.is_some() || tv.p2sh_bytes.is_some() tv.p2pkh_bytes.is_some() || tv.p2sh_bytes.is_some()
); );
assert_eq!(ua.sapling().is_some(), tv.sapling_raw_addr.is_some()); #[cfg(feature = "sapling")]
assert_eq!(_ua.sapling().is_some(), tv.sapling_raw_addr.is_some());
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
assert_eq!(ua.orchard().is_some(), tv.orchard_raw_addr.is_some()); assert_eq!(_ua.orchard().is_some(), tv.orchard_raw_addr.is_some());
} }
Some(_) => { Some(_) => {
panic!( panic!(

View File

@ -4,15 +4,20 @@
//! [zcash_primitives::constants] module. //! [zcash_primitives::constants] module.
use crate::address::UnifiedAddress; use crate::address::UnifiedAddress;
use bech32::{self, Error, FromBase32, ToBase32, Variant};
use bs58::{self, decode::Error as Bs58Error}; use bs58::{self, decode::Error as Bs58Error};
use std::fmt; use std::fmt;
use std::io::{self, Write};
use sapling::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey};
use zcash_address::unified::{self, Encoding}; use zcash_address::unified::{self, Encoding};
use zcash_primitives::{consensus, legacy::TransparentAddress}; use zcash_primitives::{consensus, legacy::TransparentAddress};
#[cfg(feature = "sapling")]
use {
bech32::{self, Error, FromBase32, ToBase32, Variant},
sapling::zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
std::io::{self, Write},
};
#[cfg(feature = "sapling")]
fn bech32_encode<F>(hrp: &str, write: F) -> String fn bech32_encode<F>(hrp: &str, write: F) -> String
where where
F: Fn(&mut dyn Write) -> io::Result<()>, F: Fn(&mut dyn Write) -> io::Result<()>,
@ -23,6 +28,7 @@ where
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
#[cfg(feature = "sapling")]
pub enum Bech32DecodeError { pub enum Bech32DecodeError {
Bech32Error(Error), Bech32Error(Error),
IncorrectVariant(Variant), IncorrectVariant(Variant),
@ -30,12 +36,14 @@ pub enum Bech32DecodeError {
HrpMismatch { expected: String, actual: String }, HrpMismatch { expected: String, actual: String },
} }
#[cfg(feature = "sapling")]
impl From<Error> for Bech32DecodeError { impl From<Error> for Bech32DecodeError {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
Bech32DecodeError::Bech32Error(err) Bech32DecodeError::Bech32Error(err)
} }
} }
#[cfg(feature = "sapling")]
impl fmt::Display for Bech32DecodeError { impl fmt::Display for Bech32DecodeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self { match &self {
@ -57,6 +65,7 @@ impl fmt::Display for Bech32DecodeError {
} }
} }
#[cfg(feature = "sapling")]
fn bech32_decode<T, F>(hrp: &str, s: &str, read: F) -> Result<T, Bech32DecodeError> fn bech32_decode<T, F>(hrp: &str, s: &str, read: F) -> Result<T, Bech32DecodeError>
where where
F: Fn(Vec<u8>) -> Option<T>, F: Fn(Vec<u8>) -> Option<T>,
@ -140,6 +149,7 @@ impl<P: consensus::Parameters> AddressCodec<P> for TransparentAddress {
} }
} }
#[cfg(feature = "sapling")]
impl<P: consensus::Parameters> AddressCodec<P> for sapling::PaymentAddress { impl<P: consensus::Parameters> AddressCodec<P> for sapling::PaymentAddress {
type Error = Bech32DecodeError; type Error = Bech32DecodeError;
@ -193,6 +203,7 @@ impl<P: consensus::Parameters> AddressCodec<P> for UnifiedAddress {
/// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk); /// let encoded = encode_extended_spending_key(HRP_SAPLING_EXTENDED_SPENDING_KEY, &extsk);
/// ``` /// ```
/// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey /// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey
#[cfg(feature = "sapling")]
pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> String { pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> String {
bech32_encode(hrp, |w| extsk.write(w)) bech32_encode(hrp, |w| extsk.write(w))
} }
@ -200,6 +211,7 @@ pub fn encode_extended_spending_key(hrp: &str, extsk: &ExtendedSpendingKey) -> S
/// Decodes an [`ExtendedSpendingKey`] from a Bech32-encoded string. /// Decodes an [`ExtendedSpendingKey`] from a Bech32-encoded string.
/// ///
/// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey /// [`ExtendedSpendingKey`]: sapling::zip32::ExtendedSpendingKey
#[cfg(feature = "sapling")]
pub fn decode_extended_spending_key( pub fn decode_extended_spending_key(
hrp: &str, hrp: &str,
s: &str, s: &str,
@ -227,6 +239,7 @@ pub fn decode_extended_spending_key(
/// let encoded = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk); /// let encoded = encode_extended_full_viewing_key(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk);
/// ``` /// ```
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey /// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey
#[cfg(feature = "sapling")]
pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingKey) -> String { pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingKey) -> String {
bech32_encode(hrp, |w| extfvk.write(w)) bech32_encode(hrp, |w| extfvk.write(w))
} }
@ -234,6 +247,7 @@ pub fn encode_extended_full_viewing_key(hrp: &str, extfvk: &ExtendedFullViewingK
/// Decodes an [`ExtendedFullViewingKey`] from a Bech32-encoded string. /// Decodes an [`ExtendedFullViewingKey`] from a Bech32-encoded string.
/// ///
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey /// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey
#[cfg(feature = "sapling")]
pub fn decode_extended_full_viewing_key( pub fn decode_extended_full_viewing_key(
hrp: &str, hrp: &str,
s: &str, s: &str,
@ -269,6 +283,7 @@ pub fn decode_extended_full_viewing_key(
/// ); /// );
/// ``` /// ```
/// [`PaymentAddress`]: sapling::PaymentAddress /// [`PaymentAddress`]: sapling::PaymentAddress
#[cfg(feature = "sapling")]
pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> String { pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> String {
bech32_encode(hrp, |w| w.write_all(&addr.to_bytes())) bech32_encode(hrp, |w| w.write_all(&addr.to_bytes()))
} }
@ -278,6 +293,7 @@ pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> Stri
/// network parameters. /// network parameters.
/// ///
/// [`PaymentAddress`]: sapling::PaymentAddress /// [`PaymentAddress`]: sapling::PaymentAddress
#[cfg(feature = "sapling")]
pub fn encode_payment_address_p<P: consensus::Parameters>( pub fn encode_payment_address_p<P: consensus::Parameters>(
params: &P, params: &P,
addr: &sapling::PaymentAddress, addr: &sapling::PaymentAddress,
@ -316,6 +332,7 @@ pub fn encode_payment_address_p<P: consensus::Parameters>(
/// ); /// );
/// ``` /// ```
/// [`PaymentAddress`]: sapling::PaymentAddress /// [`PaymentAddress`]: sapling::PaymentAddress
#[cfg(feature = "sapling")]
pub fn decode_payment_address( pub fn decode_payment_address(
hrp: &str, hrp: &str,
s: &str, s: &str,
@ -454,15 +471,15 @@ pub fn decode_transparent_address(
} }
#[cfg(test)] #[cfg(test)]
mod tests { #[cfg(feature = "sapling")]
use sapling::{zip32::ExtendedSpendingKey, PaymentAddress}; mod tests_sapling {
use zcash_primitives::constants;
use super::{ use super::{
decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address, decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address, encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
Bech32DecodeError, Bech32DecodeError,
}; };
use sapling::{zip32::ExtendedSpendingKey, PaymentAddress};
use zcash_primitives::constants;
#[test] #[test]
fn extended_spending_key() { fn extended_spending_key() {

View File

@ -31,6 +31,7 @@ use {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
use orchard::{self, keys::Scope}; use orchard::{self, keys::Scope};
#[cfg(feature = "sapling")]
pub mod sapling { pub mod sapling {
pub use sapling::zip32::{ pub use sapling::zip32::{
DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey, DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey,
@ -167,6 +168,7 @@ impl Era {
pub struct UnifiedSpendingKey { pub struct UnifiedSpendingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: legacy::AccountPrivKey, transparent: legacy::AccountPrivKey,
#[cfg(feature = "sapling")]
sapling: sapling::ExtendedSpendingKey, sapling: sapling::ExtendedSpendingKey,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: orchard::keys::SpendingKey, orchard: orchard::keys::SpendingKey,
@ -175,29 +177,27 @@ pub struct UnifiedSpendingKey {
#[doc(hidden)] #[doc(hidden)]
impl UnifiedSpendingKey { impl UnifiedSpendingKey {
pub fn from_seed<P: consensus::Parameters>( pub fn from_seed<P: consensus::Parameters>(
params: &P, _params: &P,
seed: &[u8], seed: &[u8],
account: AccountId, _account: AccountId,
) -> Result<UnifiedSpendingKey, DerivationError> { ) -> Result<UnifiedSpendingKey, DerivationError> {
if seed.len() < 32 { if seed.len() < 32 {
panic!("ZIP 32 seeds MUST be at least 32 bytes"); panic!("ZIP 32 seeds MUST be at least 32 bytes");
} }
#[cfg(feature = "orchard")]
let orchard =
orchard::keys::SpendingKey::from_zip32_seed(seed, params.coin_type(), account)
.map_err(DerivationError::Orchard)?;
#[cfg(feature = "transparent-inputs")]
let transparent = legacy::AccountPrivKey::from_seed(params, seed, account)
.map_err(DerivationError::Transparent)?;
Ok(UnifiedSpendingKey { Ok(UnifiedSpendingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent, transparent: legacy::AccountPrivKey::from_seed(_params, seed, _account)
sapling: sapling::spending_key(seed, params.coin_type(), account), .map_err(DerivationError::Transparent)?,
#[cfg(feature = "sapling")]
sapling: sapling::spending_key(seed, _params.coin_type(), _account),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard: orchard::keys::SpendingKey::from_zip32_seed(
seed,
_params.coin_type(),
_account,
)
.map_err(DerivationError::Orchard)?,
}) })
} }
@ -205,6 +205,7 @@ impl UnifiedSpendingKey {
UnifiedFullViewingKey { UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: Some(self.transparent.to_account_pubkey()), transparent: Some(self.transparent.to_account_pubkey()),
#[cfg(feature = "sapling")]
sapling: Some(self.sapling.to_diversifiable_full_viewing_key()), sapling: Some(self.sapling.to_diversifiable_full_viewing_key()),
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Some((&self.orchard).into()), orchard: Some((&self.orchard).into()),
@ -220,6 +221,7 @@ impl UnifiedSpendingKey {
} }
/// Returns the Sapling extended spending key component of this unified spending key. /// Returns the Sapling extended spending key component of this unified spending key.
#[cfg(feature = "sapling")]
pub fn sapling(&self) -> &sapling::ExtendedSpendingKey { pub fn sapling(&self) -> &sapling::ExtendedSpendingKey {
&self.sapling &self.sapling
} }
@ -254,15 +256,16 @@ impl UnifiedSpendingKey {
result.write_all(orchard_key_bytes).unwrap(); result.write_all(orchard_key_bytes).unwrap();
} }
// sapling #[cfg(feature = "sapling")]
let sapling_key = self.sapling(); {
CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap(); let sapling_key = self.sapling();
CompactSize::write(&mut result, usize::try_from(Typecode::Sapling).unwrap()).unwrap();
let sapling_key_bytes = sapling_key.to_bytes(); let sapling_key_bytes = sapling_key.to_bytes();
CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap(); CompactSize::write(&mut result, sapling_key_bytes.len()).unwrap();
result.write_all(&sapling_key_bytes).unwrap(); result.write_all(&sapling_key_bytes).unwrap();
}
// transparent
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
let account_tkey = self.transparent(); let account_tkey = self.transparent();
@ -294,6 +297,7 @@ impl UnifiedSpendingKey {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let mut orchard = None; let mut orchard = None;
#[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let mut transparent = None; let mut transparent = None;
@ -335,12 +339,15 @@ impl UnifiedSpendingKey {
source source
.read_exact(&mut key) .read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?; .map_err(|_| DecodingError::InsufficientData(Typecode::Sapling))?;
sapling = Some(
sapling::ExtendedSpendingKey::from_bytes(&key) #[cfg(feature = "sapling")]
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?, {
); sapling = Some(
sapling::ExtendedSpendingKey::from_bytes(&key)
.map_err(|_| DecodingError::KeyDataInvalid(Typecode::Sapling))?,
);
}
} }
#[cfg(feature = "transparent-inputs")]
Typecode::P2pkh => { Typecode::P2pkh => {
if len != 64 { if len != 64 {
return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len)); return Err(DecodingError::LengthMismatch(Typecode::P2pkh, len));
@ -350,10 +357,14 @@ impl UnifiedSpendingKey {
source source
.read_exact(&mut key) .read_exact(&mut key)
.map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?; .map_err(|_| DecodingError::InsufficientData(Typecode::P2pkh))?;
transparent = Some(
legacy::AccountPrivKey::from_bytes(&key) #[cfg(feature = "transparent-inputs")]
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?, {
); transparent = Some(
legacy::AccountPrivKey::from_bytes(&key)
.ok_or(DecodingError::KeyDataInvalid(Typecode::P2pkh))?,
);
}
} }
_ => { _ => {
return Err(DecodingError::TypecodeInvalid); return Err(DecodingError::TypecodeInvalid);
@ -365,15 +376,21 @@ impl UnifiedSpendingKey {
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
let has_orchard = true; let has_orchard = true;
#[cfg(feature = "sapling")]
let has_sapling = sapling.is_some();
#[cfg(not(feature = "sapling"))]
let has_sapling = true;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let has_transparent = transparent.is_some(); let has_transparent = transparent.is_some();
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
let has_transparent = true; let has_transparent = true;
if has_orchard && sapling.is_some() && has_transparent { if has_orchard && has_sapling && has_transparent {
return Ok(UnifiedSpendingKey { return Ok(UnifiedSpendingKey {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: orchard.unwrap(), orchard: orchard.unwrap(),
#[cfg(feature = "sapling")]
sapling: sapling.unwrap(), sapling: sapling.unwrap(),
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: transparent.unwrap(), transparent: transparent.unwrap(),
@ -382,7 +399,7 @@ impl UnifiedSpendingKey {
} }
} }
#[cfg(feature = "test-dependencies")] #[cfg(any(test, feature = "test-dependencies"))]
pub fn default_address( pub fn default_address(
&self, &self,
request: UnifiedAddressRequest, request: UnifiedAddressRequest,
@ -390,7 +407,10 @@ impl UnifiedSpendingKey {
self.to_unified_full_viewing_key().default_address(request) self.to_unified_full_viewing_key().default_address(request)
} }
#[cfg(all(feature = "test-dependencies", feature = "transparent-inputs"))] #[cfg(all(
feature = "transparent-inputs",
any(test, feature = "test-dependencies")
))]
pub fn default_transparent_address(&self) -> (TransparentAddress, NonHardenedChildIndex) { pub fn default_transparent_address(&self) -> (TransparentAddress, NonHardenedChildIndex) {
self.transparent() self.transparent()
.to_account_pubkey() .to_account_pubkey()
@ -405,8 +425,10 @@ impl UnifiedSpendingKey {
pub enum AddressGenerationError { pub enum AddressGenerationError {
/// The requested diversifier index was outside the range of valid transparent /// The requested diversifier index was outside the range of valid transparent
/// child address indices. /// child address indices.
#[cfg(feature = "transparent-inputs")]
InvalidTransparentChildIndex(DiversifierIndex), InvalidTransparentChildIndex(DiversifierIndex),
/// The diversifier index could not be mapped to a valid Sapling diversifier. /// The diversifier index could not be mapped to a valid Sapling diversifier.
#[cfg(feature = "sapling")]
InvalidSaplingDiversifierIndex(DiversifierIndex), InvalidSaplingDiversifierIndex(DiversifierIndex),
/// A requested address typecode was not recognized, so we are unable to generate the address /// A requested address typecode was not recognized, so we are unable to generate the address
/// as requested. /// as requested.
@ -422,13 +444,12 @@ pub enum AddressGenerationError {
/// Specification for how a unified address should be generated from a unified viewing key. /// Specification for how a unified address should be generated from a unified viewing key.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct UnifiedAddressRequest { pub struct UnifiedAddressRequest {
_has_orchard: bool, has_orchard: bool,
has_sapling: bool, has_sapling: bool,
has_p2pkh: bool, has_p2pkh: bool,
} }
impl UnifiedAddressRequest { impl UnifiedAddressRequest {
/// Construct a new unified address request from its constituent parts
pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> { pub fn new(has_orchard: bool, has_sapling: bool, has_p2pkh: bool) -> Option<Self> {
let has_shielded_receiver = has_orchard || has_sapling; let has_shielded_receiver = has_orchard || has_sapling;
@ -436,7 +457,7 @@ impl UnifiedAddressRequest {
None None
} else { } else {
Some(Self { Some(Self {
_has_orchard: has_orchard, has_orchard,
has_sapling, has_sapling,
has_p2pkh, has_p2pkh,
}) })
@ -452,7 +473,7 @@ impl UnifiedAddressRequest {
} }
Self { Self {
_has_orchard: has_orchard, has_orchard,
has_sapling, has_sapling,
has_p2pkh, has_p2pkh,
} }
@ -465,6 +486,7 @@ impl UnifiedAddressRequest {
pub struct UnifiedFullViewingKey { pub struct UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent: Option<legacy::AccountPubKey>, transparent: Option<legacy::AccountPubKey>,
#[cfg(feature = "sapling")]
sapling: Option<sapling::DiversifiableFullViewingKey>, sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard: Option<orchard::keys::FullViewingKey>, orchard: Option<orchard::keys::FullViewingKey>,
@ -476,15 +498,24 @@ impl UnifiedFullViewingKey {
/// Construct a new unified full viewing key, if the required components are present. /// Construct a new unified full viewing key, if the required components are present.
pub fn new( pub fn new(
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>, #[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
sapling: Option<sapling::DiversifiableFullViewingKey>, #[cfg(feature = "sapling")] sapling: Option<sapling::DiversifiableFullViewingKey>,
#[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>, #[cfg(feature = "orchard")] orchard: Option<orchard::keys::FullViewingKey>,
// TODO: Implement construction of UFVKs with metadata items.
) -> Option<UnifiedFullViewingKey> { ) -> Option<UnifiedFullViewingKey> {
if sapling.is_none() { #[cfg(feature = "orchard")]
None let has_orchard = orchard.is_some();
} else { #[cfg(not(feature = "orchard"))]
let has_orchard = false;
#[cfg(feature = "sapling")]
let has_sapling = sapling.is_some();
#[cfg(not(feature = "sapling"))]
let has_sapling = false;
if has_orchard || has_sapling {
Some(UnifiedFullViewingKey { Some(UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent, transparent,
#[cfg(feature = "sapling")]
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
@ -492,6 +523,8 @@ impl UnifiedFullViewingKey {
// this to allow parsing such UFVKs. // this to allow parsing such UFVKs.
unknown: vec![], unknown: vec![],
}) })
} else {
None
} }
} }
@ -510,6 +543,7 @@ impl UnifiedFullViewingKey {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let mut orchard = None; let mut orchard = None;
#[cfg(feature = "sapling")]
let mut sapling = None; let mut sapling = None;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let mut transparent = None; let mut transparent = None;
@ -529,9 +563,11 @@ impl UnifiedFullViewingKey {
}) })
.transpose(), .transpose(),
#[cfg(not(feature = "orchard"))] #[cfg(not(feature = "orchard"))]
unified::Fvk::Orchard(data) => { unified::Fvk::Orchard(data) => Some(Ok::<_, &'static str>((
Some(Ok((unified::Typecode::Orchard.into(), data.to_vec()))) u32::from(unified::Typecode::Orchard),
} data.to_vec(),
))),
#[cfg(feature = "sapling")]
unified::Fvk::Sapling(data) => { unified::Fvk::Sapling(data) => {
sapling::DiversifiableFullViewingKey::from_bytes(data) sapling::DiversifiableFullViewingKey::from_bytes(data)
.ok_or("Invalid Sapling FVK in Unified FVK") .ok_or("Invalid Sapling FVK in Unified FVK")
@ -541,6 +577,11 @@ impl UnifiedFullViewingKey {
}) })
.transpose() .transpose()
} }
#[cfg(not(feature = "sapling"))]
unified::Fvk::Sapling(data) => Some(Ok::<_, &'static str>((
u32::from(unified::Typecode::Sapling),
data.to_vec(),
))),
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data) unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data)
.map_err(|_| "Invalid transparent FVK in Unified FVK") .map_err(|_| "Invalid transparent FVK in Unified FVK")
@ -550,9 +591,10 @@ impl UnifiedFullViewingKey {
}) })
.transpose(), .transpose(),
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
unified::Fvk::P2pkh(data) => { unified::Fvk::P2pkh(data) => Some(Ok::<_, &'static str>((
Some(Ok((unified::Typecode::P2pkh.into(), data.to_vec()))) u32::from(unified::Typecode::P2pkh),
} data.to_vec(),
))),
unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))), unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))),
}) })
.collect::<Result<_, _>>()?; .collect::<Result<_, _>>()?;
@ -560,6 +602,7 @@ impl UnifiedFullViewingKey {
Ok(Self { Ok(Self {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent, transparent,
#[cfg(feature = "sapling")]
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
@ -569,21 +612,12 @@ impl UnifiedFullViewingKey {
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network. /// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String { pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
let items = std::iter::empty() let items = std::iter::empty().chain(self.unknown.iter().map(|(typecode, data)| {
.chain( unified::Fvk::Unknown {
self.sapling typecode: *typecode,
.as_ref() data: data.clone(),
.map(|dfvk| dfvk.to_bytes()) }
.map(unified::Fvk::Sapling), }));
)
.chain(
self.unknown
.iter()
.map(|(typecode, data)| unified::Fvk::Unknown {
typecode: *typecode,
data: data.clone(),
}),
);
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let items = items.chain( let items = items.chain(
self.orchard self.orchard
@ -591,6 +625,13 @@ impl UnifiedFullViewingKey {
.map(|fvk| fvk.to_bytes()) .map(|fvk| fvk.to_bytes())
.map(unified::Fvk::Orchard), .map(unified::Fvk::Orchard),
); );
#[cfg(feature = "sapling")]
let items = items.chain(
self.sapling
.as_ref()
.map(|dfvk| dfvk.to_bytes())
.map(unified::Fvk::Sapling),
);
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let items = items.chain( let items = items.chain(
self.transparent self.transparent
@ -612,6 +653,7 @@ impl UnifiedFullViewingKey {
} }
/// Returns the Sapling diversifiable full viewing key component of this unified key. /// Returns the Sapling diversifiable full viewing key component of this unified key.
#[cfg(feature = "sapling")]
pub fn sapling(&self) -> Option<&sapling::DiversifiableFullViewingKey> { pub fn sapling(&self) -> Option<&sapling::DiversifiableFullViewingKey> {
self.sapling.as_ref() self.sapling.as_ref()
} }
@ -628,39 +670,52 @@ impl UnifiedFullViewingKey {
/// Returns `None` if the specified index does not produce a valid diversifier. /// Returns `None` if the specified index does not produce a valid diversifier.
pub fn address( pub fn address(
&self, &self,
j: DiversifierIndex, _j: DiversifierIndex,
request: UnifiedAddressRequest, _request: UnifiedAddressRequest,
) -> Result<UnifiedAddress, AddressGenerationError> { ) -> Result<UnifiedAddress, AddressGenerationError> {
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let orchard = if request._has_orchard { let mut orchard = None;
if _request.has_orchard {
#[cfg(not(feature = "orchard"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Orchard,
));
#[cfg(feature = "orchard")]
if let Some(ofvk) = &self.orchard { if let Some(ofvk) = &self.orchard {
let orchard_j = orchard::keys::DiversifierIndex::from(*j.as_bytes()); let orchard_j = orchard::keys::DiversifierIndex::from(*_j.as_bytes());
Some(ofvk.address_at(orchard_j, Scope::External)) orchard = Some(ofvk.address_at(orchard_j, Scope::External))
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::Orchard));
} }
} else { }
None
};
let sapling = if request.has_sapling { #[cfg(feature = "sapling")]
let mut sapling = None;
if _request.has_sapling {
#[cfg(not(feature = "sapling"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::Sapling,
));
#[cfg(feature = "sapling")]
if let Some(extfvk) = &self.sapling { if let Some(extfvk) = &self.sapling {
// If a Sapling receiver type is requested, we must be able to construct an // If a Sapling receiver type is requested, we must be able to construct an
// address; if we're unable to do so, then no Unified Address exists at this // address; if we're unable to do so, then no Unified Address exists at this
// diversifier and we use `?` to early-return from this method. // diversifier and we use `?` to early-return from this method.
Some( sapling = Some(
extfvk extfvk
.address(j) .address(_j)
.ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(j))?, .ok_or(AddressGenerationError::InvalidSaplingDiversifierIndex(_j))?,
) );
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::Sapling));
} }
} else { }
None
};
let transparent = if request.has_p2pkh { #[cfg(feature = "transparent-inputs")]
let mut transparent = None;
if _request.has_p2pkh {
#[cfg(not(feature = "transparent-inputs"))] #[cfg(not(feature = "transparent-inputs"))]
return Err(AddressGenerationError::ReceiverTypeNotSupported( return Err(AddressGenerationError::ReceiverTypeNotSupported(
Typecode::P2pkh, Typecode::P2pkh,
@ -671,30 +726,25 @@ impl UnifiedFullViewingKey {
// If a transparent receiver type is requested, we must be able to construct an // If a transparent receiver type is requested, we must be able to construct an
// address; if we're unable to do so, then no Unified Address exists at this // address; if we're unable to do so, then no Unified Address exists at this
// diversifier. // diversifier.
match to_transparent_child_index(j) { let transparent_j = to_transparent_child_index(_j)
Some(transparent_j) => match tfvk .ok_or(AddressGenerationError::InvalidTransparentChildIndex(_j))?;
.derive_external_ivk()
transparent = Some(
tfvk.derive_external_ivk()
.and_then(|tivk| tivk.derive_address(transparent_j)) .and_then(|tivk| tivk.derive_address(transparent_j))
{ .map_err(|_| AddressGenerationError::InvalidTransparentChildIndex(_j))?,
Ok(taddr) => Some(taddr), );
Err(_) => {
return Err(AddressGenerationError::InvalidTransparentChildIndex(j))
}
},
// Diversifier doesn't generate a valid transparent child index, so we eagerly
// return `None`.
None => return Err(AddressGenerationError::InvalidTransparentChildIndex(j)),
}
} else { } else {
return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh)); return Err(AddressGenerationError::KeyNotAvailable(Typecode::P2pkh));
} }
} else { }
None #[cfg(not(feature = "transparent-inputs"))]
}; let transparent = None;
UnifiedAddress::from_receivers( UnifiedAddress::from_receivers(
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
#[cfg(feature = "sapling")]
sapling, sapling,
transparent, transparent,
) )
@ -706,6 +756,7 @@ impl UnifiedFullViewingKey {
/// diversifier along with the index at which the valid diversifier was found. /// diversifier along with the index at which the valid diversifier was found.
/// ///
/// Returns `None` if no valid diversifier exists /// Returns `None` if no valid diversifier exists
#[allow(unused_mut)]
pub fn find_address( pub fn find_address(
&self, &self,
mut j: DiversifierIndex, mut j: DiversifierIndex,
@ -726,6 +777,7 @@ impl UnifiedFullViewingKey {
Ok(ua) => { Ok(ua) => {
break Some((ua, j)); break Some((ua, j));
} }
#[cfg(feature = "sapling")]
Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => { Err(AddressGenerationError::InvalidSaplingDiversifierIndex(_)) => {
if j.increment().is_err() { if j.increment().is_err() {
break None; break None;
@ -776,10 +828,15 @@ pub mod testing {
mod tests { mod tests {
use proptest::prelude::proptest; use proptest::prelude::proptest;
use super::{sapling, UnifiedFullViewingKey}; use super::UnifiedFullViewingKey;
use zcash_primitives::consensus::MAIN_NETWORK; use zcash_primitives::consensus::MAIN_NETWORK;
#[cfg(any(feature = "orchard", feature = "sapling"))]
use zip32::AccountId; use zip32::AccountId;
#[cfg(feature = "sapling")]
use super::sapling;
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use {
crate::{address::Address, encoding::AddressCodec}, crate::{address::Address, encoding::AddressCodec},
@ -808,6 +865,7 @@ mod tests {
#[test] #[test]
#[should_panic] #[should_panic]
#[cfg(feature = "sapling")]
fn spending_key_panics_on_short_seed() { fn spending_key_panics_on_short_seed() {
let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::ZERO); let _ = sapling::spending_key(&[0; 31][..], 0, AccountId::ZERO);
} }
@ -831,8 +889,6 @@ mod tests {
#[test] #[test]
fn ufvk_round_trip() { fn ufvk_round_trip() {
let account = AccountId::ZERO;
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
let orchard = { let orchard = {
let sk = let sk =
@ -840,76 +896,122 @@ mod tests {
Some(orchard::keys::FullViewingKey::from(&sk)) Some(orchard::keys::FullViewingKey::from(&sk))
}; };
#[cfg(feature = "sapling")]
let sapling = { let sapling = {
let extsk = sapling::spending_key(&[0; 32], 0, account); let extsk = sapling::spending_key(&[0; 32], 0, AccountId::ZERO);
Some(extsk.to_diversifiable_full_viewing_key()) Some(extsk.to_diversifiable_full_viewing_key())
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
let transparent = { let transparent = {
let privkey = AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], account).unwrap(); let privkey =
AccountPrivKey::from_seed(&MAIN_NETWORK, &[0; 32], AccountId::ZERO).unwrap();
Some(privkey.to_account_pubkey()) Some(privkey.to_account_pubkey())
}; };
let ufvk = UnifiedFullViewingKey::new( let ufvk = UnifiedFullViewingKey::new(
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
transparent, transparent,
#[cfg(feature = "sapling")]
sapling, sapling,
#[cfg(feature = "orchard")] #[cfg(feature = "orchard")]
orchard, orchard,
) );
.unwrap();
let encoded = ufvk.encode(&MAIN_NETWORK); #[cfg(not(any(feature = "orchard", feature = "sapling")))]
assert_eq!(ufvk, None);
// Test encoded form against known values; these test vectors contain Orchard receivers #[cfg(any(feature = "orchard", feature = "sapling"))]
// that will be treated as unknown if the `orchard` feature is not enabled.
let encoded_with_t = "uview1tg6rpjgju2s2j37gkgjq79qrh5lvzr6e0ed3n4sf4hu5qd35vmsh7avl80xa6mx7ryqce9hztwaqwrdthetpy4pc0kce25x453hwcmax02p80pg5savlg865sft9reat07c5vlactr6l2pxtlqtqunt2j9gmvr8spcuzf07af80h5qmut38h0gvcfa9k4rwujacwwca9vu8jev7wq6c725huv8qjmhss3hdj2vh8cfxhpqcm2qzc34msyrfxk5u6dqttt4vv2mr0aajreww5yufpk0gn4xkfm888467k7v6fmw7syqq6cceu078yw8xja502jxr0jgum43lhvpzmf7eu5dmnn6cr6f7p43yw8znzgxg598mllewnx076hljlvynhzwn5es94yrv65tdg3utuz2u3sras0wfcq4adxwdvlk387d22g3q98t5z74quw2fa4wed32escx8dwh4mw35t4jwf35xyfxnu83mk5s4kw2glkgsshmxk";
let _encoded_no_t = "uview12z384wdq76ceewlsu0esk7d97qnd23v2qnvhujxtcf2lsq8g4hwzpx44fwxssnm5tg8skyh4tnc8gydwxefnnm0hd0a6c6etmj0pp9jqkdsllkr70u8gpf7ndsfqcjlqn6dec3faumzqlqcmtjf8vp92h7kj38ph2786zx30hq2wru8ae3excdwc8w0z3t9fuw7mt7xy5sn6s4e45kwm0cjp70wytnensgdnev286t3vew3yuwt2hcz865y037k30e428dvgne37xvyeal2vu8yjnznphf9t2rw3gdp0hk5zwq00ws8f3l3j5n3qkqgsyzrwx4qzmgq0xwwk4vz2r6vtsykgz089jncvycmem3535zjwvvtvjw8v98y0d5ydwte575gjm7a7k";
// We test the full roundtrip only with the `orchard` feature enabled, because we will
// not generate the `orchard` part of the encoding if the UFVK does not have an Orchard
// part.
#[cfg(feature = "orchard")]
{ {
let ufvk = ufvk.expect("Orchard or Sapling fvk is present.");
let encoded = ufvk.encode(&MAIN_NETWORK);
// Test encoded form against known values; these test vectors contain Orchard receivers
// that will be treated as unknown if the `orchard` feature is not enabled.
let encoded_with_t = "uview1tg6rpjgju2s2j37gkgjq79qrh5lvzr6e0ed3n4sf4hu5qd35vmsh7avl80xa6mx7ryqce9hztwaqwrdthetpy4pc0kce25x453hwcmax02p80pg5savlg865sft9reat07c5vlactr6l2pxtlqtqunt2j9gmvr8spcuzf07af80h5qmut38h0gvcfa9k4rwujacwwca9vu8jev7wq6c725huv8qjmhss3hdj2vh8cfxhpqcm2qzc34msyrfxk5u6dqttt4vv2mr0aajreww5yufpk0gn4xkfm888467k7v6fmw7syqq6cceu078yw8xja502jxr0jgum43lhvpzmf7eu5dmnn6cr6f7p43yw8znzgxg598mllewnx076hljlvynhzwn5es94yrv65tdg3utuz2u3sras0wfcq4adxwdvlk387d22g3q98t5z74quw2fa4wed32escx8dwh4mw35t4jwf35xyfxnu83mk5s4kw2glkgsshmxk";
let _encoded_no_t = "uview12z384wdq76ceewlsu0esk7d97qnd23v2qnvhujxtcf2lsq8g4hwzpx44fwxssnm5tg8skyh4tnc8gydwxefnnm0hd0a6c6etmj0pp9jqkdsllkr70u8gpf7ndsfqcjlqn6dec3faumzqlqcmtjf8vp92h7kj38ph2786zx30hq2wru8ae3excdwc8w0z3t9fuw7mt7xy5sn6s4e45kwm0cjp70wytnensgdnev286t3vew3yuwt2hcz865y037k30e428dvgne37xvyeal2vu8yjnznphf9t2rw3gdp0hk5zwq00ws8f3l3j5n3qkqgsyzrwx4qzmgq0xwwk4vz2r6vtsykgz089jncvycmem3535zjwvvtvjw8v98y0d5ydwte575gjm7a7k";
// We test the full roundtrip only with the `sapling` and `orchard` features enabled,
// because we will not generate these parts of the encoding if the UFVK does not have an
// these parts.
#[cfg(all(feature = "sapling", feature = "orchard"))]
{
#[cfg(feature = "transparent-inputs")]
assert_eq!(encoded, encoded_with_t);
#[cfg(not(feature = "transparent-inputs"))]
assert_eq!(encoded, _encoded_no_t);
}
let decoded = UnifiedFullViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap();
let reencoded = decoded.encode(&MAIN_NETWORK);
assert_eq!(encoded, reencoded);
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
assert_eq!(encoded, encoded_with_t); assert_eq!(
#[cfg(not(feature = "transparent-inputs"))] decoded.transparent.map(|t| t.serialize()),
assert_eq!(encoded, _encoded_no_t); ufvk.transparent.as_ref().map(|t| t.serialize()),
);
#[cfg(feature = "sapling")]
assert_eq!(
decoded.sapling.map(|s| s.to_bytes()),
ufvk.sapling.map(|s| s.to_bytes()),
);
#[cfg(feature = "orchard")]
assert_eq!(
decoded.orchard.map(|o| o.to_bytes()),
ufvk.orchard.map(|o| o.to_bytes()),
);
let decoded_with_t =
UnifiedFullViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap();
#[cfg(feature = "transparent-inputs")]
assert_eq!(
decoded_with_t.transparent.map(|t| t.serialize()),
ufvk.transparent.as_ref().map(|t| t.serialize()),
);
// Both Orchard and Sapling enabled
#[cfg(all(
feature = "orchard",
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 0);
#[cfg(all(
feature = "orchard",
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
// Orchard enabled
#[cfg(all(
feature = "orchard",
not(feature = "sapling"),
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
#[cfg(all(
feature = "orchard",
not(feature = "sapling"),
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
// Sapling enabled
#[cfg(all(
not(feature = "orchard"),
feature = "sapling",
feature = "transparent-inputs"
))]
assert_eq!(decoded_with_t.unknown.len(), 1);
#[cfg(all(
not(feature = "orchard"),
feature = "sapling",
not(feature = "transparent-inputs")
))]
assert_eq!(decoded_with_t.unknown.len(), 2);
} }
let decoded = UnifiedFullViewingKey::decode(&MAIN_NETWORK, &encoded).unwrap();
let reencoded = decoded.encode(&MAIN_NETWORK);
assert_eq!(encoded, reencoded);
#[cfg(feature = "transparent-inputs")]
assert_eq!(
decoded.transparent.map(|t| t.serialize()),
ufvk.transparent.as_ref().map(|t| t.serialize()),
);
assert_eq!(
decoded.sapling.map(|s| s.to_bytes()),
ufvk.sapling.map(|s| s.to_bytes()),
);
#[cfg(feature = "orchard")]
assert_eq!(
decoded.orchard.map(|o| o.to_bytes()),
ufvk.orchard.map(|o| o.to_bytes()),
);
let decoded_with_t = UnifiedFullViewingKey::decode(&MAIN_NETWORK, encoded_with_t).unwrap();
#[cfg(feature = "transparent-inputs")]
assert_eq!(
decoded_with_t.transparent.map(|t| t.serialize()),
ufvk.transparent.as_ref().map(|t| t.serialize()),
);
#[cfg(all(not(feature = "orchard"), not(feature = "transparent-inputs")))]
assert_eq!(decoded_with_t.unknown.len(), 2);
#[cfg(all(feature = "transparent-inputs", not(feature = "orchard")))]
assert_eq!(decoded_with_t.unknown.len(), 1);
#[cfg(all(feature = "orchard", not(feature = "transparent-inputs")))]
assert_eq!(decoded_with_t.unknown.len(), 1);
} }
#[test] #[test]