From e7b76889bdb41e025157ae328f0cf83099610308 Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Sat, 23 Jul 2022 08:18:53 -0300 Subject: [PATCH] fix(addresses): Fix Byron cbor structure (#155) * Use correct CBOR struct for Byron addresses * Implement universal to / from string that works across all eras --- pallas-addresses/src/byron.rs | 79 ++++++++++++++++++++++- pallas-addresses/src/lib.rs | 116 +++++++++++++++++++++++----------- pallas-codec/src/utils.rs | 2 +- pallas-traverse/src/output.rs | 18 +++--- 4 files changed, 167 insertions(+), 48 deletions(-) diff --git a/pallas-addresses/src/byron.rs b/pallas-addresses/src/byron.rs index 780b6fdc..9bd4c215 100644 --- a/pallas-addresses/src/byron.rs +++ b/pallas-addresses/src/byron.rs @@ -1,10 +1,12 @@ use pallas_codec::{ minicbor::{self, bytes::ByteVec, Decode, Encode}, - utils::OrderPreservingProperties, + utils::{OrderPreservingProperties, TagWrap}, }; use pallas_crypto::hash::Hash; +use crate::Error; + pub type Blake2b224 = Hash<28>; pub type AddressId = Blake2b224; @@ -157,3 +159,78 @@ pub struct AddressPayload { #[n(2)] pub addrtype: AddrType, } + +/// New type wrapping a Byron address primitive +#[derive(Debug, Encode, Decode, Clone, PartialEq, PartialOrd)] +pub struct ByronAddress { + #[n(0)] + payload: TagWrap, + + #[n(1)] + crc: u64, +} + +impl ByronAddress { + pub fn new(payload: &[u8], crc: u64) -> Self { + Self { + payload: TagWrap(ByteVec::from(Vec::from(payload))), + crc, + } + } + + pub fn from_bytes(value: &[u8]) -> Result { + pallas_codec::minicbor::decode(value).map_err(|_| Error::InvalidByronCbor) + } + + // Tries to decode an address from its hex representation + pub fn from_base58(value: &str) -> Result { + let bytes = base58::FromBase58::from_base58(value).map_err(Error::BadBase58)?; + Self::from_bytes(&bytes) + } + + /// Gets a numeric id describing the type of the address + pub fn typeid(&self) -> u8 { + 0b1000 + } + + pub fn to_vec(&self) -> Vec { + pallas_codec::minicbor::to_vec(&self).unwrap() + } + + pub fn to_base58(&self) -> String { + let bytes = self.to_vec(); + base58::ToBase58::to_base58(bytes.as_slice()) + } + + pub fn to_hex(&self) -> String { + let bytes = self.to_vec(); + hex::encode(bytes) + } + + pub fn decode(&self) -> Result { + minicbor::decode(&self.payload.0).map_err(|_| Error::InvalidByronCbor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_VECTOR: &str = "37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na"; + + const ROOT_HASH: &str = "7e9ee4a9527dea9091e2d580edd6716888c42f75d96276290f98fe0b"; + + #[test] + fn roundtrip_base58() { + let addr = ByronAddress::from_base58(TEST_VECTOR).unwrap(); + let ours = addr.to_base58(); + assert_eq!(TEST_VECTOR, ours); + } + + #[test] + fn payload_matches() { + let addr = ByronAddress::from_base58(TEST_VECTOR).unwrap(); + let payload = addr.decode().unwrap(); + assert_eq!(payload.root.to_string(), ROOT_HASH); + } +} diff --git a/pallas-addresses/src/lib.rs b/pallas-addresses/src/lib.rs index 232a850b..82ad2881 100644 --- a/pallas-addresses/src/lib.rs +++ b/pallas-addresses/src/lib.rs @@ -10,7 +10,7 @@ pub mod byron; pub mod varuint; -use std::io::Cursor; +use std::{io::Cursor, str::FromStr}; use pallas_crypto::hash::Hash; use thiserror::Error; @@ -20,6 +20,15 @@ pub enum Error { #[error("error converting from/to bech32 {0}")] BadBech32(bech32::Error), + #[error("error decoding base58 value")] + BadBase58(base58::FromBase58Error), + + #[error("error decoding hex value")] + BadHex, + + #[error("unknown or bad string format for address {0}")] + UnknownStringFormat(String), + #[error("address header not found")] MissingHeader, @@ -235,15 +244,7 @@ pub enum StakePayload { #[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct StakeAddress(Network, StakePayload); -/// New type wrapping a Byron address primitive -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct ByronAddress(byron::AddressPayload); - -impl ByronAddress { - pub fn new(primitive: byron::AddressPayload) -> Self { - Self(primitive) - } -} +pub use byron::ByronAddress; /// A decoded Cardano address of any type #[derive(Debug, Clone, PartialEq, PartialOrd)] @@ -335,8 +336,8 @@ parse_shelley_fn!(parse_type_7, script_hash); // type 8 (1000) are Byron addresses fn parse_type_8(header: u8, payload: &[u8]) -> Result { let vec = [&[header], payload].concat(); - let prim = pallas_codec::minicbor::decode(&vec).map_err(|_| Error::InvalidByronCbor)?; - Ok(Address::Byron(ByronAddress(prim))) + let inner = pallas_codec::minicbor::decode(&vec).map_err(|_| Error::InvalidByronCbor)?; + Ok(Address::Byron(inner)) } // types 14-15 are Stake addresses @@ -382,22 +383,6 @@ impl Network { } } -impl ByronAddress { - /// Gets a numeric id describing the type of the address - pub fn typeid(&self) -> u8 { - 0b1000 - } - - fn to_vec(&self) -> Vec { - pallas_codec::minicbor::to_vec(&self.0).unwrap() - } - - pub fn to_hex(&self) -> String { - let bytes = self.to_vec(); - hex::encode(bytes) - } -} - impl ShelleyAddress { pub fn new( network: Network, @@ -554,7 +539,7 @@ impl Address { } } - /// Tries to parse a bech32 address into an Address + /// Tries to parse a bech32 value into an Address pub fn from_bech32(bech32: &str) -> Result { bech32_to_address(bech32) } @@ -564,6 +549,12 @@ impl Address { bytes_to_address(bytes) } + // Tries to parse a hex value into an Address + pub fn from_hex(bytes: &str) -> Result { + let bytes = hex::decode(bytes).map_err(|_| Error::BadHex)?; + bytes_to_address(&bytes) + } + /// Gets the network assoaciated with this address pub fn network(&self) -> Option { match self { @@ -625,6 +616,36 @@ impl Address { } } +impl ToString for Address { + fn to_string(&self) -> String { + match self { + Address::Byron(x) => x.to_base58(), + Address::Shelley(x) => x.to_bech32().unwrap_or_else(|_| x.to_hex()), + Address::Stake(x) => x.to_bech32().unwrap_or_else(|_| x.to_hex()), + } + } +} + +impl FromStr for Address { + type Err = Error; + + fn from_str(s: &str) -> Result { + if let Ok(x) = Address::from_bech32(s) { + return Ok(x); + } + + if let Ok(x) = ByronAddress::from_base58(s) { + return Ok(x.into()); + } + + if let Ok(x) = Address::from_hex(s) { + return Ok(x); + } + + Err(Error::UnknownStringFormat(s.to_owned())) + } +} + impl TryFrom<&[u8]> for Address { type Error = Error; @@ -668,6 +689,7 @@ mod tests { ("addr1w8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gtcyjy7wx", 07u8), ("stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw", 14u8), ("stake178phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gtcccycj5", 15u8), + ("37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na", 08u8), ]; const PAYMENT_PUBLIC_KEY: &str = @@ -685,8 +707,23 @@ mod tests { fn roundtrip_bech32() { for vector in MAINNET_TEST_VECTORS { let original = vector.0; - let addr = Address::from_bech32(original).unwrap(); - let ours = addr.to_bech32().unwrap(); + match Address::from_str(original) { + Ok(Address::Byron(_)) => (), + Ok(addr) => { + let ours = addr.to_bech32().unwrap(); + assert_eq!(original, ours); + } + _ => panic!("should be able to decode from bech32"), + } + } + } + + #[test] + fn roundtrip_string() { + for vector in MAINNET_TEST_VECTORS { + let original = vector.0; + let addr = Address::from_str(original).unwrap(); + let ours = addr.to_string(); assert_eq!(original, ours); } } @@ -695,7 +732,7 @@ mod tests { fn typeid_matches() { for vector in MAINNET_TEST_VECTORS { let original = vector.0; - let addr = Address::from_bech32(original).unwrap(); + let addr = Address::from_str(original).unwrap(); assert_eq!(addr.typeid(), vector.1); } } @@ -704,8 +741,12 @@ mod tests { fn network_matches() { for vector in MAINNET_TEST_VECTORS { let original = vector.0; - let addr = Address::from_bech32(original).unwrap(); - assert!(matches!(addr.network(), Some(Network::Mainnet))); + let addr = Address::from_str(original).unwrap(); + + match addr { + Address::Byron(_) => assert!(matches!(addr.network(), None)), + _ => assert!(matches!(addr.network(), Some(Network::Mainnet))), + } } } @@ -713,7 +754,7 @@ mod tests { fn payload_matches() { for vector in MAINNET_TEST_VECTORS { let original = vector.0; - let addr = Address::from_bech32(original).unwrap(); + let addr = Address::from_str(original).unwrap(); match addr { Address::Shelley(x) => { @@ -758,7 +799,10 @@ mod tests { assert_eq!(hash, expected); } }, - Address::Byron(_) => (), + Address::Byron(_) => { + // byron has it's own payload tests + () + } }; } } diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index 41face87..864065d8 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -274,7 +274,7 @@ impl Deref for CborWrap { } #[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct TagWrap(I); +pub struct TagWrap(pub I); impl TagWrap { pub fn new(inner: I) -> Self { diff --git a/pallas-traverse/src/output.rs b/pallas-traverse/src/output.rs index 6261f654..6a4e8f9e 100644 --- a/pallas-traverse/src/output.rs +++ b/pallas-traverse/src/output.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, ops::Deref}; -use pallas_addresses::{Address, Error as AddressError}; +use pallas_addresses::{Address, ByronAddress, Error as AddressError}; use pallas_codec::minicbor; use pallas_primitives::{alonzo, babbage, byron}; @@ -19,21 +19,19 @@ impl<'b> MultiEraOutput<'b> { Self::Babbage(Box::new(Cow::Borrowed(output))) } - pub fn address_raw(&self) -> &[u8] { + pub fn address(&self) -> Result { match self { - MultiEraOutput::AlonzoCompatible(x) => &x.address, + MultiEraOutput::AlonzoCompatible(x) => Address::from_bytes(&x.address), MultiEraOutput::Babbage(x) => match x.deref().deref() { - babbage::TransactionOutput::Legacy(x) => &x.address, - babbage::TransactionOutput::PostAlonzo(x) => &x.address, + babbage::TransactionOutput::Legacy(x) => Address::from_bytes(&x.address), + babbage::TransactionOutput::PostAlonzo(x) => Address::from_bytes(&x.address), }, - MultiEraOutput::Byron(x) => x.address.payload.deref(), + MultiEraOutput::Byron(x) => { + Ok(ByronAddress::new(&x.address.payload.0, x.address.crc).into()) + } } } - pub fn address(&self) -> Result { - Address::from_bytes(self.address_raw()) - } - pub fn ada_amount(&self) -> u64 { match self { MultiEraOutput::Byron(x) => x.amount,