Make unified container construction correctly sort items.

Newly constructed unified containers now obey ZIP 316 item
ordering rules, while parsing and reserialization preserve
order.
This commit is contained in:
Kris Nuttycombe 2021-12-07 10:42:05 -07:00
parent e7c57e4a02
commit 4e906508ae
4 changed files with 84 additions and 45 deletions

View File

@ -1,6 +1,5 @@
use bech32::{self, FromBase32, ToBase32, Variant};
use std::cmp;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
@ -141,6 +140,7 @@ pub(crate) mod private {
use crate::Network;
use std::{
cmp,
collections::HashSet,
convert::{TryFrom, TryInto},
io::Write,
};
@ -155,7 +155,7 @@ pub(crate) mod private {
}
/// A Unified Container containing addresses or viewing keys.
pub trait SealedContainer: super::Container {
pub trait SealedContainer: super::Container + std::marker::Sized {
const MAINNET: &'static str;
const TESTNET: &'static str;
const REGTEST: &'static str;
@ -281,36 +281,73 @@ pub(crate) mod private {
Ok(result)
}
/// A private function that constructs a unified container with the
/// items in their given order.
fn try_from_items_internal(items: Vec<Self::Item>) -> Result<Self, ParseError> {
let mut typecodes = HashSet::with_capacity(items.len());
for item in &items {
let t = item.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().all(|t| t.is_transparent()) {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
}
}
fn parse_internal(hrp: &str, buf: &[u8]) -> Result<Self, ParseError> {
Self::parse_items(hrp, buf).and_then(Self::try_from_items_internal)
}
}
}
use private::SealedItem;
/// Trait providing common encoding and decoding logic for Unified containers.
pub trait Encoding: private::SealedContainer + std::marker::Sized {
fn try_from_items(items: Vec<Self::Item>) -> Result<Self, ParseError> {
let mut typecodes = HashSet::with_capacity(items.len());
for item in &items {
let t = item.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().all(|t| t.is_transparent()) {
Err(ParseError::OnlyTransparent)
} else {
// All checks pass!
Ok(Self::from_inner(items))
}
pub trait Encoding: private::SealedContainer {
/// Constructs a value of a unified container type from a vector
/// of container items, sorted according to typecode as specified
/// in ZIP 316.
///
/// 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 only a single transparent item
fn try_from_items(mut items: Vec<Self::Item>) -> Result<Self, ParseError> {
items.sort_unstable_by_key(|i| i.typecode());
Self::try_from_items_internal(items)
}
/// Constructs a value of a unified container type from a vector
/// of container items, preserving the order of the provided vector
/// in the serialized form, potentially contravening the ordering
/// recommended by ZIP 316.
///
/// 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 only a single transparent item
fn try_from_items_preserving_order(items: Vec<Self::Item>) -> Result<Self, ParseError> {
Self::try_from_items_internal(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();
@ -321,14 +358,16 @@ pub trait Encoding: private::SealedContainer + std::marker::Sized {
let data = Vec::<u8>::from_base32(&data)
.map_err(|e| ParseError::InvalidEncoding(e.to_string()))?;
Self::parse_items(hrp, &data[..])
.and_then(Self::try_from_items)
.map(|value| (net, value))
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(network);
bech32::encode(

View File

@ -213,7 +213,7 @@ mod tests {
0x7b, 0x28, 0x69, 0xc9, 0x84,
];
assert_eq!(
Address::parse_items(Address::MAINNET, &invalid_padding[..]),
Address::parse_internal(Address::MAINNET, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -228,7 +228,7 @@ mod tests {
0x4b, 0x31, 0xee, 0x5a,
];
assert_eq!(
Address::parse_items(Address::MAINNET, &truncated_padding[..]),
Address::parse_internal(Address::MAINNET, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -253,7 +253,7 @@ mod tests {
0xc6, 0x5e, 0x68, 0xa2, 0x78, 0x6c, 0x9e,
];
assert_matches!(
Address::parse_items(Address::MAINNET, &truncated_sapling_data[..]),
Address::parse_internal(Address::MAINNET, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -266,7 +266,7 @@ mod tests {
0xe6, 0x70, 0x36, 0x5b, 0x7b, 0x9e,
];
assert_matches!(
Address::parse_items(Address::MAINNET, &truncated_after_sapling_typecode[..]),
Address::parse_internal(Address::MAINNET, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -278,7 +278,7 @@ mod tests {
let ua = Address(vec![Receiver::Sapling([1; 43]), Receiver::Sapling([2; 43])]);
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
assert_eq!(
Address::parse_items(Address::MAINNET, &encoded[..]).and_then(Address::try_from_items),
Address::parse_internal(Address::MAINNET, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
);
}
@ -291,7 +291,7 @@ mod tests {
let encoded = ua.to_jumbled_bytes(Address::MAINNET);
// ensure that decoding catches the error
assert_eq!(
Address::parse_items(Address::MAINNET, &encoded[..]).and_then(Address::try_from_items),
Address::parse_internal(Address::MAINNET, &encoded[..]),
Err(ParseError::BothP2phkAndP2sh)
);
}
@ -310,7 +310,7 @@ mod tests {
// with only one of them we don't have sufficient data for F4Jumble (so we hit a
// different error).
assert_matches!(
Address::parse_items(Address::MAINNET, &encoded[..]),
Address::parse_internal(Address::MAINNET, &encoded[..]),
Err(ParseError::InvalidEncoding(_))
);
}

View File

@ -232,7 +232,7 @@ mod tests {
0xdf, 0x63, 0xe7, 0xef, 0x65, 0x6b, 0x18, 0x23, 0xf7, 0x3e, 0x35, 0x7c, 0xf3, 0xc4,
];
assert_eq!(
Ufvk::parse_items(Ufvk::MAINNET, &invalid_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -250,7 +250,7 @@ mod tests {
0x43, 0x8e, 0xc0, 0x3e, 0x9f, 0xf4, 0xf1, 0x80, 0x32, 0xcf, 0x2f, 0x7e, 0x7f, 0x91,
];
assert_eq!(
Ufvk::parse_items(Ufvk::MAINNET, &truncated_padding[..]),
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -282,7 +282,7 @@ mod tests {
0x8c, 0x7a, 0xbf, 0x7b, 0x9a, 0xdd, 0xee, 0x18, 0x2c, 0x2d, 0xc2, 0xfc,
];
assert_matches!(
Ufvk::parse_items(Ufvk::MAINNET, &truncated_sapling_data[..]),
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -297,7 +297,7 @@ mod tests {
0x54, 0xd1, 0x9e, 0xec, 0x8b, 0xef, 0x35, 0xb8, 0x44, 0xdd, 0xab, 0x9a, 0x8d,
];
assert_matches!(
Ufvk::parse_items(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
Ufvk::parse_internal(Ufvk::MAINNET, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -309,7 +309,7 @@ mod tests {
let ufvk = Ufvk(vec![Fvk::Sapling([1; 128]), Fvk::Sapling([2; 128])]);
let encoded = ufvk.to_jumbled_bytes(Ufvk::MAINNET);
assert_eq!(
Ufvk::parse_items(Ufvk::MAINNET, &encoded[..]).and_then(Ufvk::try_from_items),
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Err(ParseError::DuplicateTypecode(Typecode::Sapling))
);
}
@ -327,7 +327,7 @@ mod tests {
];
assert_eq!(
Ufvk::parse_items(Ufvk::MAINNET, &encoded[..]).and_then(Ufvk::try_from_items),
Ufvk::parse_internal(Ufvk::MAINNET, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}

View File

@ -223,7 +223,7 @@ mod tests {
0x83, 0xe8, 0x92, 0x18, 0x28, 0x70, 0x1e, 0x81, 0x76, 0x56, 0xb6, 0x15,
];
assert_eq!(
Uivk::parse_items(Uivk::MAINNET, &invalid_padding[..]),
Uivk::parse_internal(Uivk::MAINNET, &invalid_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -239,7 +239,7 @@ mod tests {
0xf9, 0x65, 0x49, 0x14, 0xab, 0x7c, 0x55, 0x7b, 0x39, 0x47,
];
assert_eq!(
Uivk::parse_items(Uivk::MAINNET, &truncated_padding[..]),
Uivk::parse_internal(Uivk::MAINNET, &truncated_padding[..]),
Err(ParseError::InvalidEncoding(
"Invalid padding bytes".to_owned()
))
@ -267,7 +267,7 @@ mod tests {
0xf5, 0xd5, 0x8a, 0xb5, 0x1a,
];
assert_matches!(
Uivk::parse_items(Uivk::MAINNET, &truncated_sapling_data[..]),
Uivk::parse_internal(Uivk::MAINNET, &truncated_sapling_data[..]),
Err(ParseError::InvalidEncoding(_))
);
@ -280,7 +280,7 @@ mod tests {
0xd8, 0x21, 0x5e, 0x8, 0xa, 0x82, 0x95, 0x21, 0x74,
];
assert_matches!(
Uivk::parse_items(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
Uivk::parse_internal(Uivk::MAINNET, &truncated_after_sapling_typecode[..]),
Err(ParseError::InvalidEncoding(_))
);
}
@ -309,7 +309,7 @@ mod tests {
];
assert_eq!(
Uivk::parse_items(Uivk::MAINNET, &encoded[..]).and_then(Uivk::try_from_items),
Uivk::parse_internal(Uivk::MAINNET, &encoded[..]),
Err(ParseError::OnlyTransparent)
);
}