zcash_client_backend: Add `RecipientAddress::Unified`

This commit is contained in:
Jack Grigg 2022-06-07 11:35:53 +00:00
parent f20366cf86
commit ed6016857e
6 changed files with 226 additions and 8 deletions

View File

@ -25,6 +25,8 @@ and this library adheres to Rust's notion of
of a `zcash_client_backend::zip321::TransactionRequest` value. of a `zcash_client_backend::zip321::TransactionRequest` value.
This facilitates the implementation of ZIP 321 support in wallets and This facilitates the implementation of ZIP 321 support in wallets and
provides substantially greater flexibility in transaction creation. provides substantially greater flexibility in transaction creation.
- `zcash_client_backend::address`:
- `RecipientAddress::Unified`
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx` - `actions` field on `compact_formats::CompactTx`
- `compact_formats::CompactOrchardAction` - `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 - New experimental APIs that should be considered unstable, and are
likely to be modified and/or moved to a different module in a future likely to be modified and/or moved to a different module in a future
release: release:
- `zcash_client_backend::address::UnifiedAddress`
- `zcash_client_backend::keys::{UnifiedSpendingKey`, `UnifiedFullViewingKey`} - `zcash_client_backend::keys::{UnifiedSpendingKey`, `UnifiedFullViewingKey`}
- `zcash_client_backend::encoding::AddressCodec` - `zcash_client_backend::encoding::AddressCodec`
- `zcash_client_backend::encoding::encode_payment_address` - `zcash_client_backend::encoding::encode_payment_address`

View File

@ -24,6 +24,7 @@ hdwallet = { version = "0.3.1", optional = true }
jubjub = "0.9" jubjub = "0.9"
log = "0.4" log = "0.4"
nom = "7" nom = "7"
orchard = "0.1"
percent-encoding = "2.1.0" percent-encoding = "2.1.0"
proptest = { version = "1.0.0", optional = true } proptest = { version = "1.0.0", optional = true }
protobuf = "~2.27.1" # MSRV 1.52.1 protobuf = "~2.27.1" # MSRV 1.52.1
@ -48,7 +49,11 @@ zcash_proofs = { version = "0.6", path = "../zcash_proofs" }
[features] [features]
transparent-inputs = ["ripemd", "hdwallet", "sha2", "secp256k1", "zcash_primitives/transparent-inputs"] 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] [lib]
bench = false bench = false

View File

@ -1,6 +1,11 @@
//! Structs for handling supported address types. //! 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}; use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress};
fn params_to_network<P: consensus::Parameters>(params: &P) -> Network { fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
@ -13,12 +18,134 @@ fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
} }
} }
/// A Unified Address.
#[derive(Clone, Debug, PartialEq)]
pub struct UnifiedAddress {
orchard: Option<orchard::Address>,
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
unknown: Vec<(u32, Vec<u8>)>,
}
impl TryFrom<unified::Address> for UnifiedAddress {
type Error = &'static str;
fn try_from(ua: unified::Address) -> Result<Self, Self::Error> {
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::<Result<_, _>>()?;
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<orchard::Address>,
sapling: Option<PaymentAddress>,
transparent: Option<TransparentAddress>,
) -> Option<Self> {
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. /// An address that funds can be sent to.
// TODO: rename to ParsedAddress // TODO: rename to ParsedAddress
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum RecipientAddress { pub enum RecipientAddress {
Shielded(PaymentAddress), Shielded(PaymentAddress),
Transparent(TransparentAddress), Transparent(TransparentAddress),
Unified(UnifiedAddress),
} }
impl From<PaymentAddress> for RecipientAddress { impl From<PaymentAddress> for RecipientAddress {
@ -33,6 +160,12 @@ impl From<TransparentAddress> for RecipientAddress {
} }
} }
impl From<UnifiedAddress> for RecipientAddress {
fn from(addr: UnifiedAddress) -> Self {
RecipientAddress::Unified(addr)
}
}
impl TryFromRawAddress for RecipientAddress { impl TryFromRawAddress for RecipientAddress {
type Error = &'static str; type Error = &'static str;
@ -41,6 +174,14 @@ impl TryFromRawAddress for RecipientAddress {
Ok(pa.into()) Ok(pa.into())
} }
fn try_from_raw_unified(
ua: zcash_address::unified::Address,
) -> Result<Self, ConversionError<Self::Error>> {
UnifiedAddress::try_from(ua)
.map_err(ConversionError::User)
.map(RecipientAddress::from)
}
fn try_from_raw_transparent_p2pkh( fn try_from_raw_transparent_p2pkh(
data: [u8; 20], data: [u8; 20],
) -> Result<Self, ConversionError<Self::Error>> { ) -> Result<Self, ConversionError<Self::Error>> {
@ -69,7 +210,42 @@ impl RecipientAddress {
} }
TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data), TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data),
}, },
RecipientAddress::Unified(ua) => ua.to_address(net),
} }
.to_string() .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)
);
}
}

