diff --git a/Cargo.lock b/Cargo.lock index ffcd83f1e0..4e443b7b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -701,12 +701,12 @@ name = "astria-core" version = "0.1.0" dependencies = [ "astria-core", + "astria-core-address", "astria-core-consts", "astria-core-crypto", "astria-merkle", "base64 0.21.7", "base64-serde", - "bech32 0.11.0", "brotli", "bytes", "celestia-tendermint", @@ -731,6 +731,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "astria-core-address" +version = "0.1.0" +dependencies = [ + "astria-core-consts", + "bech32 0.11.0", + "thiserror", +] + [[package]] name = "astria-core-consts" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ae4a4cda5c..bded5e96a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/astria-conductor", "crates/astria-config", "crates/astria-core", + "crates/astria-core-address", "crates/astria-core-consts", "crates/astria-core-crypto", "crates/astria-eyre", @@ -36,6 +37,7 @@ default-members = [ "crates/astria-conductor", "crates/astria-config", "crates/astria-core", + "crates/astria-core-address", "crates/astria-core-consts", "crates/astria-core-crypto", "crates/astria-grpc-mock", diff --git a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs index 0bd72886a5..13f6b8c22e 100644 --- a/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/bridge_withdrawer/submitter/mod.rs @@ -15,6 +15,7 @@ use astria_core::{ Action, TransactionBody, }, + Protobuf as _, }; use astria_eyre::eyre::{ self, diff --git a/crates/astria-core-address/CHANGELOG.md b/crates/astria-core-address/CHANGELOG.md new file mode 100644 index 0000000000..e8d6d38ac2 --- /dev/null +++ b/crates/astria-core-address/CHANGELOG.md @@ -0,0 +1,14 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Initial release. [#1802](https://github.com/astriaorg/astria/pull/1802) diff --git a/crates/astria-core-address/Cargo.toml b/crates/astria-core-address/Cargo.toml new file mode 100644 index 0000000000..85a2a03eaf --- /dev/null +++ b/crates/astria-core-address/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "astria-core-address" +version = "0.1.0" +edition = "2021" + +[dependencies] +astria-core-consts = { path = "../astria-core-consts" } + +thiserror = { workspace = true } + +bech32 = "0.11.0" + +[features] +unchecked-constructor = [] diff --git a/crates/astria-core-address/src/lib.rs b/crates/astria-core-address/src/lib.rs new file mode 100644 index 0000000000..3685222d87 --- /dev/null +++ b/crates/astria-core-address/src/lib.rs @@ -0,0 +1,440 @@ +use std::{ + marker::PhantomData, + str::FromStr, +}; + +pub use astria_core_consts::ADDRESS_LENGTH; + +#[derive(Debug, Hash)] +pub struct Address { + bytes: [u8; ADDRESS_LENGTH], + prefix: bech32::Hrp, + format: PhantomData, +} + +impl Clone for Address { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Address {} + +impl PartialEq for Address { + fn eq(&self, other: &Self) -> bool { + self.bytes.eq(&other.bytes) && self.prefix.eq(&other.prefix) + } +} + +impl Eq for Address {} + +impl Address { + #[must_use = "the builder must be used to construct an address to be useful"] + pub fn builder() -> Builder { + Builder::new() + } + + #[must_use] + pub fn bytes(self) -> [u8; ADDRESS_LENGTH] { + self.bytes + } + + #[must_use] + pub fn as_bytes(&self) -> &[u8; ADDRESS_LENGTH] { + &self.bytes + } + + #[must_use] + pub fn prefix(&self) -> &str { + self.prefix.as_str() + } + + /// Converts to a new address with the given `prefix`. + /// + /// # Errors + /// Returns an error if an address with `prefix` cannot be constructed. + /// The error conditions for this are the same as for [`AddressBuilder::try_build`]. + pub fn to_prefix(&self, prefix: &str) -> Result { + Self::builder() + .array(*self.as_bytes()) + .prefix(prefix) + .try_build() + } + + /// Converts to a new address with the type argument `OtherFormat`. + /// + /// `OtherFormat` is usually [`Bech32`] or [`Bech32m`]. + #[must_use] + pub fn to_format(&self) -> Address { + Address { + bytes: self.bytes, + prefix: self.prefix, + format: PhantomData, + } + } +} + +impl Address { + /// Should only be used where the inputs have been provided by a trusted entity, e.g. read + /// from our own state store. + /// + /// Note that this function is not considered part of the public API and is subject to breaking + /// change at any time. + #[cfg(feature = "unchecked-constructor")] + #[doc(hidden)] + #[must_use] + pub fn unchecked_from_parts(bytes: [u8; ADDRESS_LENGTH], prefix: &str) -> Self { + Self { + bytes, + prefix: bech32::Hrp::parse_unchecked(prefix), + format: PhantomData, + } + } +} + +impl FromStr for Address { + type Err = Error; + + fn from_str(s: &str) -> Result { + let checked = bech32::primitives::decode::CheckedHrpstring::new::(s) + .map_err(Self::Err::decode)?; + let hrp = checked.hrp(); + Self::builder() + .with_iter(checked.byte_iter()) + .prefix(hrp.as_str()) + .try_build() + } +} + +impl std::fmt::Display for Address { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use bech32::EncodeError; + match bech32::encode_lower_to_fmt::(f, self.prefix, self.as_bytes()) { + Ok(()) => Ok(()), + Err(EncodeError::Fmt(err)) => Err(err), + Err(err) => panic!( + "only formatting errors are valid when encoding astria addresses; all other error \ + variants (only TooLong as of bech32-0.11.0) are guaranteed to not happen because \ + `Address` is length checked:\n{err:?}", + ), + } + } +} + +pub struct NoBytes; +pub struct NoPrefix; +pub struct WithBytes<'a, I>(WithBytesInner<'a, I>); +enum WithBytesInner<'a, I> { + Array([u8; ADDRESS_LENGTH]), + Iter(I), + Slice(std::borrow::Cow<'a, [u8]>), +} +pub struct WithPrefix<'a>(std::borrow::Cow<'a, str>); + +pub struct NoBytesIter; + +impl Iterator for NoBytesIter { + type Item = u8; + + fn next(&mut self) -> Option { + None + } +} + +impl ExactSizeIterator for NoBytesIter { + fn len(&self) -> usize { + 0 + } +} + +pub struct Builder { + bytes: TBytes, + prefix: TPrefix, + format: PhantomData, +} + +impl Builder { + const fn new() -> Self { + Self { + bytes: NoBytes, + prefix: NoPrefix, + format: PhantomData, + } + } +} + +impl Builder { + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn array( + self, + array: [u8; ADDRESS_LENGTH], + ) -> Builder, TPrefix> { + Builder { + bytes: WithBytes(WithBytesInner::Array(array)), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn slice<'a, T: Into>>( + self, + bytes: T, + ) -> Builder, TPrefix> { + Builder { + bytes: WithBytes(WithBytesInner::Slice(bytes.into())), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + fn with_iter(self, iter: T) -> Builder, TPrefix> + where + T: IntoIterator, + T::IntoIter: ExactSizeIterator, + { + Builder { + bytes: WithBytes(WithBytesInner::Iter(iter)), + prefix: self.prefix, + format: self.format, + } + } + + #[must_use = "the builder must be built to construct an address to be useful"] + pub fn prefix<'a, T: Into>>( + self, + prefix: T, + ) -> Builder> { + Builder { + bytes: self.bytes, + prefix: WithPrefix(prefix.into()), + format: self.format, + } + } +} + +impl<'a, 'b, TFormat, TBytesIter> Builder, WithPrefix<'b>> +where + TBytesIter: IntoIterator, + TBytesIter::IntoIter: ExactSizeIterator, +{ + /// Attempts to build an address from the configured prefix and bytes. + /// + /// # Errors + /// Returns an error if one of the following conditions are violated: + /// + if the prefix shorter than 1 or longer than 83 characters, or contains characters outside + /// 33-126 of ASCII characters. + /// + if the provided bytes are not exactly 20 bytes. + pub fn try_build(self) -> Result, Error> { + let Self { + bytes: WithBytes(bytes), + prefix: WithPrefix(prefix), + format, + } = self; + let bytes = match bytes { + WithBytesInner::Array(bytes) => bytes, + WithBytesInner::Iter(bytes) => try_collect_to_array(bytes)?, + WithBytesInner::Slice(bytes) => <[u8; ADDRESS_LENGTH]>::try_from(bytes.as_ref()) + .map_err(|_| Error::incorrect_length(bytes.len()))?, + }; + let prefix = bech32::Hrp::parse(&prefix).map_err(Error::invalid_prefix)?; + Ok(Address { + bytes, + prefix, + format, + }) + } +} + +fn try_collect_to_array(iter: I) -> Result<[u8; ADDRESS_LENGTH], Error> +where + I: IntoIterator, + I::IntoIter: ExactSizeIterator, +{ + let iter = iter.into_iter(); + + if iter.len() != ADDRESS_LENGTH { + return Err(Error::incorrect_length(iter.len())); + } + let mut arr = [0; ADDRESS_LENGTH]; + for (left, right) in arr.iter_mut().zip(iter) { + *left = right; + } + Ok(arr) +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(ErrorKind); + +impl Error { + fn decode(source: bech32::primitives::decode::CheckedHrpstringError) -> Self { + Self(ErrorKind::Decode { + source, + }) + } + + fn invalid_prefix(source: bech32::primitives::hrp::Error) -> Self { + Self(ErrorKind::InvalidPrefix { + source, + }) + } + + fn incorrect_length(received: usize) -> Self { + Self(ErrorKind::IncorrectLength { + received, + }) + } +} + +#[derive(Debug, thiserror::Error, PartialEq)] +enum ErrorKind { + #[error("failed decoding provided string")] + Decode { + source: bech32::primitives::decode::CheckedHrpstringError, + }, + #[error("expected an address of 20 bytes, got `{received}`")] + IncorrectLength { received: usize }, + #[error("the provided prefix was not a valid bech32 human readable prefix")] + InvalidPrefix { + source: bech32::primitives::hrp::Error, + }, +} + +#[derive(Clone, Copy, Debug)] +pub enum Bech32m {} +#[derive(Clone, Copy, Debug)] +pub enum Bech32 {} +#[derive(Clone, Copy, Debug)] +pub enum NoFormat {} + +#[expect( + private_bounds, + reason = "prevent downstream implementation of this trait" +)] +pub trait Format: Sealed { + type Checksum: bech32::Checksum; +} + +impl Format for Bech32m { + type Checksum = bech32::Bech32m; +} + +impl Format for Bech32 { + type Checksum = bech32::Bech32; +} + +impl Format for NoFormat { + type Checksum = bech32::NoChecksum; +} + +trait Sealed {} +impl Sealed for Bech32m {} +impl Sealed for Bech32 {} +impl Sealed for NoFormat {} + +#[cfg(test)] +mod tests { + use super::{ + Address, + Bech32, + Bech32m, + Error, + ErrorKind, + }; + + const ASTRIA_ADDRESS_PREFIX: &str = "astria"; + const ASTRIA_COMPAT_ADDRESS_PREFIX: &str = "astriacompat"; + + #[track_caller] + fn assert_wrong_address_bytes(bad_account: &[u8]) { + let error = Address::::builder() + .slice(bad_account) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .expect_err( + "converting from an incorrectly sized byte slice succeeded where it should have \ + failed", + ); + let Error(ErrorKind::IncorrectLength { + received, + }) = error + else { + panic!("expected ErrorKind::IncorrectLength, got {error:?}"); + }; + assert_eq!(bad_account.len(), received); + } + + #[test] + fn account_of_incorrect_length_gives_error() { + assert_wrong_address_bytes(&[42; 0]); + assert_wrong_address_bytes(&[42; 19]); + assert_wrong_address_bytes(&[42; 21]); + assert_wrong_address_bytes(&[42; 100]); + } + + #[test] + fn parse_bech32m_address() { + let expected = Address::builder() + .array([42; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let actual = expected.to_string().parse::
().unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn parse_bech32_address() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let actual = expected.to_string().parse::>().unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn parsing_bech32_address_as_bech32m_fails() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let err = expected + .to_string() + .parse::>() + .expect_err("this must not work"); + match err { + Error(ErrorKind::Decode { + .. + }) => {} + other => { + panic!("expected Error(ErrorKind::Decode {{ .. }}), but got {other:?}") + } + } + } + + #[test] + fn parsing_bech32m_address_as_bech32_fails() { + let expected = Address::::builder() + .array([42; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let err = expected + .to_string() + .parse::>() + .expect_err("this must not work"); + match err { + Error(ErrorKind::Decode { + .. + }) => {} + other => { + panic!("expected Error(ErrorKind::Decode {{ .. }}), but got {other:?}") + } + } + } +} diff --git a/crates/astria-core-consts/CHANGELOG.md b/crates/astria-core-consts/CHANGELOG.md new file mode 100644 index 0000000000..27692b72c0 --- /dev/null +++ b/crates/astria-core-consts/CHANGELOG.md @@ -0,0 +1,14 @@ + + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Unreleased + +### Added + +- Initial release. [#1800](https://github.com/astriaorg/astria/pull/1800) diff --git a/crates/astria-core/CHANGELOG.md b/crates/astria-core/CHANGELOG.md index 1d113ed38a..68610fc818 100644 --- a/crates/astria-core/CHANGELOG.md +++ b/crates/astria-core/CHANGELOG.md @@ -14,14 +14,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release. - Added method `TracePrefixed::leading_channel` to read the left-most channel of a trace prefixed ICS20 asset [#1768](https://github.com/astriaorg/astria/pull/1768) +- Added `impl Protobuf for Address` [#1802](https://github.com/astriaorg/astria/pull/1802) ### Changed - Moved `astria_core::crypto` to `astria-core-crypto` and reexported `astria_core_crypto as crypto` (this change is transparent) [#1800](https://github.com/astriaorg/astria/pull/1800/) +- Moved definitions of address domain type to `astria-core-address` and + reexported items using the same aliases [#1802](https://github.com/astriaorg/astria/pull/1802) ### Removed - Removed method `TracePrefixed::last_channel` [#1768](https://github.com/astriaorg/astria/pull/1768) - Removed method `SigningKey::try_address` [#1800](https://github.com/astriaorg/astria/pull/1800/) +- Removed inherent methods `Address::try_from_raw` and `Address::to_raw` + [#1802](https://github.com/astriaorg/astria/pull/1802) +- Removed `AddressBuilder::with_iter` from public interface [#1802](https://github.com/astriaorg/astria/pull/1802) diff --git a/crates/astria-core/Cargo.toml b/crates/astria-core/Cargo.toml index e8774c9a16..b816e3fdf5 100644 --- a/crates/astria-core/Cargo.toml +++ b/crates/astria-core/Cargo.toml @@ -16,11 +16,11 @@ keywords = ["astria", "grpc", "rpc", "blockchain", "execution", "protobuf"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bech32 = "0.11.0" brotli = { version = "5.0.0", optional = true } celestia-types = { version = "0.1.1", optional = true } pbjson = { version = "0.6.0", optional = true } +astria-core-address = { path = "../astria-core-address" } astria-core-consts = { path = "../astria-core-consts" } astria-core-crypto = { path = "../astria-core-crypto" } merkle = { package = "astria-merkle", path = "../astria-merkle" } @@ -56,7 +56,7 @@ brotli = ["dep:brotli"] # When enabled, this adds constructors for some types that skip the normal constructor validity # checks. It supports the case where the inputs are already deemed valid, e.g. having read them from # local storage. -unchecked-constructors = [] +unchecked-constructors = ["astria-core-address/unchecked-constructor"] [dev-dependencies] astria-core = { path = ".", features = ["serde"] } diff --git a/crates/astria-core/src/primitive/v1/mod.rs b/crates/astria-core/src/primitive/v1/mod.rs index bc88b6feed..1a60a5bfe5 100644 --- a/crates/astria-core/src/primitive/v1/mod.rs +++ b/crates/astria-core/src/primitive/v1/mod.rs @@ -1,12 +1,15 @@ pub mod asset; pub mod u128; -use std::{ - marker::PhantomData, - str::FromStr, +pub use astria_core_address::{ + Address, + Bech32, + Bech32m, + Builder as AddressBuilder, + Error as AddressError, + Format, + ADDRESS_LENGTH as ADDRESS_LEN, }; - -pub use astria_core_consts::ADDRESS_LENGTH as ADDRESS_LEN; use base64::{ display::Base64Display, prelude::BASE64_URL_SAFE, @@ -26,6 +29,24 @@ pub const ROLLUP_ID_LEN: usize = 32; pub const TRANSACTION_ID_LEN: usize = 32; +impl Protobuf for Address { + type Error = AddressError; + type Raw = raw::Address; + + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + let raw::Address { + bech32m, + } = raw; + bech32m.parse() + } + + fn to_raw(&self) -> Self::Raw { + raw::Address { + bech32m: self.to_string(), + } + } +} + impl Protobuf for merkle::Proof { type Error = merkle::audit::InvalidProof; type Raw = raw::Proof; @@ -253,431 +274,6 @@ pub struct IncorrectRollupIdLength { received: usize, } -#[derive(Debug, thiserror::Error)] -#[error(transparent)] -pub struct AddressError(AddressErrorKind); - -impl AddressError { - fn decode(source: bech32::primitives::decode::CheckedHrpstringError) -> Self { - Self(AddressErrorKind::Decode { - source, - }) - } - - fn invalid_prefix(source: bech32::primitives::hrp::Error) -> Self { - Self(AddressErrorKind::InvalidPrefix { - source, - }) - } - - fn incorrect_address_length(received: usize) -> Self { - Self(AddressErrorKind::IncorrectAddressLength { - received, - }) - } -} - -#[derive(Debug, thiserror::Error, PartialEq)] -enum AddressErrorKind { - #[error("failed decoding provided string")] - Decode { - source: bech32::primitives::decode::CheckedHrpstringError, - }, - #[error("expected an address of 20 bytes, got `{received}`")] - IncorrectAddressLength { received: usize }, - #[error("the provided prefix was not a valid bech32 human readable prefix")] - InvalidPrefix { - source: bech32::primitives::hrp::Error, - }, -} - -pub struct NoBytes; -pub struct NoPrefix; -pub struct WithBytes<'a, I>(WithBytesInner<'a, I>); -enum WithBytesInner<'a, I> { - Array([u8; ADDRESS_LEN]), - Iter(I), - Slice(std::borrow::Cow<'a, [u8]>), -} -pub struct WithPrefix<'a>(std::borrow::Cow<'a, str>); - -pub struct NoBytesIter; - -impl Iterator for NoBytesIter { - type Item = u8; - - fn next(&mut self) -> Option { - None - } -} - -pub struct AddressBuilder { - bytes: TBytes, - prefix: TPrefix, - format: PhantomData, -} - -impl AddressBuilder { - const fn new() -> Self { - Self { - bytes: NoBytes, - prefix: NoPrefix, - format: PhantomData, - } - } -} - -impl AddressBuilder { - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn array( - self, - array: [u8; ADDRESS_LEN], - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Array(array)), - prefix: self.prefix, - format: self.format, - } - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn slice<'a, T: Into>>( - self, - bytes: T, - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Slice(bytes.into())), - prefix: self.prefix, - format: self.format, - } - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn with_iter>( - self, - iter: T, - ) -> AddressBuilder, TPrefix> { - AddressBuilder { - bytes: WithBytes(WithBytesInner::Iter(iter)), - prefix: self.prefix, - format: self.format, - } - } - - /// Use the given verification key for address generation. - /// - /// The verification key is hashed with SHA256 and the first 20 bytes are used as the address - /// bytes. - #[expect(clippy::missing_panics_doc, reason = "the conversion is infallible")] - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn verification_key( - self, - key: &crate::crypto::VerificationKey, - ) -> AddressBuilder, TPrefix> { - let hash = Sha256::digest(key.as_bytes()); - let array: [u8; ADDRESS_LEN] = hash[0..ADDRESS_LEN] - .try_into() - .expect("hash is 32 bytes long, so must always be able to convert to 20 bytes"); - self.array(array) - } - - #[must_use = "the builder must be built to construct an address to be useful"] - pub fn prefix<'a, T: Into>>( - self, - prefix: T, - ) -> AddressBuilder> { - AddressBuilder { - bytes: self.bytes, - prefix: WithPrefix(prefix.into()), - format: self.format, - } - } -} - -impl<'a, 'b, TFormat, TBytesIter> AddressBuilder, WithPrefix<'b>> -where - TBytesIter: IntoIterator, -{ - /// Attempts to build an address from the configured prefix and bytes. - /// - /// # Errors - /// Returns an error if one of the following conditions are violated: - /// + if the prefix shorter than 1 or longer than 83 characters, or contains characters outside - /// 33-126 of ASCII characters. - /// + if the provided bytes are not exactly 20 bytes. - pub fn try_build(self) -> Result, AddressError> { - let Self { - bytes: WithBytes(bytes), - prefix: WithPrefix(prefix), - format, - } = self; - let bytes = match bytes { - WithBytesInner::Array(bytes) => bytes, - WithBytesInner::Iter(bytes) => try_collect_to_array(bytes)?, - WithBytesInner::Slice(bytes) => <[u8; ADDRESS_LEN]>::try_from(bytes.as_ref()) - .map_err(|_| AddressError::incorrect_address_length(bytes.len()))?, - }; - let prefix = bech32::Hrp::parse(&prefix).map_err(AddressError::invalid_prefix)?; - Ok(Address { - bytes, - prefix, - format, - }) - } -} - -fn try_collect_to_array>( - iter: I, -) -> Result<[u8; ADDRESS_LEN], AddressError> { - let mut arr = [0; ADDRESS_LEN]; - let mut iter = iter.into_iter(); - let mut i = 0; - loop { - if i >= ADDRESS_LEN { - break; - } - let Some(byte) = iter.next() else { - break; - }; - arr[i] = byte; - i = i.saturating_add(1); - } - let items_in_iterator = i.saturating_add(iter.count()); - if items_in_iterator != ADDRESS_LEN { - return Err(AddressError::incorrect_address_length(items_in_iterator)); - } - Ok(arr) -} - -#[derive(Clone, Copy, Debug)] -pub enum Bech32m {} -#[derive(Clone, Copy, Debug)] -pub enum Bech32 {} -#[derive(Clone, Copy, Debug)] -pub enum NoFormat {} - -pub trait Format: private::Sealed { - type Checksum: bech32::Checksum; -} - -impl Format for Bech32m { - type Checksum = bech32::Bech32m; -} - -impl Format for Bech32 { - type Checksum = bech32::Bech32; -} - -impl Format for NoFormat { - type Checksum = bech32::NoChecksum; -} - -mod private { - pub trait Sealed {} - impl Sealed for super::Bech32m {} - impl Sealed for super::Bech32 {} - impl Sealed for super::NoFormat {} -} - -#[derive(Debug, Hash)] -pub struct Address { - bytes: [u8; ADDRESS_LEN], - prefix: bech32::Hrp, - format: PhantomData, -} - -// The serde impls need to be manually implemented for Address because they -// only work for Address which cannot be expressed using serde -// attributes. -#[cfg(feature = "serde")] -mod _serde_impls { - use serde::de::Error as _; - impl serde::Serialize for super::Address { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.to_raw().serialize(serializer) - } - } - impl<'de> serde::Deserialize<'de> for super::Address { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - super::raw::Address::deserialize(deserializer) - .and_then(|raw| raw.try_into().map_err(D::Error::custom)) - } - } -} - -impl Clone for Address { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Address {} - -impl PartialEq for Address { - fn eq(&self, other: &Self) -> bool { - self.bytes.eq(&other.bytes) && self.prefix.eq(&other.prefix) - } -} - -impl Eq for Address {} - -impl Address { - #[must_use = "the builder must be used to construct an address to be useful"] - pub fn builder() -> AddressBuilder { - AddressBuilder::::new() - } - - #[must_use] - pub fn bytes(self) -> [u8; ADDRESS_LEN] { - self.bytes - } - - #[must_use] - pub fn as_bytes(&self) -> &[u8; ADDRESS_LEN] { - &self.bytes - } - - #[must_use] - pub fn prefix(&self) -> &str { - self.prefix.as_str() - } - - /// Converts to a new address with the given `prefix`. - /// - /// # Errors - /// Returns an error if an address with `prefix` cannot be constructed. - /// The error conditions for this are the same as for [`AddressBuilder::try_build`]. - pub fn to_prefix(&self, prefix: &str) -> Result { - Self::builder() - .array(*self.as_bytes()) - .prefix(prefix) - .try_build() - } - - /// Converts to a new address with the type argument `OtherFormat`. - /// - /// `OtherFormat` is usually [`Bech32`] or [`Bech32m`]. - #[must_use] - pub fn to_format(&self) -> Address { - Address { - bytes: self.bytes, - prefix: self.prefix, - format: PhantomData, - } - } -} - -impl Address { - /// Convert [`Address`] to a [`raw::Address`]. - #[expect( - clippy::missing_panics_doc, - reason = "panics are checked to not happen" - )] - #[must_use] - pub fn to_raw(&self) -> raw::Address { - let bech32m = - bech32::encode_lower::<::Checksum>(self.prefix, self.as_bytes()) - .expect( - "should not fail because len(prefix) + len(bytes) <= 63 < BECH32M::CODELENGTH", - ); - raw::Address { - bech32m, - } - } - - #[must_use] - pub fn into_raw(self) -> raw::Address { - self.to_raw() - } - - /// Convert from protobuf to rust type an address. - /// - /// # Errors - /// - /// Returns an error if the account buffer was not 20 bytes long. - pub fn try_from_raw(raw: &raw::Address) -> Result { - let raw::Address { - bech32m, - } = raw; - bech32m.parse() - } - - /// This should only be used where the inputs have been provided by a trusted entity, e.g. read - /// from our own state store. - /// - /// Note that this function is not considered part of the public API and is subject to breaking - /// change at any time. - #[cfg(feature = "unchecked-constructors")] - #[doc(hidden)] - #[must_use] - pub fn unchecked_from_parts(bytes: [u8; ADDRESS_LEN], prefix: &str) -> Self { - Self { - bytes, - prefix: bech32::Hrp::parse_unchecked(prefix), - format: PhantomData, - } - } -} - -impl From> for raw::Address { - fn from(value: Address) -> Self { - value.into_raw() - } -} - -impl FromStr for Address { - type Err = AddressError; - - fn from_str(s: &str) -> Result { - let checked = bech32::primitives::decode::CheckedHrpstring::new::(s) - .map_err(Self::Err::decode)?; - let hrp = checked.hrp(); - Self::builder() - .with_iter(checked.byte_iter()) - .prefix(hrp.as_str()) - .try_build() - } -} - -impl TryFrom for Address { - type Error = AddressError; - - fn try_from(value: raw::Address) -> Result { - Self::try_from_raw(&value) - } -} - -impl std::fmt::Display for Address { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use bech32::EncodeError; - match bech32::encode_lower_to_fmt::(f, self.prefix, self.as_bytes()) { - Ok(()) => Ok(()), - Err(EncodeError::Fmt(err)) => Err(err), - Err(err) => panic!( - "only formatting errors are valid when encoding astria addresses; all other error \ - variants (only TooLong as of bech32-0.11.0) are guaranteed to not happen because \ - `Address` is length checked:\n{err:?}", - ), - } - } -} -/// Constructs a dummy address from a given `prefix`, otherwise fail. -pub(crate) fn try_construct_dummy_address_from_prefix( - prefix: &str, -) -> Result<(), AddressError> { - Address::::builder() - .array([0u8; ADDRESS_LEN]) - .prefix(prefix) - .try_build() - .map(|_| ()) -} - /// Derive a [`merkle::Tree`] from an iterable. /// /// It is the responsibility of the caller to ensure that the iterable is @@ -798,42 +394,12 @@ enum TransactionIdErrorKind { mod tests { use super::{ Address, - AddressError, - AddressErrorKind, - Bech32m, ADDRESS_LEN, }; - use crate::primitive::v1::Bech32; + use crate::Protobuf as _; const ASTRIA_ADDRESS_PREFIX: &str = "astria"; const ASTRIA_COMPAT_ADDRESS_PREFIX: &str = "astriacompat"; - #[track_caller] - fn assert_wrong_address_bytes(bad_account: &[u8]) { - let error = Address::::builder() - .slice(bad_account) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .expect_err( - "converting from an incorrectly sized byte slice succeeded where it should have \ - failed", - ); - let AddressError(AddressErrorKind::IncorrectAddressLength { - received, - }) = error - else { - panic!("expected AddressErrorKind::IncorrectAddressLength, got {error:?}"); - }; - assert_eq!(bad_account.len(), received); - } - - #[test] - fn account_of_incorrect_length_gives_error() { - assert_wrong_address_bytes(&[42; 0]); - assert_wrong_address_bytes(&[42; 19]); - assert_wrong_address_bytes(&[42; 21]); - assert_wrong_address_bytes(&[42; 100]); - } - #[cfg(feature = "serde")] #[test] fn snapshots() { @@ -855,74 +421,6 @@ mod tests { insta::assert_snapshot!(&compat_address); } - #[test] - fn parse_bech32m_address() { - let expected = Address::builder() - .array([42; 20]) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let actual = expected.to_string().parse::
().unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn parse_bech32_address() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let actual = expected.to_string().parse::>().unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn parsing_bech32_address_as_bech32m_fails() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_COMPAT_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let err = expected - .to_string() - .parse::>() - .expect_err("this must not work"); - match err { - AddressError(AddressErrorKind::Decode { - .. - }) => {} - other => { - panic!( - "expected AddressError(AddressErrorKind::Decode {{ .. }}), but got {other:?}" - ) - } - } - } - - #[test] - fn parsing_bech32m_address_as_bech32_fails() { - let expected = Address::::builder() - .array([42; 20]) - .prefix(ASTRIA_ADDRESS_PREFIX) - .try_build() - .unwrap(); - let err = expected - .to_string() - .parse::>() - .expect_err("this must not work"); - match err { - AddressError(AddressErrorKind::Decode { - .. - }) => {} - other => { - panic!( - "expected AddressError(AddressErrorKind::Decode {{ .. }}), but got {other:?}" - ) - } - } - } - #[test] fn can_construct_protobuf_from_address_with_maximally_sized_prefix() { // 83 is the maximal length of a hrp @@ -945,7 +443,7 @@ mod tests { .try_build() .unwrap(); let unchecked = input.into_raw(); - let roundtripped = Address::try_from_raw(&unchecked).unwrap(); + let roundtripped = Address::try_from_raw(unchecked).unwrap(); assert_eq!(input, roundtripped); assert_eq!(input.as_bytes(), roundtripped.as_bytes()); assert_eq!("astria", input.prefix()); diff --git a/crates/astria-core/src/protocol/bridge/v1/mod.rs b/crates/astria-core/src/protocol/bridge/v1/mod.rs index df30ed9068..49b3e07c90 100644 --- a/crates/astria-core/src/protocol/bridge/v1/mod.rs +++ b/crates/astria-core/src/protocol/bridge/v1/mod.rs @@ -1,13 +1,18 @@ use bytes::Bytes; use super::raw; -use crate::primitive::v1::{ - asset, - asset::denom::ParseDenomError, - Address, - AddressError, - IncorrectRollupIdLength, - RollupId, +use crate::{ + primitive::v1::{ + asset::{ + self, + denom::ParseDenomError, + }, + Address, + AddressError, + IncorrectRollupIdLength, + RollupId, + }, + Protobuf as _, }; #[derive(Debug, Clone, PartialEq, Eq)] @@ -159,9 +164,9 @@ impl BridgeAccountInfoResponse { rollup_id: RollupId::try_from_raw(rollup_id) .map_err(BridgeAccountInfoResponseError::invalid_rollup_id)?, asset, - sudo_address: Address::try_from_raw(&sudo_address) + sudo_address: Address::try_from_raw(sudo_address) .map_err(BridgeAccountInfoResponseError::invalid_sudo_address)?, - withdrawer_address: Address::try_from_raw(&withdrawer_address) + withdrawer_address: Address::try_from_raw(withdrawer_address) .map_err(BridgeAccountInfoResponseError::invalid_withdrawer_address)?, }), }) diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index 69bb2269bd..8df8f7e2bf 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -10,7 +10,6 @@ use crate::{ denom::ParseTracePrefixedError, ParseDenomError, }, - try_construct_dummy_address_from_prefix, Address, AddressError, Bech32, @@ -183,16 +182,18 @@ impl Protobuf for GenesisAppState { .as_ref() .ok_or_else(|| Self::Error::field_not_set("authority_sudo_address")) .and_then(|addr| { - Address::try_from_raw(addr).map_err(Self::Error::authority_sudo_address) + Address::try_from_raw_ref(addr).map_err(Self::Error::authority_sudo_address) })?; let ibc_sudo_address = ibc_sudo_address .as_ref() .ok_or_else(|| Self::Error::field_not_set("ibc_sudo_address")) - .and_then(|addr| Address::try_from_raw(addr).map_err(Self::Error::ibc_sudo_address))?; + .and_then(|addr| { + Address::try_from_raw_ref(addr).map_err(Self::Error::ibc_sudo_address) + })?; let ibc_relayer_addresses = ibc_relayer_addresses .iter() - .map(Address::try_from_raw) + .map(Address::try_from_raw_ref) .collect::>() .map_err(Self::Error::ibc_relayer_addresses)?; @@ -405,7 +406,7 @@ impl Protobuf for Account { let address = address .as_ref() .ok_or_else(|| AccountError::field_not_set("address")) - .and_then(|addr| Address::try_from_raw(addr).map_err(Self::Error::address))?; + .and_then(|addr| Address::try_from_raw_ref(addr).map_err(Self::Error::address))?; let balance = balance .ok_or_else(|| AccountError::field_not_set("balance")) .map(Into::into)?; @@ -481,12 +482,22 @@ impl Protobuf for AddressPrefixes { type Raw = raw::AddressPrefixes; fn try_from_raw_ref(raw: &Self::Raw) -> Result { + fn dummy_addr(prefix: &str) -> Result<(), AddressError> { + Address::::builder() + .array([0u8; crate::primitive::v1::ADDRESS_LEN]) + .prefix(prefix) + .try_build() + .map(|_| ()) + } + let Self::Raw { base, ibc_compat, } = raw; - try_construct_dummy_address_from_prefix::(base).map_err(Self::Error::base)?; - try_construct_dummy_address_from_prefix::(ibc_compat).map_err(Self::Error::base)?; + + dummy_addr::(base).map_err(Self::Error::base)?; + dummy_addr::(ibc_compat).map_err(Self::Error::base)?; + Ok(Self { base: base.to_string(), ibc_compat: ibc_compat.to_string(), diff --git a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs index c4f8320f9c..80ba7d2326 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs @@ -538,7 +538,7 @@ impl Protobuf for Transfer { let Some(to) = to else { return Err(TransferError::field_not_set("to")); }; - let to = Address::try_from_raw(to).map_err(TransferError::address)?; + let to = Address::try_from_raw_ref(to).map_err(TransferError::address)?; let amount = amount.map_or(0, Into::into); let asset = asset.parse().map_err(TransferError::asset)?; let fee_asset = fee_asset.parse().map_err(TransferError::fee_asset)?; @@ -780,7 +780,7 @@ impl Protobuf for SudoAddressChange { return Err(SudoAddressChangeError::field_not_set("new_address")); }; let new_address = - Address::try_from_raw(new_address).map_err(SudoAddressChangeError::address)?; + Address::try_from_raw_ref(new_address).map_err(SudoAddressChangeError::address)?; Ok(Self { new_address, }) @@ -847,7 +847,7 @@ impl Protobuf for IbcSudoChange { return Err(IbcSudoChangeError::field_not_set("new_address")); }; let new_address = - Address::try_from_raw(new_address).map_err(IbcSudoChangeError::address)?; + Address::try_from_raw_ref(new_address).map_err(IbcSudoChangeError::address)?; Ok(Self { new_address, }) @@ -1034,7 +1034,7 @@ impl Protobuf for Ics20Withdrawal { } = proto; let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?; let return_address = Address::try_from_raw( - &return_address.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, + return_address.ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, ) .map_err(Ics20WithdrawalError::return_address)?; @@ -1042,7 +1042,6 @@ impl Protobuf for Ics20Withdrawal { .ok_or(Ics20WithdrawalError::field_not_set("timeout_height"))? .into(); let bridge_address = bridge_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(Ics20WithdrawalError::invalid_bridge_address)?; @@ -1090,12 +1089,13 @@ impl Protobuf for Ics20Withdrawal { use_compat_address, } = proto; let amount = amount.ok_or(Ics20WithdrawalError::field_not_set("amount"))?; - let return_address = Address::try_from_raw( - return_address - .as_ref() - .ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, - ) - .map_err(Ics20WithdrawalError::return_address)?; + let return_address = return_address + .as_ref() + .ok_or_else(|| Ics20WithdrawalError::field_not_set("return_address")) + .and_then(|return_address| { + Address::try_from_raw_ref(return_address) + .map_err(Ics20WithdrawalError::return_address) + })?; let timeout_height = timeout_height .clone() @@ -1103,7 +1103,7 @@ impl Protobuf for Ics20Withdrawal { .into(); let bridge_address = bridge_address .as_ref() - .map(Address::try_from_raw) + .map(Address::try_from_raw_ref) .transpose() .map_err(Ics20WithdrawalError::invalid_bridge_address)?; @@ -1245,14 +1245,14 @@ impl Protobuf for IbcRelayerChange { value: Some(raw::ibc_relayer_change::Value::Addition(address)), } => { let address = - Address::try_from_raw(address).map_err(IbcRelayerChangeError::address)?; + Address::try_from_raw_ref(address).map_err(IbcRelayerChangeError::address)?; Ok(IbcRelayerChange::Addition(address)) } raw::IbcRelayerChange { value: Some(raw::ibc_relayer_change::Value::Removal(address)), } => { let address = - Address::try_from_raw(address).map_err(IbcRelayerChangeError::address)?; + Address::try_from_raw_ref(address).map_err(IbcRelayerChangeError::address)?; Ok(IbcRelayerChange::Removal(address)) } _ => Err(IbcRelayerChangeError::missing_address()), @@ -1423,13 +1423,11 @@ impl Protobuf for InitBridgeAccount { .map_err(InitBridgeAccountError::invalid_fee_asset)?; let sudo_address = proto .sudo_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(InitBridgeAccountError::invalid_sudo_address)?; let withdrawer_address = proto .withdrawer_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(InitBridgeAccountError::invalid_withdrawer_address)?; @@ -1558,7 +1556,7 @@ impl Protobuf for BridgeLock { let Some(to) = proto.to else { return Err(BridgeLockError::field_not_set("to")); }; - let to = Address::try_from_raw(&to).map_err(BridgeLockError::address)?; + let to = Address::try_from_raw(to).map_err(BridgeLockError::address)?; let amount = proto.amount.ok_or(BridgeLockError::missing_amount())?; let asset = proto .asset @@ -1704,13 +1702,13 @@ impl Protobuf for BridgeUnlock { } = proto; let to = to .ok_or_else(|| BridgeUnlockError::field_not_set("to")) - .and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::address))?; + .and_then(|to| Address::try_from_raw(to).map_err(BridgeUnlockError::address))?; let amount = amount.ok_or_else(|| BridgeUnlockError::field_not_set("amount"))?; let fee_asset = fee_asset.parse().map_err(BridgeUnlockError::fee_asset)?; let bridge_address = bridge_address .ok_or_else(|| BridgeUnlockError::field_not_set("bridge_address")) - .and_then(|to| Address::try_from_raw(&to).map_err(BridgeUnlockError::bridge_address))?; + .and_then(|to| Address::try_from_raw(to).map_err(BridgeUnlockError::bridge_address))?; Ok(Self { to, amount: amount.into(), @@ -1825,17 +1823,15 @@ impl Protobuf for BridgeSudoChange { let Some(bridge_address) = proto.bridge_address else { return Err(BridgeSudoChangeError::field_not_set("bridge_address")); }; - let bridge_address = Address::try_from_raw(&bridge_address) + let bridge_address = Address::try_from_raw(bridge_address) .map_err(BridgeSudoChangeError::invalid_bridge_address)?; let new_sudo_address = proto .new_sudo_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(BridgeSudoChangeError::invalid_new_sudo_address)?; let new_withdrawer_address = proto .new_withdrawer_address - .as_ref() .map(Address::try_from_raw) .transpose() .map_err(BridgeSudoChangeError::invalid_new_withdrawer_address)?; diff --git a/crates/astria-core/src/sequencerblock/v1/block.rs b/crates/astria-core/src/sequencerblock/v1/block.rs index 48f8907fac..c7013e4d85 100644 --- a/crates/astria-core/src/sequencerblock/v1/block.rs +++ b/crates/astria-core/src/sequencerblock/v1/block.rs @@ -1428,7 +1428,7 @@ impl Deposit { return Err(DepositError::field_not_set("bridge_address")); }; let bridge_address = - Address::try_from_raw(&bridge_address).map_err(DepositError::address)?; + Address::try_from_raw(bridge_address).map_err(DepositError::address)?; let amount = amount.ok_or(DepositError::field_not_set("amount"))?.into(); let Some(rollup_id) = rollup_id else { return Err(DepositError::field_not_set("rollup_id")); diff --git a/crates/astria-sequencer-client/src/tests/http.rs b/crates/astria-sequencer-client/src/tests/http.rs index 23f1ee481c..9fca6a4949 100644 --- a/crates/astria-sequencer-client/src/tests/http.rs +++ b/crates/astria-sequencer-client/src/tests/http.rs @@ -12,6 +12,7 @@ use astria_core::{ Transaction, TransactionBody, }, + Protobuf as _, }; use hex_literal::hex; use prost::bytes::Bytes; diff --git a/crates/astria-sequencer/src/fees/tests.rs b/crates/astria-sequencer/src/fees/tests.rs index f486683a9e..088cf62de1 100644 --- a/crates/astria-sequencer/src/fees/tests.rs +++ b/crates/astria-sequencer/src/fees/tests.rs @@ -30,6 +30,7 @@ use astria_core::{ }, }, sequencerblock::v1::block::Deposit, + Protobuf as _, }; use cnidarium::StateDelta; diff --git a/crates/astria-sequencer/src/grpc/sequencer.rs b/crates/astria-sequencer/src/grpc/sequencer.rs index 7d74f077f6..2a87c73182 100644 --- a/crates/astria-sequencer/src/grpc/sequencer.rs +++ b/crates/astria-sequencer/src/grpc/sequencer.rs @@ -188,7 +188,7 @@ impl SequencerService for SequencerServer { )); }; - let address = Address::try_from_raw(&address).map_err(|e| { + let address = Address::try_from_raw(address).map_err(|e| { info!( error = %e, "failed to parse address from request",