zcash_client_backend: Migrate to correct ZIP 316 UFVK encoding

We also add support for parsing Orchard full viewing keys from encoded
UFVKs (rather than treating them as unknown). `UnifiedSpendingKey` still
does not have Orchard support, so `UnifiedFullViewingKey`s will be
generated without Orchard components.
This commit is contained in:
Jack Grigg 2022-06-14 02:41:01 +00:00
parent 76d015ed11
commit b52e949bd6
5 changed files with 132 additions and 68 deletions

View File

@ -8,7 +8,7 @@ use zcash_address::{
};
use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress};
fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
pub(crate) fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
// Use the Sapling HRP as an indicator of network.
match params.hrp_sapling_payment_address() {
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Main,

View File

@ -1,12 +1,14 @@
//! Helper functions for managing light client key material.
use zcash_address::unified::{self, Container, Encoding};
use zcash_primitives::{
consensus,
sapling::keys as sapling_keys,
zip32::{AccountId, DiversifierIndex},
};
use crate::address::UnifiedAddress;
use crate::address::{params_to_network, UnifiedAddress};
#[cfg(feature = "transparent-inputs")]
use std::convert::TryInto;
#[cfg(feature = "transparent-inputs")]
@ -110,6 +112,8 @@ impl UnifiedSpendingKey {
#[cfg(feature = "transparent-inputs")]
transparent: Some(self.transparent.to_account_pubkey()),
sapling: Some(sapling::ExtendedFullViewingKey::from(&self.sapling).into()),
orchard: None,
unknown: vec![],
}
}
@ -139,6 +143,8 @@ pub struct UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")]
transparent: Option<legacy::AccountPubKey>,
sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
orchard: Option<orchard::keys::FullViewingKey>,
unknown: Vec<(u32, Vec<u8>)>,
}
#[doc(hidden)]
@ -147,6 +153,7 @@ impl UnifiedFullViewingKey {
pub fn new(
#[cfg(feature = "transparent-inputs")] transparent: Option<legacy::AccountPubKey>,
sapling: Option<sapling_keys::DiversifiableFullViewingKey>,
orchard: Option<orchard::keys::FullViewingKey>,
) -> Option<UnifiedFullViewingKey> {
if sapling.is_none() {
None
@ -155,70 +162,113 @@ impl UnifiedFullViewingKey {
#[cfg(feature = "transparent-inputs")]
transparent,
sapling,
orchard,
// We don't allow constructing new UFVKs with unknown items, but we store
// this to allow parsing such UFVKs.
unknown: vec![],
})
}
}
/// Attempts to decode the given string as an encoding of a `UnifiedFullViewingKey`
/// for the given network.
/// Parses a `UnifiedFullViewingKey` from its [ZIP 316] string encoding.
///
/// [ZIP 316]: https://zips.z.cash/zip-0316
pub fn decode<P: consensus::Parameters>(params: &P, encoding: &str) -> Result<Self, String> {
encoding
.strip_prefix("DONOTUSEUFVK")
.and_then(|data| hex::decode(data).ok())
.as_ref()
.and_then(|data| data.split_first())
.and_then(|(flag, data)| {
let (net, ufvk) = unified::Ufvk::decode(encoding).map_err(|e| e.to_string())?;
let expected_net = params_to_network(params);
if net != expected_net {
return Err(format!(
"UFVK is for network {:?} but we expected {:?}",
net, expected_net,
));
}
let mut orchard = None;
let mut sapling = None;
#[cfg(feature = "transparent-inputs")]
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 = ufvk
.items_as_parsed()
.iter()
.filter_map(|receiver| match receiver {
unified::Fvk::Orchard(data) => orchard::keys::FullViewingKey::from_bytes(data)
.ok_or("Invalid Orchard FVK in Unified FVK")
.map(|addr| {
orchard = Some(addr);
None
})
.transpose(),
unified::Fvk::Sapling(data) => {
sapling_keys::DiversifiableFullViewingKey::from_bytes(data)
.ok_or("Invalid Sapling FVK in Unified FVK")
.map(|pa| {
sapling = Some(pa);
None
})
.transpose()
}
#[cfg(feature = "transparent-inputs")]
let (transparent, data) = if flag & 1 != 0 {
if data.len() < 65 {
return None;
}
let (tfvk, data) = data.split_at(65);
(
Some(legacy::AccountPubKey::deserialize(tfvk.try_into().unwrap()).ok()?),
data,
)
} else {
(None, data)
};
let sapling = if flag & 2 != 0 {
if data.len() != 128 {
return None;
}
Some(sapling_keys::DiversifiableFullViewingKey::from_bytes(
data.try_into().unwrap(),
)?)
} else {
None
};
Some(Self {
#[cfg(feature = "transparent-inputs")]
transparent,
sapling,
})
unified::Fvk::P2pkh(data) => legacy::AccountPubKey::deserialize(data)
.map_err(|_| "Invalid transparent FVK in Unified FVK")
.map(|tfvk| {
transparent = Some(tfvk);
None
})
.transpose(),
#[cfg(not(feature = "transparent-inputs"))]
unified::Fvk::P2pkh(data) => {
Some(Ok((unified::Typecode::P2pkh.into(), data.to_vec())))
}
unified::Fvk::Unknown { typecode, data } => Some(Ok((*typecode, data.clone()))),
})
.ok_or("TODO Implement real UFVK encoding after fixing struct".to_string())
.collect::<Result<_, _>>()?;
Ok(Self {
#[cfg(feature = "transparent-inputs")]
transparent,
sapling,
orchard,
unknown,
})
}
/// Returns the string encoding of this `UnifiedFullViewingKey` for the given network.
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
let flag = if self.sapling.is_some() { 2 } else { 0 };
let items = std::iter::empty()
.chain(
self.orchard
.as_ref()
.map(|fvk| fvk.to_bytes())
.map(unified::Fvk::Orchard),
)
.chain(
self.sapling
.as_ref()
.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 = "transparent-inputs")]
let flag = flag | if self.transparent.is_some() { 1 } else { 0 };
let mut ufvk = vec![flag];
let items = items.chain(
self.transparent
.as_ref()
.map(|tfvk| tfvk.serialize().try_into().unwrap())
.map(unified::Fvk::P2pkh),
);
#[cfg(feature = "transparent-inputs")]
if let Some(transparent) = self.transparent.as_ref() {
ufvk.append(&mut transparent.serialize());
};
if let Some(sapling) = self.sapling.as_ref() {
ufvk.extend_from_slice(&sapling.to_bytes());
}
format!("DONOTUSEUFVK{}", hex::encode(&ufvk))
let ufvk = unified::Ufvk::try_from_items(items.collect())
.expect("UnifiedFullViewingKey should only be constructed safely");
ufvk.encode(&params_to_network(params))
}
/// Returns the transparent component of the unified key at the
@ -348,6 +398,11 @@ mod tests {
fn ufvk_round_trip() {
let account = 0.into();
let orchard = {
let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap();
Some(orchard::keys::FullViewingKey::from(&sk))
};
let sapling = {
let extsk = sapling::spending_key(&[0; 32], 0, account);
Some(ExtendedFullViewingKey::from(&extsk).into())
@ -360,6 +415,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
transparent,
sapling,
orchard,
)
.unwrap();
@ -374,5 +430,9 @@ mod tests {
decoded.sapling.map(|s| s.to_bytes()),
ufvk.sapling.map(|s| s.to_bytes()),
);
assert_eq!(
decoded.orchard.map(|o| o.to_bytes()),
ufvk.orchard.map(|o| o.to_bytes()),
);
}
}

