diff --git a/core/src/types/address.rs b/core/src/types/address.rs index 79a27a440fa..b4ef7bff76a 100644 --- a/core/src/types/address.rs +++ b/core/src/types/address.rs @@ -6,26 +6,20 @@ use std::fmt::{Debug, Display}; use std::hash::Hash; use std::str::FromStr; -use bech32::{self, FromBase32, ToBase32, Variant}; use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use thiserror::Error; -use crate::types::key; +use crate::impl_display_and_from_str_via_format; use crate::types::key::PublicKeyHash; +use crate::types::{key, string_encoding}; /// The length of an established [`Address`] encoded with Borsh. pub const ESTABLISHED_ADDRESS_BYTES_LEN: usize = 45; /// The length of [`Address`] encoded with Bech32m. -pub const ADDRESS_LEN: usize = 79 + ADDRESS_HRP.len(); +pub const ADDRESS_LEN: usize = 79 + string_encoding::hrp_len::
(); -/// human-readable part of Bech32m encoded address -// TODO use "a" for live network -const ADDRESS_HRP: &str = "atest"; -/// We're using "Bech32m" variant -pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; pub(crate) const HASH_LEN: usize = 40; /// An address string before bech32m encoding must be this size. @@ -71,6 +65,12 @@ mod internal { "ano::ETH Bridge Address "; } +/// Error from decoding address from string +pub type DecodeError = string_encoding::DecodeError; + +/// Result of decoding address from string +pub type Result = std::result::Result; + /// Fixed-length address strings prefix for established addresses. const PREFIX_ESTABLISHED: &str = "est"; /// Fixed-length address strings prefix for implicit addresses. @@ -80,24 +80,6 @@ const PREFIX_INTERNAL: &str = "ano"; /// Fixed-length address strings prefix for IBC addresses. const PREFIX_IBC: &str = "ibc"; -#[allow(missing_docs)] -#[derive(Error, Debug)] -pub enum DecodeError { - #[error("Error decoding address from Bech32m: {0}")] - DecodeBech32(bech32::Error), - #[error("Error decoding address from base32: {0}")] - DecodeBase32(bech32::Error), - #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] - UnexpectedBech32Prefix(String, String), - #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] - UnexpectedBech32Variant(bech32::Variant), - #[error("Invalid address encoding")] - InvalidInnerEncoding(std::io::Error), -} - -/// Result of a function that may fail -pub type Result = std::result::Result; - /// An account's address #[derive( Clone, @@ -122,34 +104,12 @@ pub enum Address { impl Address { /// Encode an address with Bech32m encoding pub fn encode(&self) -> String { - let bytes = self.to_fixed_len_string(); - bech32::encode(ADDRESS_HRP, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - ADDRESS_HRP - ) - }) + string_encoding::Format::encode(self) } /// Decode an address from Bech32m encoding pub fn decode(string: impl AsRef) -> Result { - let (prefix, hash_base32, variant) = bech32::decode(string.as_ref()) - .map_err(DecodeError::DecodeBech32)?; - if prefix != ADDRESS_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - ADDRESS_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&hash_base32) - .map_err(DecodeError::DecodeBase32)?; - Self::try_from_fixed_len_string(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) + string_encoding::Format::decode(string) } /// Try to get a raw hash of an address, only defined for established and @@ -163,7 +123,7 @@ impl Address { } /// Convert an address to a fixed length 7-bit ascii string bytes - fn to_fixed_len_string(&self) -> Vec { + pub fn to_fixed_len_string(&self) -> Vec { let mut string = match self { Address::Established(EstablishedAddress { hash }) => { format!("{}::{}", PREFIX_ESTABLISHED, hash) @@ -209,7 +169,7 @@ impl Address { } /// Try to parse an address from fixed-length utf-8 encoded address string. - fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result { + pub fn try_from_fixed_len_string(buf: &mut &[u8]) -> std::io::Result { use std::io::{Error, ErrorKind}; let string = std::str::from_utf8(buf) .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; @@ -302,6 +262,20 @@ impl Address { } } +impl string_encoding::Format for Address { + const HRP: &'static str = string_encoding::ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + Self::to_fixed_len_string(self) + } + + fn decode_bytes(bytes: &[u8]) -> std::result::Result { + Self::try_from_fixed_len_string(&mut &bytes[..]) + } +} + +impl_display_and_from_str_via_format!(Address); + impl serde::Serialize for Address { fn serialize( &self, @@ -326,26 +300,12 @@ impl<'de> serde::Deserialize<'de> for Address { } } -impl Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.encode()) - } -} - impl Debug for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.pretty_fmt(f) } } -impl FromStr for Address { - type Err = DecodeError; - - fn from_str(s: &str) -> Result { - Address::decode(s) - } -} - /// An established address is generated on-chain #[derive( Debug, diff --git a/core/src/types/masp.rs b/core/src/types/masp.rs index c0ccb67d1ee..7a0fd49205b 100644 --- a/core/src/types/masp.rs +++ b/core/src/types/masp.rs @@ -8,17 +8,14 @@ use bech32::{FromBase32, ToBase32}; use borsh::{BorshDeserialize, BorshSerialize}; use sha2::{Digest, Sha256}; -use crate::types::address::{ - masp, Address, DecodeError, BECH32M_VARIANT, HASH_LEN, +use crate::impl_display_and_from_str_via_format; +use crate::types::address::{masp, Address, DecodeError, HASH_LEN}; +use crate::types::string_encoding::{ + self, BECH32M_VARIANT, MASP_EXT_FULL_VIEWING_KEY_HRP, + MASP_EXT_SPENDING_KEY_HRP, MASP_PAYMENT_ADDRESS_HRP, + MASP_PINNED_PAYMENT_ADDRESS_HRP, }; -/// human-readable part of Bech32m encoded address -// TODO remove "test" suffix for live network -const EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; -const PAYMENT_ADDRESS_HRP: &str = "patest"; -const PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; -const EXT_SPENDING_KEY_HRP: &str = "xsktest"; - /// Wrapper for masp_primitive's FullViewingKey #[derive( Clone, @@ -34,51 +31,99 @@ const EXT_SPENDING_KEY_HRP: &str = "xsktest"; )] pub struct ExtendedViewingKey(masp_primitives::zip32::ExtendedFullViewingKey); -impl Display for ExtendedViewingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl ExtendedViewingKey { + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut bytes[..]) .expect("should be able to serialize an ExtendedFullViewingKey"); - let encoded = bech32::encode( - EXT_FULL_VIEWING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, + bytes.to_vec() + } + + /// Try to decode `Self` from bytes + pub fn decode_bytes(bytes: &[u8]) -> Result { + masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) + .map(Self) + } +} + +impl string_encoding::Format for ExtendedViewingKey { + const HRP: &'static str = MASP_EXT_FULL_VIEWING_KEY_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes(bytes: &[u8]) -> Result { + Self::decode_bytes(bytes) + } +} + +impl_display_and_from_str_via_format!(ExtendedViewingKey); + +impl string_encoding::Format for PaymentAddress { + const HRP: &'static str = MASP_PAYMENT_ADDRESS_HRP; + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn decode_bytes(_bytes: &[u8]) -> Result { + unimplemented!( + "Cannot determine if the PaymentAddress is pinned from bytes. Use \ + `PaymentAddress::decode_bytes(bytes, is_pinned)` instead." ) - .unwrap_or_else(|_| { + } + + // We override `encode` because we need to determine whether the address + // is pinned from its HRP + fn encode(&self) -> String { + let hrp = if self.is_pinned() { + MASP_PINNED_PAYMENT_ADDRESS_HRP + } else { + MASP_PAYMENT_ADDRESS_HRP + }; + let base32 = self.to_bytes().to_base32(); + bech32::encode(hrp, base32, BECH32M_VARIANT).unwrap_or_else(|_| { panic!( "The human-readable part {} should never cause a failure", - EXT_FULL_VIEWING_KEY_HRP + hrp ) - }); - write!(f, "{encoded}") + }) } -} - -impl FromStr for ExtendedViewingKey { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_FULL_VIEWING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( + // We override `decode` because we need to use different HRP for pinned and + // non-pinned address + fn decode( + string: impl AsRef, + ) -> Result { + let (prefix, base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + let is_pinned = if prefix == MASP_PAYMENT_ADDRESS_HRP { + false + } else if prefix == MASP_PINNED_PAYMENT_ADDRESS_HRP { + true + } else { + return Err(DecodeError::UnexpectedBech32Hrp( prefix, - EXT_FULL_VIEWING_KEY_HRP.into(), + MASP_PAYMENT_ADDRESS_HRP.into(), )); - } + }; match variant { BECH32M_VARIANT => {} _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), } let bytes: Vec = FromBase32::from_base32(&base32) .map_err(DecodeError::DecodeBase32)?; - masp_primitives::zip32::ExtendedFullViewingKey::read(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) - .map(Self) + + PaymentAddress::decode_bytes(&bytes, is_pinned) + .map_err(DecodeError::InvalidBytes) } } +impl_display_and_from_str_via_format!(PaymentAddress); + impl From for masp_primitives::zip32::ExtendedFullViewingKey { @@ -155,78 +200,45 @@ impl PaymentAddress { // hex of the first 40 chars of the hash format!("{:.width$X}", hasher.finalize(), width = HASH_LEN) } -} -impl From for masp_primitives::primitives::PaymentAddress { - fn from(addr: PaymentAddress) -> Self { - addr.0 - } -} - -impl From for PaymentAddress { - fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self { - Self(addr, false) - } -} - -impl Display for PaymentAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let bytes = self.0.to_bytes(); - let hrp = if self.1 { - PINNED_PAYMENT_ADDRESS_HRP - } else { - PAYMENT_ADDRESS_HRP - }; - let encoded = bech32::encode(hrp, bytes.to_base32(), BECH32M_VARIANT) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - PAYMENT_ADDRESS_HRP - ) - }); - write!(f, "{encoded}") + /// Encode `Self` to bytes + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() } -} - -impl FromStr for PaymentAddress { - type Err = DecodeError; - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - let pinned = if prefix == PAYMENT_ADDRESS_HRP { - false - } else if prefix == PINNED_PAYMENT_ADDRESS_HRP { - true - } else { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - PAYMENT_ADDRESS_HRP.into(), - )); - }; - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } + /// Try to decode `Self` from bytes + pub fn decode_bytes( + bytes: &[u8], + is_pinned: bool, + ) -> Result { let addr_len_err = |_| { - DecodeError::InvalidInnerEncoding(Error::new( + Error::new( ErrorKind::InvalidData, "expected 43 bytes for the payment address", - )) + ) }; let addr_data_err = || { - DecodeError::InvalidInnerEncoding(Error::new( + Error::new( ErrorKind::InvalidData, "invalid payment address provided", - )) + ) }; - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; - masp_primitives::primitives::PaymentAddress::from_bytes( - &bytes.try_into().map_err(addr_len_err)?, - ) - .ok_or_else(addr_data_err) - .map(|x| Self(x, pinned)) + let bytes: &[u8; 43] = &bytes.try_into().map_err(addr_len_err)?; + masp_primitives::primitives::PaymentAddress::from_bytes(bytes) + .ok_or_else(addr_data_err) + .map(|addr| Self(addr, is_pinned)) + } +} + +impl From for masp_primitives::primitives::PaymentAddress { + fn from(addr: PaymentAddress) -> Self { + addr.0 + } +} + +impl From for PaymentAddress { + fn from(addr: masp_primitives::primitives::PaymentAddress) -> Self { + Self(addr, false) } } @@ -258,51 +270,25 @@ impl<'de> serde::Deserialize<'de> for PaymentAddress { #[derive(Clone, Debug, Copy, BorshSerialize, BorshDeserialize)] pub struct ExtendedSpendingKey(masp_primitives::zip32::ExtendedSpendingKey); -impl Display for ExtendedSpendingKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl string_encoding::Format for ExtendedSpendingKey { + const HRP: &'static str = MASP_EXT_SPENDING_KEY_HRP; + + fn to_bytes(&self) -> Vec { let mut bytes = [0; 169]; self.0 .write(&mut &mut bytes[..]) .expect("should be able to serialize an ExtendedSpendingKey"); - let encoded = bech32::encode( - EXT_SPENDING_KEY_HRP, - bytes.to_base32(), - BECH32M_VARIANT, - ) - .unwrap_or_else(|_| { - panic!( - "The human-readable part {} should never cause a failure", - EXT_SPENDING_KEY_HRP - ) - }); - write!(f, "{encoded}") + bytes.to_vec() } -} -impl FromStr for ExtendedSpendingKey { - type Err = DecodeError; - - fn from_str(string: &str) -> Result { - let (prefix, base32, variant) = - bech32::decode(string).map_err(DecodeError::DecodeBech32)?; - if prefix != EXT_SPENDING_KEY_HRP { - return Err(DecodeError::UnexpectedBech32Prefix( - prefix, - EXT_SPENDING_KEY_HRP.into(), - )); - } - match variant { - BECH32M_VARIANT => {} - _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), - } - let bytes: Vec = FromBase32::from_base32(&base32) - .map_err(DecodeError::DecodeBase32)?; + fn decode_bytes(bytes: &[u8]) -> Result { masp_primitives::zip32::ExtendedSpendingKey::read(&mut &bytes[..]) - .map_err(DecodeError::InvalidInnerEncoding) .map(Self) } } +impl_display_and_from_str_via_format!(ExtendedSpendingKey); + impl From for masp_primitives::zip32::ExtendedSpendingKey { fn from(key: ExtendedSpendingKey) -> Self { key.0 diff --git a/core/src/types/mod.rs b/core/src/types/mod.rs index 05500604985..2c8af14d324 100644 --- a/core/src/types/mod.rs +++ b/core/src/types/mod.rs @@ -9,6 +9,7 @@ pub mod internal; pub mod key; pub mod masp; pub mod storage; +pub mod string_encoding; pub mod time; pub mod token; pub mod transaction; diff --git a/core/src/types/string_encoding.rs b/core/src/types/string_encoding.rs new file mode 100644 index 00000000000..afa6594e2a3 --- /dev/null +++ b/core/src/types/string_encoding.rs @@ -0,0 +1,121 @@ +//! Namada's standard string encoding for public types. +//! +//! We're using [bech32m](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki), +//! a format with a human-readable, followed by base32 encoding with a limited +//! character set with checksum check. +//! +//! To use this encoding for a new type, add a HRP (human-readable part) const +//! below and use it to `impl string_encoding::Format for YourType`. + +use bech32::{self, FromBase32, ToBase32, Variant}; +use thiserror::Error; + +/// We're using "Bech32m" variant +pub const BECH32M_VARIANT: bech32::Variant = Variant::Bech32m; + +// Human-readable parts of Bech32m encoding +// +// Invariant: HRPs must be unique !!! +// +// TODO: remove "test" suffix for live network +/// `Address` human-readable part +pub const ADDRESS_HRP: &str = "atest"; +/// MASP extended viewing key human-readable part +pub const MASP_EXT_FULL_VIEWING_KEY_HRP: &str = "xfvktest"; +/// MASP payment address (not pinned) human-readable part +pub const MASP_PAYMENT_ADDRESS_HRP: &str = "patest"; +/// MASP pinned payment address human-readable part +pub const MASP_PINNED_PAYMENT_ADDRESS_HRP: &str = "ppatest"; +/// MASP extended spending key human-readable part +pub const MASP_EXT_SPENDING_KEY_HRP: &str = "xsktest"; + +#[allow(missing_docs)] +#[derive(Error, Debug)] +pub enum DecodeError { + #[error("Error decoding from Bech32m: {0}")] + DecodeBech32(bech32::Error), + #[error("Error decoding from base32: {0}")] + DecodeBase32(bech32::Error), + #[error("Unexpected Bech32m human-readable part {0}, expected {1}")] + UnexpectedBech32Hrp(String, String), + #[error("Unexpected Bech32m variant {0:?}, expected {BECH32M_VARIANT:?}")] + UnexpectedBech32Variant(bech32::Variant), + #[error("Invalid bytes: {0}")] + InvalidBytes(std::io::Error), +} + +/// Format to string with bech32m +pub trait Format: Sized { + /// Human-readable part + const HRP: &'static str; + + /// Encode `Self` to a string + fn encode(&self) -> String { + let base32 = self.to_bytes().to_base32(); + bech32::encode(Self::HRP, base32, BECH32M_VARIANT).unwrap_or_else( + |_| { + panic!( + "The human-readable part {} should never cause a failure", + Self::HRP + ) + }, + ) + } + + /// Try to decode `Self` from a string + fn decode(string: impl AsRef) -> Result { + let (hrp, hash_base32, variant) = bech32::decode(string.as_ref()) + .map_err(DecodeError::DecodeBech32)?; + if hrp != Self::HRP { + return Err(DecodeError::UnexpectedBech32Hrp( + hrp, + Self::HRP.into(), + )); + } + match variant { + BECH32M_VARIANT => {} + _ => return Err(DecodeError::UnexpectedBech32Variant(variant)), + } + let bytes: Vec = FromBase32::from_base32(&hash_base32) + .map_err(DecodeError::DecodeBase32)?; + + Self::decode_bytes(&bytes).map_err(DecodeError::InvalidBytes) + } + + /// Encode `Self` to bytes + fn to_bytes(&self) -> Vec; + + /// Try to decode `Self` from bytes + fn decode_bytes(bytes: &[u8]) -> Result; +} + +/// Implement [`std::fmt::Display`] and [`std::str::FromStr`] via +/// [`Format`]. +#[macro_export] +macro_rules! impl_display_and_from_str_via_format { + ($t:path) => { + impl std::fmt::Display for $t { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + $crate::types::string_encoding::Format::encode(self) + ) + } + } + + impl std::str::FromStr for $t { + type Err = $crate::types::string_encoding::DecodeError; + + fn from_str(s: &str) -> std::result::Result { + $crate::types::string_encoding::Format::decode(s) + } + } + }; +} + +/// Get the length of the human-readable part +// Not in the `Format` trait, cause functions in traits cannot be const +pub const fn hrp_len() -> usize { + T::HRP.len() +}