View File

@ -334,6 +334,16 @@ where
for payment in request.payments() { for payment in request.payments() {
match &payment.recipient_address { 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 RecipientAddress::Shielded(to) => builder
.add_sapling_output( .add_sapling_output(
ovk, ovk,
@ -359,7 +369,8 @@ where
let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| { let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| {
let idx = match &payment.recipient_address { let idx = match &payment.recipient_address {
// Sapling outputs are shuffled, so we need to look up where the output ended up. // 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."), tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."),
RecipientAddress::Transparent(addr) => { RecipientAddress::Transparent(addr) => {
let script = addr.script(); let script = addr.script();

View File

@ -452,7 +452,9 @@ mod parse {
match v { match v {
Param::Amount(a) => payment.amount = a, Param::Amount(a) => payment.amount = a,
Param::Memo(m) => match payment.recipient_address { Param::Memo(m) => match payment.recipient_address {
RecipientAddress::Shielded(_) => payment.memo = Some(m), RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => {
payment.memo = Some(m)
}
RecipientAddress::Transparent(_) => { RecipientAddress::Transparent(_) => {
return Err(Zip321Error::TransparentMemo(i)) return Err(Zip321Error::TransparentMemo(i))
} }
@ -646,14 +648,24 @@ pub mod testing {
transaction::components::amount::testing::arb_nonnegative_amount, transaction::components::amount::testing::arb_nonnegative_amount,
}; };
use crate::address::RecipientAddress; use crate::address::{RecipientAddress, UnifiedAddress};
use super::{MemoBytes, Payment, TransactionRequest}; 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<Value = RecipientAddress> { pub fn arb_addr() -> impl Strategy<Value = RecipientAddress> {
prop_oneof![ prop_oneof![
arb_shielded_addr().prop_map(RecipientAddress::Shielded), arb_shielded_addr().prop_map(RecipientAddress::Shielded),
arb_transparent_addr().prop_map(RecipientAddress::Transparent), 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::<String>(), 0..3), other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
) -> Payment { ) -> Payment {
let is_sapling = match recipient_address { let is_shielded = match recipient_address {
RecipientAddress::Transparent(_) => false, RecipientAddress::Transparent(_) => false,
RecipientAddress::Shielded(_) => true, RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => true,
}; };
Payment { Payment {
recipient_address, recipient_address,
amount, amount,
memo: memo.filter(|_| is_sapling), memo: memo.filter(|_| is_shielded),
label, label,
message, message,
other_params: other_params.into_iter().collect(), other_params: other_params.into_iter().collect(),

View File

@ -642,6 +642,17 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
for output in &sent_tx.outputs { for output in &sent_tx.outputs {
match output.recipient_address { 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( RecipientAddress::Shielded(addr) => wallet::insert_sent_note(
up, up,
tx_ref, tx_ref,