Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unified Address parser structural checks #416

Merged
merged 10 commits into from
Jul 12, 2021
12 changes: 12 additions & 0 deletions components/zcash_address/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ pub enum ParseError {
InvalidEncoding,
/// The string is not a Zcash address.
NotZcash,
/// Errors specific to unified addresses.
Unified(unified::ParseError),
}

impl From<unified::ParseError> for ParseError {
fn from(e: unified::ParseError) -> Self {
match e {
unified::ParseError::InvalidEncoding => Self::InvalidEncoding,
_ => Self::Unified(e),
}
}
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::InvalidEncoding => write!(f, "Invalid encoding"),
ParseError::NotZcash => write!(f, "Not a Zcash address"),
ParseError::Unified(e) => e.fmt(f),
}
}
}
Expand Down
168 changes: 152 additions & 16 deletions components/zcash_address/src/kind/unified.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::collections::HashSet;
use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::fmt;
use std::iter;

use crate::{kind, ParseError};
use crate::kind;

mod f4jumble;

Expand All @@ -24,6 +27,77 @@ pub(crate) const REGTEST: &str = "uregtest";

const PADDING_LEN: usize = 16;

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Typecode {
P2pkh,
P2sh,
Sapling,
Orchard,
Unknown(u8),
}

impl From<u8> for Typecode {
fn from(typecode: u8) -> Self {
match typecode {
0x00 => Typecode::P2pkh,
0x01 => Typecode::P2sh,
0x02 => Typecode::Sapling,
0x03 => Typecode::Orchard,
_ => Typecode::Unknown(typecode),
}
}
}

impl From<Typecode> for u8 {
fn from(t: Typecode) -> Self {
match t {
Typecode::P2pkh => 0x00,
Typecode::P2sh => 0x01,
Typecode::Sapling => 0x02,
Typecode::Orchard => 0x03,
Typecode::Unknown(typecode) => typecode,
}
}
}

impl Typecode {
fn is_shielded(&self) -> bool {
match self {
Typecode::P2pkh | Typecode::P2sh => false,
// Assume that unknown typecodes are shielded, because they might be.
_ => true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be better to explicitly list the remaining typecodes here, just for future-proofness. This reminds me, how will TZE interact with the addressing scheme?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in zcash/zips#536, I clarified the requirement that uses this as follows:

  • A Unified Address or Unified Viewing Key MUST NOT contain only
    transparent P2SH or P2PKH addresses (Typecodes :math:\mathtt{0x00}
    and :math:\mathtt{0x01}). The rationale is that the existing
    P2SH and P2PKH transparent-only address formats suffice for this
    purpose and are already supported by the existing ecosystem.

So it is explicitly only P2SH and P2PKH addresses that are treated as transparent here (which fits with the rationale).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nuttycom we cannot list the remaining typecodes here, because they include Typecode::Unknown which is explicitly unbounded. As for TZE: we would need to decide at the time whether the property we want is "UA must contain a shielded component" (which would preclude TZE-only addrs) or "UA must not contain only a t-address-equivalent component" (which would enable the creation of non-shielded UAs). I suspect the latter is what we will want, treating UAs as the adapter mechanism for plugging in new receivers rather than needing to define a new address encoding every time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The presence of Typecode::Unknown makes me tempted to say that this function should actually return an Option<Bool> because otherwise callers can't use it in isolation of other checks against the typecode. This is probably fine; it just means that this won't be flagged by the compiler as a call site that needs inspection if the set of receivers changes. I'm just lazy, I want the compiler to do that work for me, rather than having to remember. :)

}
}
str4d marked this conversation as resolved.
Show resolved Hide resolved
}

/// An error while attempting to parse a string as a Zcash address.
#[derive(Debug, PartialEq)]
pub enum ParseError {
/// The unified address contains both P2PKH and P2SH receivers.
BothP2phkAndP2sh,
/// The unified address contains a duplicated typecode.
DuplicateTypecode(Typecode),
/// The string is an invalid encoding.
InvalidEncoding,
/// The unified address only contains transparent receivers.
OnlyTransparent,
}

impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::BothP2phkAndP2sh => write!(f, "UA contains both P2PKH and P2SH receivers"),
ParseError::DuplicateTypecode(typecode) => {
write!(f, "Duplicate typecode {}", u8::from(*typecode))
}
ParseError::InvalidEncoding => write!(f, "Invalid encoding"),
ParseError::OnlyTransparent => write!(f, "UA only contains transparent receivers"),
}
}
}

impl Error for ParseError {}

