660 lines
25 KiB
Rust
660 lines
25 KiB
Rust
//! Implementation of [ZIP 316](https://zips.z.cash/zip-0316) Unified Addresses and Viewing Keys.
|
|
|
|
use bech32::{self, FromBase32, ToBase32, Variant};
|
|
use std::cmp;
|
|
use std::convert::{TryFrom, TryInto};
|
|
use std::error::Error;
|
|
use std::fmt;
|
|
use std::num::TryFromIntError;
|
|
use zcash_encoding::MAX_COMPACT_SIZE;
|
|
|
|
use crate::Network;
|
|
|
|
pub(crate) mod address;
|
|
pub(crate) mod fvk;
|
|
pub(crate) mod ivk;
|
|
|
|
pub use address::{Address, Receiver};
|
|
pub use fvk::{Fvk, Ufvk};
|
|
pub use ivk::{Ivk, Uivk};
|
|
|
|
const PADDING_LEN: usize = 16;
|
|
|
|
/// The known Receiver and Viewing Key types.
|
|
///
|
|
/// The typecodes `0xFFFA..=0xFFFF` reserved for experiments are currently not
|
|
/// distinguished from unknown values, and will be parsed as [`DataTypecode::Unknown`].
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum DataTypecode {
|
|
/// A transparent P2PKH address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
|
|
P2pkh,
|
|
/// A transparent P2SH address.
|
|
///
|
|
/// This typecode cannot occur in a [`Ufvk`] or [`Uivk`].
|
|
P2sh,
|
|
/// A Sapling raw address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
|
|
Sapling,
|
|
/// An Orchard raw address, FVK, or IVK encoding as specified in [ZIP 316](https://zips.z.cash/zip-0316).
|
|
Orchard,
|
|
/// An unknown or experimental typecode.
|
|
Unknown(u32),
|
|
}
|
|
|
|
impl TryFrom<u32> for DataTypecode {
|
|
type Error = ();
|
|
|
|
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
|
match typecode {
|
|
0x00 => Ok(DataTypecode::P2pkh),
|
|
0x01 => Ok(DataTypecode::P2sh),
|
|
0x02 => Ok(DataTypecode::Sapling),
|
|
0x03 => Ok(DataTypecode::Orchard),
|
|
0x04..=0xBF | 0xFD..=MAX_COMPACT_SIZE => Ok(DataTypecode::Unknown(typecode)),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DataTypecode> for u32 {
|
|
fn from(t: DataTypecode) -> Self {
|
|
match t {
|
|
DataTypecode::P2pkh => 0x00,
|
|
DataTypecode::P2sh => 0x01,
|
|
DataTypecode::Sapling => 0x02,
|
|
DataTypecode::Orchard => 0x03,
|
|
DataTypecode::Unknown(typecode) => typecode,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DataTypecode {
|
|
/// A total ordering over the data typecodes that can be used to sort
|
|
/// receivers and/or key items in order of decreasing priority,
|
|
/// as specified in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
|
|
pub fn preference_order(a: &Self, b: &Self) -> cmp::Ordering {
|
|
match (a, b) {
|
|
// Trivial equality checks.
|
|
(Self::Orchard, Self::Orchard)
|
|
| (Self::Sapling, Self::Sapling)
|
|
| (Self::P2sh, Self::P2sh)
|
|
| (Self::P2pkh, Self::P2pkh) => cmp::Ordering::Equal,
|
|
|
|
// We don't know for certain the preference order of unknown items, but it
|
|
// is likely that the higher typecode has higher preference. The exact order
|
|
// doesn't really matter, as unknown items have lower preference than
|
|
// known items.
|
|
(Self::Unknown(a), Self::Unknown(b)) => b.cmp(a),
|
|
|
|
// For the remaining cases, we rely on `match` always choosing the first arm
|
|
// with a matching pattern. Patterns below are listed in priority order:
|
|
(Self::Orchard, _) => cmp::Ordering::Less,
|
|
(_, Self::Orchard) => cmp::Ordering::Greater,
|
|
|
|
(Self::Sapling, _) => cmp::Ordering::Less,
|
|
(_, Self::Sapling) => cmp::Ordering::Greater,
|
|
|
|
(Self::P2sh, _) => cmp::Ordering::Less,
|
|
(_, Self::P2sh) => cmp::Ordering::Greater,
|
|
|
|
(Self::P2pkh, _) => cmp::Ordering::Less,
|
|
(_, Self::P2pkh) => cmp::Ordering::Greater,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The known Metadata Typecodes
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum MetadataTypecode {
|
|
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
|
ExpiryHeight,
|
|
/// Expiration height metadata as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
|
ExpiryTime,
|
|
/// An unknown MUST-understand metadata item as specified in
|
|
/// [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
|
///
|
|
/// A parser encountering this typecode MUST halt with an error.
|
|
MustUnderstand(u32),
|
|
/// An unknown metadata item as specified in [ZIP 316, Revision 1](https://zips.z.cash/zip-0316)
|
|
Unknown(u32),
|
|
}
|
|
|
|
impl TryFrom<u32> for MetadataTypecode {
|
|
type Error = ();
|
|
|
|
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
|
match typecode {
|
|
0xC0..=0xDF => Ok(MetadataTypecode::Unknown(typecode)),
|
|
0xE0 => Ok(MetadataTypecode::ExpiryHeight),
|
|
0xE1 => Ok(MetadataTypecode::ExpiryTime),
|
|
0xE2..=0xFC => Ok(MetadataTypecode::MustUnderstand(typecode)),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<MetadataTypecode> for u32 {
|
|
fn from(t: MetadataTypecode) -> Self {
|
|
match t {
|
|
MetadataTypecode::ExpiryHeight => 0xE0,
|
|
MetadataTypecode::ExpiryTime => 0xE1,
|
|
MetadataTypecode::MustUnderstand(value) => value,
|
|
MetadataTypecode::Unknown(value) => value,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An enumeration of the Unified Container Item Typecodes.
|
|
///
|
|
/// Unified Address Items are partitioned into two sets: data items, which include
|
|
/// receivers and viewing keys, and metadata items.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Typecode {
|
|
/// A data (receiver or viewing key) typecode.
|
|
Data(DataTypecode),
|
|
/// A metadata typecode.
|
|
Metadata(MetadataTypecode),
|
|
}
|
|
|
|
impl Typecode {
|
|
/// The typecode for p2pkh data items.
|
|
pub const P2PKH: Typecode = Typecode::Data(DataTypecode::P2pkh);
|
|
/// The typecode for p2sh data items.
|
|
pub const P2SH: Typecode = Typecode::Data(DataTypecode::P2sh);
|
|
/// The typecode for Sapling data items.
|
|
pub const SAPLING: Typecode = Typecode::Data(DataTypecode::Sapling);
|
|
/// The typecode for Orchard data items.
|
|
pub const ORCHARD: Typecode = Typecode::Data(DataTypecode::Orchard);
|
|
}
|
|
|
|
impl TryFrom<u32> for Typecode {
|
|
type Error = ParseError;
|
|
|
|
fn try_from(typecode: u32) -> Result<Self, Self::Error> {
|
|
DataTypecode::try_from(typecode)
|
|
.map_or_else(
|
|
|()| MetadataTypecode::try_from(typecode).map(Typecode::Metadata),
|
|
|t| Ok(Typecode::Data(t)),
|
|
)
|
|
.map_err(|()| ParseError::InvalidTypecodeValue(typecode))
|
|
}
|
|
}
|
|
|
|
impl From<Typecode> for u32 {
|
|
fn from(t: Typecode) -> Self {
|
|
match t {
|
|
Typecode::Data(tc) => tc.into(),
|
|
Typecode::Metadata(tc) => tc.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<Typecode> for usize {
|
|
type Error = TryFromIntError;
|
|
|
|
fn try_from(t: Typecode) -> Result<Self, Self::Error> {
|
|
u32::from(t).try_into()
|
|
}
|
|
}
|
|
|
|
/// An enumeration of known Unified Metadata Item types.
|
|
///
|
|
/// Unknown MUST-understand metadata items are NOT represented using this type, as the presence of
|
|
/// an unrecognized metadata item with a typecode in the `MUST-understand` range will result in a
|
|
/// parse failure, instead of the construction of a metadata item.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub enum MetadataItem {
|
|
/// The expiry height for a Unified Address or Unified Viewing Key
|
|
ExpiryHeight(u32),
|
|
/// The expiry time for a Unified Address or Unified Viewing Key
|
|
ExpiryTime(u64),
|
|
/// A Metadata Item with an unrecognized Typecode. MUST-understand metadata items are NOT
|
|
/// represented using this type, as the presence of an unrecognized metadata item with a
|
|
/// typecode in the `MUST-understand` range will result in a parse failure.
|
|
Unknown { typecode: u32, data: Vec<u8> },
|
|
}
|
|
|
|
impl MetadataItem {
|
|
/// Parse a metadata item for the specified metadata typecode from the provided bytes.
|
|
pub fn parse(
|
|
revision: Revision,
|
|
typecode: MetadataTypecode,
|
|
data: &[u8],
|
|
) -> Result<Self, ParseError> {
|
|
use MetadataTypecode::*;
|
|
use Revision::*;
|
|
match (revision, typecode) {
|
|
(R1, ExpiryHeight) => data
|
|
.try_into()
|
|
.map(u32::from_le_bytes)
|
|
.map(MetadataItem::ExpiryHeight)
|
|
.map_err(|_| {
|
|
ParseError::InvalidEncoding(
|
|
"Expiry height must be a 32-bit little-endian value.".to_string(),
|
|
)
|
|
}),
|
|
(R1, ExpiryTime) => data
|
|
.try_into()
|
|
.map(u64::from_le_bytes)
|
|
.map(MetadataItem::ExpiryTime)
|
|
.map_err(|_| {
|
|
ParseError::InvalidEncoding(
|
|
"Expiry time must be a 64-bit little-endian value.".to_string(),
|
|
)
|
|
}),
|
|
(R0, ExpiryHeight | ExpiryTime) => Err(ParseError::NotUnderstood(typecode.into())),
|
|
(R0 | R1, MustUnderstand(tc)) => Err(ParseError::NotUnderstood(tc)),
|
|
// This implementation treats the 0xC0..OxFD range as unknown metadata for both R0 and
|
|
// R1, as no typecodes were specified in this range for R0 and were "reclaimed" as
|
|
// metadata codes by ZIP 316 at the time R1 was introduced.
|
|
(R0 | R1, Unknown(typecode)) => Ok(MetadataItem::Unknown {
|
|
typecode,
|
|
data: data.to_vec(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Returns the typecode for this metadata item.
|
|
pub fn typecode(&self) -> MetadataTypecode {
|
|
match self {
|
|
MetadataItem::ExpiryHeight(_) => MetadataTypecode::ExpiryHeight,
|
|
MetadataItem::ExpiryTime(_) => MetadataTypecode::ExpiryTime,
|
|
MetadataItem::Unknown { typecode, .. } => MetadataTypecode::Unknown(*typecode),
|
|
}
|
|
}
|
|
|
|
/// Returns the raw bytes of this metadata item.
|
|
pub fn data(&self) -> Vec<u8> {
|
|
match self {
|
|
MetadataItem::ExpiryHeight(h) => h.to_le_bytes().to_vec(),
|
|
MetadataItem::ExpiryTime(t) => t.to_le_bytes().to_vec(),
|
|
MetadataItem::Unknown { data, .. } => data.clone(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A Unified Encoding Item.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub enum Item<T> {
|
|
/// A data item; either a receiver (for Unified Addresses) or a key (for Unified Viewing Keys)
|
|
Data(T),
|
|
/// A metadata item.
|
|
Metadata(MetadataItem),
|
|
}
|
|
|
|
impl<T: private::SealedDataItem> Item<T> {
|
|
/// Returns the typecode for this item.
|
|
pub fn typecode(&self) -> Typecode {
|
|
match self {
|
|
Item::Data(d) => Typecode::Data(d.typecode()),
|
|
Item::Metadata(m) => Typecode::Metadata(m.typecode()),
|
|
}
|
|
}
|
|
|
|
/// The total ordering over items by their typecodes, used for encoding as specified
|
|
/// in [ZIP 316](https://zips.z.cash/zip-0316#encoding-of-unified-addresses)
|
|
pub fn encoding_order(a: &Self, b: &Self) -> cmp::Ordering {
|
|
u32::from(a.typecode()).cmp(&u32::from(b.typecode()))
|
|
}
|
|
|
|
/// Returns the raw binary representation of the data for this item.
|
|
pub fn data(&self) -> Vec<u8> {
|
|
match self {
|
|
Item::Data(d) => d.data().to_vec(),
|
|
Item::Metadata(m) => m.data(),
|
|
}
|
|
}
|
|
|
|
/// Returns whether this item is a transparent receiver or key.
|
|
pub fn is_transparent_data_item(&self) -> bool {
|
|
self.typecode() == Typecode::P2PKH || self.typecode() == Typecode::P2SH
|
|
}
|
|
}
|
|
|
|
/// An error while attempting to parse a string as a Zcash address.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum ParseError {
|
|
/// The unified container contains both P2PKH and P2SH items.
|
|
BothP2phkAndP2sh,
|
|
/// The unified container contains a duplicated typecode.
|
|
DuplicateTypecode(Typecode),
|
|
/// The parsed typecode exceeds the maximum allowed CompactSize value.
|
|
InvalidTypecodeValue(u32),
|
|
/// The string is an invalid encoding.
|
|
InvalidEncoding(String),
|
|
/// The items in the unified container are not in typecode order.
|
|
InvalidTypecodeOrder,
|
|
/// The unified container only contains transparent items.
|
|
OnlyTransparent,
|
|
/// The string is not Bech32m encoded, and so cannot be a unified address.
|
|
NotUnified,
|
|
/// The Bech32m string has an unrecognized human-readable prefix.
|
|
UnknownPrefix(String),
|
|
/// A `MUST-understand` metadata item was not recognized.
|
|
NotUnderstood(u32),
|
|
}
|
|
|
|
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 items"),
|
|
ParseError::DuplicateTypecode(c) => write!(f, "Duplicate typecode {}", u32::from(*c)),
|
|
ParseError::InvalidTypecodeValue(v) => write!(f, "Typecode value out of range {}", v),
|
|
ParseError::InvalidEncoding(msg) => write!(f, "Invalid encoding: {}", msg),
|
|
ParseError::InvalidTypecodeOrder => write!(f, "Items are out of order."),
|
|
ParseError::OnlyTransparent => write!(f, "UA only contains transparent items"),
|
|
ParseError::NotUnified => write!(f, "Address is not Bech32m encoded"),
|
|
ParseError::UnknownPrefix(s) => {
|
|
write!(f, "Unrecognized Bech32m human-readable prefix: {}", s)
|
|
}
|
|
ParseError::NotUnderstood(tc) => {
|
|
write!(
|
|
f,
|
|
"MUST-understand metadata item with typecode {} was not recognized; please upgrade.",
|
|
tc
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for ParseError {}
|
|
|
|
/// The revision of the Unified Address standard that an address was parsed under.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Revision {
|
|
R0,
|
|
R1,
|
|
}
|
|
|
|
pub(crate) mod private {
|
|
use super::{DataTypecode, ParseError, Revision, Typecode, PADDING_LEN};
|
|
use crate::{
|
|
unified::{Item, MetadataItem},
|
|
Network,
|
|
};
|
|
use std::{
|
|
convert::{TryFrom, TryInto},
|
|
io::Write,
|
|
};
|
|
use zcash_encoding::CompactSize;
|
|
|
|
/// A raw address or viewing key.
|
|
pub trait SealedDataItem: Clone {
|
|
/// Parse a data item for the specified data typecode from the provided bytes.
|
|
fn parse(tc: DataTypecode, value: &[u8]) -> Result<Self, ParseError>;
|
|
|
|
/// Returns the typecode of this data item.
|
|
fn typecode(&self) -> DataTypecode;
|
|
|
|
/// Returns the raw bytes of this data item.
|
|
fn data(&self) -> &[u8];
|
|
}
|
|
|
|
/// A Unified Container containing addresses or viewing keys.
|
|
pub trait SealedContainer: super::Container + std::marker::Sized {
|
|
const MAINNET_R0: &'static str;
|
|
const TESTNET_R0: &'static str;
|
|
const REGTEST_R0: &'static str;
|
|
|
|
const MAINNET_R1: &'static str;
|
|
const TESTNET_R1: &'static str;
|
|
const REGTEST_R1: &'static str;
|
|
|
|
/// Implementations of this method should act as unchecked constructors
|
|
/// of the container type; the caller is guaranteed to check the
|
|
/// general invariants that apply to all unified containers.
|
|
fn from_inner(revision: Revision, items: Vec<Item<Self::DataItem>>) -> Self;
|
|
|
|
fn network_hrp(revision: Revision, network: &Network) -> &'static str {
|
|
match (revision, network) {
|
|
(Revision::R0, Network::Main) => Self::MAINNET_R0,
|
|
(Revision::R0, Network::Test) => Self::TESTNET_R0,
|
|
(Revision::R0, Network::Regtest) => Self::REGTEST_R0,
|
|
(Revision::R1, Network::Main) => Self::MAINNET_R1,
|
|
(Revision::R1, Network::Test) => Self::TESTNET_R1,
|
|
(Revision::R1, Network::Regtest) => Self::REGTEST_R1,
|
|
}
|
|
}
|
|
|
|
fn hrp_revision(hrp: &str) -> Option<Revision> {
|
|
if hrp == Self::MAINNET_R0 || hrp == Self::TESTNET_R0 || hrp == Self::REGTEST_R0 {
|
|
Some(Revision::R0)
|
|
} else if hrp == Self::MAINNET_R1 || hrp == Self::TESTNET_R1 || hrp == Self::REGTEST_R1
|
|
{
|
|
Some(Revision::R1)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn hrp_network(hrp: &str) -> Option<Network> {
|
|
if hrp == Self::MAINNET_R0 || hrp == Self::MAINNET_R1 {
|
|
Some(Network::Main)
|
|
} else if hrp == Self::TESTNET_R0 || hrp == Self::TESTNET_R1 {
|
|
Some(Network::Test)
|
|
} else if hrp == Self::REGTEST_R0 || hrp == Self::REGTEST_R1 {
|
|
Some(Network::Regtest)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn write_raw_encoding<W: Write>(&self, mut writer: W) {
|
|
for item in self.items_as_parsed() {
|
|
let data = item.data();
|
|
CompactSize::write(
|
|
&mut writer,
|
|
<u32>::from(item.typecode()).try_into().unwrap(),
|
|
)
|
|
.unwrap();
|
|
CompactSize::write(&mut writer, data.len()).unwrap();
|
|
writer.write_all(&data).unwrap();
|
|
}
|
|
}
|
|
|
|
/// Returns the jumbled padded raw encoding of this Unified Address or viewing key.
|
|
fn to_jumbled_bytes(&self, hrp: &str) -> Vec<u8> {
|
|
assert!(hrp.len() <= PADDING_LEN);
|
|
|
|
let mut writer = std::io::Cursor::new(Vec::new());
|
|
self.write_raw_encoding(&mut writer);
|
|
|
|
let mut padding = [0u8; PADDING_LEN];
|
|
padding[0..hrp.len()].copy_from_slice(hrp.as_bytes());
|
|
writer.write_all(&padding).unwrap();
|
|
|
|
let padded = writer.into_inner();
|
|
f4jumble::f4jumble(&padded)
|
|
.unwrap_or_else(|e| panic!("f4jumble failed on {:?}: {}", padded, e))
|
|
}
|
|
|
|
/// Parse the items of the unified container.
|
|
#[allow(clippy::type_complexity)]
|
|
fn parse_items<T: Into<Vec<u8>>>(
|
|
hrp: &str,
|
|
buf: T,
|
|
) -> Result<(Revision, Vec<Item<Self::DataItem>>), ParseError> {
|
|
fn read_item<R: SealedDataItem>(
|
|
revision: Revision,
|
|
mut cursor: &mut std::io::Cursor<&[u8]>,
|
|
) -> Result<Item<R>, ParseError> {
|
|
let typecode = CompactSize::read(&mut cursor)
|
|
.map(|v| u32::try_from(v).expect("CompactSize::read enforces MAX_SIZE limit"))
|
|
.map_err(|e| {
|
|
ParseError::InvalidEncoding(format!(
|
|
"Failed to deserialize CompactSize-encoded typecode {}",
|
|
e
|
|
))
|
|
})?;
|
|
let length = CompactSize::read(&mut cursor).map_err(|e| {
|
|
ParseError::InvalidEncoding(format!(
|
|
"Failed to deserialize CompactSize-encoded length {}",
|
|
e
|
|
))
|
|
})?;
|
|
let addr_end = cursor.position().checked_add(length).ok_or_else(|| {
|
|
ParseError::InvalidEncoding(format!(
|
|
"Length value {} caused an overflow error",
|
|
length
|
|
))
|
|
})?;
|
|
let buf = cursor.get_ref();
|
|
if (buf.len() as u64) < addr_end {
|
|
return Err(ParseError::InvalidEncoding(format!(
|
|
"Truncated: unable to read {} bytes of item data",
|
|
length
|
|
)));
|
|
}
|
|
// The "as usize" casts cannot change the values, because both
|
|
// cursor.position() and addr_end are u64 values <= buf.len()
|
|
// which is usize.
|
|
let data = &buf[cursor.position() as usize..addr_end as usize];
|
|
let result = match Typecode::try_from(typecode)? {
|
|
Typecode::Data(tc) => Item::Data(R::parse(tc, data)?),
|
|
Typecode::Metadata(tc) => {
|
|
Item::Metadata(MetadataItem::parse(revision, tc, data)?)
|
|
}
|
|
};
|
|
cursor.set_position(addr_end);
|
|
Ok(result)
|
|
}
|
|
|
|
// Here we allocate if necessary to get a mutable Vec<u8> to unjumble.
|
|
let mut encoded = buf.into();
|
|
f4jumble::f4jumble_inv_mut(&mut encoded[..]).map_err(|e| {
|
|
ParseError::InvalidEncoding(format!("F4Jumble decoding failed: {}", e))
|
|
})?;
|
|
|
|
// Validate and strip trailing padding bytes.
|
|
if hrp.len() > 16 {
|
|
return Err(ParseError::InvalidEncoding(
|
|
"Invalid human-readable part".to_owned(),
|
|
));
|
|
}
|
|
let mut expected_padding = [0; PADDING_LEN];
|
|
expected_padding[0..hrp.len()].copy_from_slice(hrp.as_bytes());
|
|
let encoded = match encoded.split_at(encoded.len() - PADDING_LEN) {
|
|
(encoded, tail) if tail == expected_padding => Ok(encoded),
|
|
_ => Err(ParseError::InvalidEncoding(
|
|
"Invalid padding bytes".to_owned(),
|
|
)),
|
|
}?;
|
|
|
|
let revision = Self::hrp_revision(hrp)
|
|
.ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
|
|
|
|
let mut cursor = std::io::Cursor::new(encoded);
|
|
let mut result = vec![];
|
|
while cursor.position() < encoded.len().try_into().unwrap() {
|
|
result.push(read_item(revision, &mut cursor)?);
|
|
}
|
|
assert_eq!(cursor.position(), encoded.len().try_into().unwrap());
|
|
|
|
Ok((revision, result))
|
|
}
|
|
|
|
/// A private function that constructs a unified container with the
|
|
/// specified items, which must be in ascending typecode order.
|
|
fn try_from_items_internal(
|
|
revision: Revision,
|
|
items: Vec<Item<Self::DataItem>>,
|
|
) -> Result<Self, ParseError> {
|
|
assert!(u32::from(Typecode::P2SH) == u32::from(Typecode::P2PKH) + 1);
|
|
|
|
let mut prev_code = None; // less than any Some
|
|
for item in &items {
|
|
let t = item.typecode();
|
|
let t_code = Some(u32::from(t));
|
|
if t_code < prev_code {
|
|
return Err(ParseError::InvalidTypecodeOrder);
|
|
} else if t_code == prev_code {
|
|
return Err(ParseError::DuplicateTypecode(t));
|
|
} else if t == Typecode::P2SH && prev_code == Some(u32::from(DataTypecode::P2pkh)) {
|
|
// P2pkh and P2sh can only be in that order and next to each other,
|
|
// otherwise we would detect an out-of-order or duplicate typecode.
|
|
return Err(ParseError::BothP2phkAndP2sh);
|
|
} else {
|
|
prev_code = t_code;
|
|
}
|
|
}
|
|
|
|
// All checks pass!
|
|
Ok(Self::from_inner(revision, items))
|
|
}
|
|
|
|
fn parse_internal<T: Into<Vec<u8>>>(hrp: &str, buf: T) -> Result<Self, ParseError> {
|
|
Self::parse_items(hrp, buf)
|
|
.and_then(|(revision, items)| Self::try_from_items_internal(revision, items))
|
|
}
|
|
}
|
|
}
|
|
|
|
use private::SealedDataItem;
|
|
|
|
/// Trait providing common encoding and decoding logic for Unified containers.
|
|
pub trait Encoding: private::SealedContainer {
|
|
/// Constructs a value of a unified container type from a vector of container
|
|
/// items. These items will be sorted according to typecode as specified in ZIP
|
|
/// 316, so this method is not necessarily round-trip compatible with
|
|
/// [`Container::items_as_parsed`].
|
|
///
|
|
/// This function will return an error in the case that the following ZIP 316
|
|
/// invariants concerning the composition of a unified container are
|
|
/// violated:
|
|
/// * the item list may not contain two items having the same typecode
|
|
/// * the item list may not contain both P2PKH and P2SH items.
|
|
fn try_from_items(
|
|
revision: Revision,
|
|
mut items: Vec<Item<Self::DataItem>>,
|
|
) -> Result<Self, ParseError> {
|
|
items.sort_unstable_by(Item::encoding_order);
|
|
Self::try_from_items_internal(revision, items)
|
|
}
|
|
|
|
/// Decodes a unified container from its string representation, preserving
|
|
/// the order of its components so that it correctly obeys round-trip
|
|
/// serialization invariants.
|
|
fn decode(s: &str) -> Result<(Network, Self), ParseError> {
|
|
if let Ok((hrp, data, Variant::Bech32m)) = bech32::decode(s) {
|
|
let hrp = hrp.as_str();
|
|
// validate that the HRP corresponds to a known network.
|
|
let net =
|
|
Self::hrp_network(hrp).ok_or_else(|| ParseError::UnknownPrefix(hrp.to_string()))?;
|
|
|
|
let data = Vec::<u8>::from_base32(&data)
|
|
.map_err(|e| ParseError::InvalidEncoding(e.to_string()))?;
|
|
|
|
Self::parse_internal(hrp, data).map(|value| (net, value))
|
|
} else {
|
|
Err(ParseError::NotUnified)
|
|
}
|
|
}
|
|
|
|
/// Encodes the contents of the unified container to its string representation
|
|
/// using the correct constants for the specified network, preserving the
|
|
/// ordering of the contained items such that it correctly obeys round-trip
|
|
/// serialization invariants.
|
|
fn encode(&self, network: &Network) -> String {
|
|
let hrp = Self::network_hrp(self.revision(), network);
|
|
bech32::encode(
|
|
hrp,
|
|
self.to_jumbled_bytes(hrp).to_base32(),
|
|
Variant::Bech32m,
|
|
)
|
|
.expect("hrp is invalid")
|
|
}
|
|
}
|
|
|
|
/// Trait for for Unified containers, that exposes the items within them.
|
|
pub trait Container {
|
|
/// The type of data items in this unified container.
|
|
type DataItem: SealedDataItem;
|
|
|
|
/// Returns the items in encoding order.
|
|
fn items_as_parsed(&self) -> &[Item<Self::DataItem>];
|
|
|
|
/// Returns the revision of the ZIP 316 standard that this unified container
|
|
/// conforms to.
|
|
fn revision(&self) -> Revision;
|
|
}
|