View File

@ -805,6 +805,7 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
tkey,
Some(dfvk.clone()),
None,
)
.unwrap();

View File

@ -180,7 +180,7 @@ pub fn init_wallet_db<P>(wdb: &WalletDb<P>) -> Result<(), rusqlite::Error> {
/// let account = AccountId::from(0);
/// let extsk = sapling::spending_key(&seed, Network::TestNetwork.coin_type(), account);
/// let dfvk = ExtendedFullViewingKey::from(&extsk).into();
/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk)).unwrap();
/// let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
/// init_accounts_table(&db_data, &[ufvk]).unwrap();
/// # }
/// ```
@ -330,6 +330,7 @@ mod tests {
.to_account_pubkey(),
),
Some(dfvk),
None,
)
.unwrap();

View File

@ -222,8 +222,10 @@ mod tests {
transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], AccountId::from(1))
.unwrap();
[
UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0)).unwrap(),
UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1)).unwrap(),
UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None)
.unwrap(),
UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None)
.unwrap(),
]
};
#[cfg(not(feature = "transparent-inputs"))]
@ -281,9 +283,9 @@ mod tests {
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk)).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk)).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
let to = extsk.default_address().1.into();
@ -324,9 +326,9 @@ mod tests {
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0));
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk)).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk)).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
let to = extsk.default_address().1.into();
@ -372,9 +374,9 @@ mod tests {
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0));
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note
@ -505,9 +507,9 @@ mod tests {
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0));
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note
@ -634,9 +636,9 @@ mod tests {
let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), AccountId::from(0));
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note
@ -744,9 +746,9 @@ mod tests {
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), AccountId::from(0));
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
#[cfg(feature = "transparent-inputs")]
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone())).unwrap();
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
init_accounts_table(&db_data, &[ufvk]).unwrap();
// Add funds to the wallet in a single note