/// The set of known Receivers for Unified Addresses.
///
/// This enum is an internal-only type, and is maintained in preference order, so that the
Expand All @@ -44,12 +118,12 @@ impl TryFrom<(u8, &[u8])> for Receiver {
type Error = ParseError;

fn try_from((typecode, addr): (u8, &[u8])) -> Result<Self, Self::Error> {
match typecode {
0x00 => addr.try_into().map(Receiver::P2pkh),
0x01 => addr.try_into().map(Receiver::P2sh),
0x02 => addr.try_into().map(Receiver::Sapling),
0x03 => addr.try_into().map(Receiver::Orchard),
_ => Ok(Receiver::Unknown {
match typecode.into() {
Typecode::P2pkh => addr.try_into().map(Receiver::P2pkh),
Typecode::P2sh => addr.try_into().map(Receiver::P2sh),
Typecode::Sapling => addr.try_into().map(Receiver::Sapling),
Typecode::Orchard => addr.try_into().map(Receiver::Orchard),
Typecode::Unknown(_) => Ok(Receiver::Unknown {
typecode,
data: addr.to_vec(),
}),
Expand All @@ -59,13 +133,13 @@ impl TryFrom<(u8, &[u8])> for Receiver {
}

impl Receiver {
fn typecode(&self) -> u8 {
fn typecode(&self) -> Typecode {
match self {
Receiver::P2pkh(_) => 0x00,
Receiver::P2sh(_) => 0x01,
Receiver::Sapling(_) => 0x02,
Receiver::Orchard(_) => 0x03,
Receiver::Unknown { typecode, .. } => *typecode,
Receiver::P2pkh(_) => Typecode::P2pkh,
Receiver::P2sh(_) => Typecode::P2sh,
Receiver::Sapling(_) => Typecode::Sapling,
Receiver::Orchard(_) => Typecode::Orchard,
Receiver::Unknown { typecode, .. } => Typecode::Unknown(*typecode),
}
}

Expand Down Expand Up @@ -113,7 +187,28 @@ impl TryFrom<&[u8]> for Address {
_ => Some(Err(ParseError::InvalidEncoding)),
})
.collect::<Result<_, _>>()
.map(Address)
.and_then(|receivers: Vec<Receiver>| {
let mut typecodes = HashSet::with_capacity(receivers.len());
for receiver in &receivers {
let t = receiver.typecode();
if typecodes.contains(&t) {
return Err(ParseError::DuplicateTypecode(t));
} else if (t == Typecode::P2pkh && typecodes.contains(&Typecode::P2sh))
|| (t == Typecode::P2sh && typecodes.contains(&Typecode::P2pkh))
{
return Err(ParseError::BothP2phkAndP2sh);
} else {
typecodes.insert(t);
}
}

if !typecodes.iter().any(|t| t.is_shielded()) {
str4d marked this conversation as resolved.
Show resolved Hide resolved
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Address(receivers))
}
})
}
}

Expand All @@ -129,7 +224,7 @@ impl Address {
assert!(addr.len() < 256);

iter::empty()
.chain(Some(receiver.typecode()))
.chain(Some(receiver.typecode().into()))
.chain(Some(addr.len() as u8))
.chain(addr.iter().cloned())
})
Expand All @@ -149,7 +244,7 @@ mod tests {
prelude::*,
};

use super::{Address, Receiver};
use super::{Address, ParseError, Receiver, Typecode};

prop_compose! {
fn uniform43()(a in uniform11(0u8..), b in uniform32(0u8..)) -> [u8; 43] {
Expand Down Expand Up @@ -193,4 +288,45 @@ mod tests {
prop_assert_eq!(decoded, Ok(ua));
}
}

#[test]
fn duplicate_typecode() {
// Construct and serialize an invalid UA.
let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]);
let encoded = ua.to_bytes();
assert_eq!(
Address::try_from(&encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
);
}

#[test]
fn p2pkh_and_p2sh() {
// Construct and serialize an invalid UA.
let ua = Address(vec![Receiver::P2pkh([0; 20]), Receiver::P2sh([0; 20])]);
let encoded = ua.to_bytes();
assert_eq!(
Address::try_from(&encoded[..]),
Err(ParseError::BothP2phkAndP2sh)
);
}

#[test]
fn only_transparent() {
// Encoding of `Address(vec![Receiver::P2pkh([0; 20])])`.
let encoded = vec![
0x3b, 0x3d, 0xe6, 0xb3, 0xed, 0xaa, 0x0a, 0x36, 0x12, 0xbc, 0x8d, 0x2b, 0x1a, 0xaa,
0x27, 0x7e, 0x45, 0xc0, 0xc2, 0x0e, 0xf9, 0x6f, 0x24, 0x9b, 0x79, 0x0a, 0x68, 0x76,
0xa8, 0x4c, 0x3f, 0xf0, 0x1f, 0x39, 0x97, 0xbd, 0x15, 0x0d,
];

// We can't actually exercise this error, because at present the only transparent
// receivers we can use are P2PKH and P2SH (which cannot be used together), and
// with only one of them we don't have sufficient data for F4Jumble (so we hit a
// different error).
assert_eq!(
Address::try_from(&encoded[..]),
Err(ParseError::InvalidEncoding)
);
}
}