diff --git a/Cargo.lock b/Cargo.lock index 2f8e4b408c..365d6010e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,6 +675,7 @@ dependencies = [ "astria-merkle", "base64 0.21.7", "base64-serde", + "bech32 0.11.0", "brotli", "bytes", "celestia-tendermint", diff --git a/crates/astria-bridge-withdrawer/src/withdrawer/ethereum/convert.rs b/crates/astria-bridge-withdrawer/src/withdrawer/ethereum/convert.rs index 7234459a17..ba0c6abf8c 100644 --- a/crates/astria-bridge-withdrawer/src/withdrawer/ethereum/convert.rs +++ b/crates/astria-bridge-withdrawer/src/withdrawer/ethereum/convert.rs @@ -2,8 +2,12 @@ use std::time::Duration; use astria_core::{ primitive::v1::{ - asset, - asset::Denom, + asset::{ + self, + Denom, + }, + Address, + ASTRIA_ADDRESS_PREFIX, }, protocol::transaction::v1alpha1::{ action::{ @@ -94,7 +98,11 @@ fn event_to_bridge_unlock( transaction_hash, }; let action = BridgeUnlockAction { - to: event.destination_chain_address.to_fixed_bytes().into(), + to: Address::builder() + .array(event.destination_chain_address.to_fixed_bytes()) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .wrap_err("failed to construct destination address")?, amount: event .amount .as_u128() @@ -126,7 +134,7 @@ fn event_to_ics20_withdrawal( // TODO: make this configurable const ICS20_WITHDRAWAL_TIMEOUT: Duration = Duration::from_secs(300); - let sender = event.sender.to_fixed_bytes().into(); + let sender = event.sender.to_fixed_bytes(); let denom = rollup_asset_denom.clone(); let (_, channel) = denom @@ -147,7 +155,11 @@ fn event_to_ics20_withdrawal( // returned to the rollup. // this is only ok for now because addresses on the sequencer and the rollup are both 20 // bytes, but this won't work otherwise. - return_address: sender, + return_address: Address::builder() + .array(sender) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .wrap_err("failed to construct return address")?, amount: event .amount .as_u128() @@ -202,7 +214,11 @@ mod tests { }; let expected_action = BridgeUnlockAction { - to: [1u8; 20].into(), + to: Address::builder() + .array([1u8; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 99, memo: serde_json::to_vec(&BridgeUnlockMemo { block_number: 1.into(), @@ -234,7 +250,11 @@ mod tests { }; let expected_action = BridgeUnlockAction { - to: [1u8; 20].into(), + to: Address::builder() + .array([1u8; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 99, memo: serde_json::to_vec(&BridgeUnlockMemo { block_number: 1.into(), @@ -274,7 +294,11 @@ mod tests { let expected_action = Ics20Withdrawal { denom: denom.clone(), destination_chain_address, - return_address: [0u8; 20].into(), + return_address: Address::builder() + .array([0u8; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 99, memo: serde_json::to_string(&Ics20WithdrawalMemo { memo: "hello".to_string(), diff --git a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/mod.rs b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/mod.rs index ad90e9e941..dd475a43c8 100644 --- a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/mod.rs +++ b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/mod.rs @@ -123,10 +123,14 @@ impl Submitter { let unsigned = UnsignedTransaction { actions, - params: TransactionParams { - nonce, - chain_id: self.sequencer_chain_id.clone(), - }, + params: TransactionParams::builder() + .nonce(nonce) + .chain_id(&self.sequencer_chain_id) + .try_build() + .context( + "failed to construct transcation parameters from latest nonce and configured \ + sequencer chain ID", + )?, }; // sign transaction @@ -234,7 +238,7 @@ async fn get_latest_nonce( name = "submit_tx", skip_all, fields( - nonce = tx.unsigned_transaction().params.nonce, + nonce = tx.nonce(), transaction.hash = %telemetry::display::hex(&tx.sha256_of_proto_encoding()), ) )] @@ -243,7 +247,7 @@ async fn submit_tx( tx: SignedTransaction, state: Arc, ) -> eyre::Result { - let nonce = tx.unsigned_transaction().params.nonce; + let nonce = tx.nonce(); metrics::gauge!(crate::metrics_init::CURRENT_NONCE).set(nonce); let start = std::time::Instant::now(); debug!("submitting signed transaction to sequencer"); diff --git a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/signer.rs b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/signer.rs index 853d08ff91..a75e6a5137 100644 --- a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/signer.rs +++ b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/signer.rs @@ -3,12 +3,18 @@ use std::{ path::Path, }; -use astria_core::crypto::SigningKey; +use astria_core::{ + crypto::SigningKey, + primitive::v1::{ + Address, + ASTRIA_ADDRESS_PREFIX, + }, +}; use astria_eyre::eyre::{ self, eyre, + WrapErr as _, }; -use sequencer_client::Address; pub(super) struct SequencerKey { pub(super) address: Address, @@ -27,7 +33,11 @@ impl SequencerKey { let signing_key = SigningKey::from(bytes); Ok(Self { - address: *signing_key.verification_key().address(), + address: Address::builder() + .array(signing_key.verification_key().address_bytes()) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .wrap_err("failed to construct Sequencer address")?, signing_key, }) } diff --git a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/tests.rs b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/tests.rs index 9f1f1fe053..21cdcb91fd 100644 --- a/crates/astria-bridge-withdrawer/src/withdrawer/submitter/tests.rs +++ b/crates/astria-bridge-withdrawer/src/withdrawer/submitter/tests.rs @@ -6,7 +6,11 @@ use std::{ use astria_core::{ generated::protocol::account::v1alpha1::NonceResponse, - primitive::v1::asset::Denom, + primitive::v1::{ + asset::Denom, + Address, + ASTRIA_ADDRESS_PREFIX, + }, protocol::transaction::v1alpha1::{ action::{ BridgeUnlockAction, @@ -167,7 +171,11 @@ fn make_ics20_withdrawal_action() -> Action { let inner = Ics20Withdrawal { denom: denom.clone(), destination_chain_address, - return_address: [0u8; 20].into(), + return_address: Address::builder() + .array([0u8; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 99, memo: serde_json::to_string(&Ics20WithdrawalMemo { memo: "hello".to_string(), @@ -187,7 +195,11 @@ fn make_ics20_withdrawal_action() -> Action { fn make_bridge_unlock_action() -> Action { let denom = Denom::from("nria".to_string()); let inner = BridgeUnlockAction { - to: [0u8; 20].into(), + to: Address::builder() + .array([0u8; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 99, memo: serde_json::to_vec(&BridgeUnlockMemo { block_number: 1.into(), diff --git a/crates/astria-cli/src/cli/sequencer.rs b/crates/astria-cli/src/cli/sequencer.rs index cca710530f..affb28d639 100644 --- a/crates/astria-cli/src/cli/sequencer.rs +++ b/crates/astria-cli/src/cli/sequencer.rs @@ -254,8 +254,7 @@ impl FromStr for SequencerAddressArg { "failed to decode address. address should be 20 bytes long. do not prefix with 0x", )?; let address = - Address::try_from_slice(address_bytes.as_ref()).wrap_err("failed to create address")?; - + crate::try_astria_address(&address_bytes).wrap_err("failed to create address")?; Ok(Self(address)) } } @@ -350,7 +349,7 @@ mod tests { fn test_sequencer_address_arg_from_str_valid() { let hex_str = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"; let bytes = hex::decode(hex_str).unwrap(); - let expected_address = Address::try_from_slice(&bytes).unwrap(); + let expected_address = crate::try_astria_address(&bytes).unwrap(); let sequencer_address_arg: SequencerAddressArg = hex_str.parse().unwrap(); assert_eq!(sequencer_address_arg, SequencerAddressArg(expected_address)); diff --git a/crates/astria-cli/src/commands/sequencer.rs b/crates/astria-cli/src/commands/sequencer.rs index 9b9460c08d..44f944b898 100644 --- a/crates/astria-cli/src/commands/sequencer.rs +++ b/crates/astria-cli/src/commands/sequencer.rs @@ -63,8 +63,7 @@ fn get_private_key_pretty(signing_key: &SigningKey) -> String { /// Get the address from the signing key fn get_address_pretty(signing_key: &SigningKey) -> String { - let address = *signing_key.verification_key().address(); - hex::encode(address.to_vec()) + hex::encode(signing_key.verification_key().address_bytes()) } /// Generates a new ED25519 keypair and prints the public key, private key, and address @@ -444,7 +443,7 @@ async fn submit_transaction( .map_err(|_| eyre!("invalid private key length; must be 32 bytes"))?; let sequencer_key = SigningKey::from(private_key_bytes); - let from_address = *sequencer_key.verification_key().address(); + let from_address = crate::astria_address(sequencer_key.verification_key().address_bytes()); println!("sending tx from address: {from_address}"); let nonce_res = sequencer_client @@ -453,10 +452,11 @@ async fn submit_transaction( .wrap_err("failed to get nonce")?; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: nonce_res.nonce, - chain_id, - }, + params: TransactionParams::builder() + .nonce(nonce_res.nonce) + .chain_id(chain_id) + .try_build() + .wrap_err("failed to construct transaction params from provided chain ID")?, actions: vec![action], } .into_signed(&sequencer_key); diff --git a/crates/astria-cli/src/lib.rs b/crates/astria-cli/src/lib.rs index 2c16dcbda6..2559502a00 100644 --- a/crates/astria-cli/src/lib.rs +++ b/crates/astria-cli/src/lib.rs @@ -1,3 +1,30 @@ +use astria_core::primitive::v1::{ + Address, + AddressError, +}; + pub mod cli; pub mod commands; pub mod types; + +const ADDRESS_PREFIX: &str = "astria"; + +/// Constructs an [`Address`] prefixed by `"astria"`. +pub(crate) fn astria_address(array: [u8; astria_core::primitive::v1::ADDRESS_LEN]) -> Address { + Address::builder() + .array(array) + .prefix(ADDRESS_PREFIX) + .try_build() + .unwrap() +} + +/// Tries to construct an [`Address`] prefixed by `"astria"` from a byte slice. +/// +/// # Errors +/// Fails if the slice does not contain 20 bytes. +pub(crate) fn try_astria_address(slice: &[u8]) -> Result { + Address::builder() + .slice(slice) + .prefix(ADDRESS_PREFIX) + .try_build() +} diff --git a/crates/astria-composer/src/executor/builder.rs b/crates/astria-composer/src/executor/builder.rs index 081de7993b..e24e281b31 100644 --- a/crates/astria-composer/src/executor/builder.rs +++ b/crates/astria-composer/src/executor/builder.rs @@ -6,6 +6,10 @@ use std::{ use astria_core::{ crypto::SigningKey, + primitive::v1::{ + Address, + ASTRIA_ADDRESS_PREFIX, + }, protocol::transaction::v1alpha1::action::SequenceAction, }; use astria_eyre::eyre::{ @@ -50,7 +54,11 @@ impl Builder { format!("failed reading signing key from file at path `{private_key_file}`") })?; - let sequencer_address = *sequencer_key.verification_key().address(); + let sequencer_address = Address::builder() + .prefix(ASTRIA_ADDRESS_PREFIX) + .array(sequencer_key.verification_key().address_bytes()) + .try_build() + .wrap_err("failed constructing a sequencer address from private key")?; let (serialized_rollup_transaction_tx, serialized_rollup_transaction_rx) = tokio::sync::mpsc::channel::(256); diff --git a/crates/astria-composer/src/executor/mod.rs b/crates/astria-composer/src/executor/mod.rs index 4c2dc7c23c..131446583b 100644 --- a/crates/astria-composer/src/executor/mod.rs +++ b/crates/astria-composer/src/executor/mod.rs @@ -467,7 +467,7 @@ async fn get_latest_nonce( name = "submit signed transaction", skip_all, fields( - nonce = tx.unsigned_transaction().params.nonce, + nonce = tx.nonce(), transaction.hash = hex::encode(sha256(&tx.to_raw().encode_to_vec())), ) )] @@ -475,7 +475,7 @@ async fn submit_tx( client: sequencer_client::HttpClient, tx: SignedTransaction, ) -> eyre::Result { - let nonce = tx.unsigned_transaction().params.nonce; + let nonce = tx.nonce(); metrics::gauge!(crate::metrics_init::CURRENT_NONCE).set(nonce); // TODO: change to info and log tx hash (to match info log in `SubmitFut`'s response handling @@ -568,10 +568,11 @@ impl Future for SubmitFut { let new_state = match this.state.project() { SubmitStateProj::NotStarted => { - let params = TransactionParams { - nonce: *this.nonce, - chain_id: this.chain_id.clone(), - }; + let params = TransactionParams::builder() + .nonce(*this.nonce) + .chain_id(&*this.chain_id) + .try_build() + .expect("configured chain ID is valid"); let tx = UnsignedTransaction { actions: this.bundle.clone().into_actions(), params, @@ -653,10 +654,11 @@ impl Future for SubmitFut { } => match ready!(fut.poll(cx)) { Ok(nonce) => { *this.nonce = nonce; - let params = TransactionParams { - nonce: *this.nonce, - chain_id: this.chain_id.clone(), - }; + let params = TransactionParams::builder() + .nonce(*this.nonce) + .chain_id(&*this.chain_id) + .try_build() + .expect("configured chain ID is valid"); let tx = UnsignedTransaction { actions: this.bundle.clone().into_actions(), params, diff --git a/crates/astria-composer/tests/blackbox/helper/mod.rs b/crates/astria-composer/tests/blackbox/helper/mod.rs index fe09219ef9..ea802b2896 100644 --- a/crates/astria-composer/tests/blackbox/helper/mod.rs +++ b/crates/astria-composer/tests/blackbox/helper/mod.rs @@ -177,10 +177,7 @@ fn rollup_id_nonce_from_request(request: &Request) -> (RollupId, u32) { panic!("mocked sequencer expected a sequence action"); }; - ( - sequence_action.rollup_id, - signed_tx.unsigned_transaction().params.nonce, - ) + (sequence_action.rollup_id, signed_tx.nonce()) } /// Deserializes the bytes contained in a `tx_sync::Request` to a signed sequencer transaction and diff --git a/crates/astria-core/Cargo.toml b/crates/astria-core/Cargo.toml index 7d273b985e..b9ca603ebe 100644 --- a/crates/astria-core/Cargo.toml +++ b/crates/astria-core/Cargo.toml @@ -16,6 +16,7 @@ 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 } diff --git a/crates/astria-core/src/crypto.rs b/crates/astria-core/src/crypto.rs index 2c1b6a98e6..1c3e919635 100644 --- a/crates/astria-core/src/crypto.rs +++ b/crates/astria-core/src/crypto.rs @@ -10,7 +10,6 @@ use std::{ Hash, Hasher, }, - sync::OnceLock, }; use base64::{ @@ -37,10 +36,7 @@ use zeroize::{ ZeroizeOnDrop, }; -use crate::primitive::v1::{ - Address, - ADDRESS_LEN, -}; +use crate::primitive::v1::ADDRESS_LEN; /// An Ed25519 signing key. // *Implementation note*: this is currently a refinement type around @@ -78,7 +74,6 @@ impl SigningKey { pub fn verification_key(&self) -> VerificationKey { VerificationKey { key: self.0.verification_key(), - address: OnceLock::new(), } } } @@ -116,19 +111,17 @@ impl From<[u8; 32]> for SigningKey { #[derive(Clone)] pub struct VerificationKey { key: Ed25519VerificationKey, - // The address is lazily-initialized. Since it may or may not be initialized for any given - // instance of a verification key, it is excluded from `PartialEq`, `Eq`, `PartialOrd`, `Ord` - // and `Hash` impls. - address: OnceLock
, } impl VerificationKey { /// Returns the byte encoding of the verification key. + #[must_use] pub fn to_bytes(&self) -> [u8; 32] { self.key.to_bytes() } /// Returns the byte encoding of the verification key. + #[must_use] pub fn as_bytes(&self) -> &[u8; 32] { self.key.as_bytes() } @@ -147,28 +140,28 @@ impl VerificationKey { // Silence the clippy lint because the function body asserts that the panic // cannot happen. #[allow(clippy::missing_panics_doc)] - pub fn address(&self) -> &Address { - self.address.get_or_init(|| { - /// this ensures that `ADDRESS_LEN` is never accidentally changed to a value - /// that would violate this assumption. - #[allow(clippy::assertions_on_constants)] - const _: () = assert!(ADDRESS_LEN <= 32); - let bytes: [u8; 32] = Sha256::digest(self).into(); - Address::try_from_slice(&bytes[..ADDRESS_LEN]) - .expect("can convert 32 byte hash to 20 byte array") - }) + #[must_use] + pub fn address_bytes(&self) -> [u8; ADDRESS_LEN] { + fn first_20(array: [u8; 32]) -> [u8; ADDRESS_LEN] { + [ + array[0], array[1], array[2], array[3], array[4], array[5], array[6], array[7], + array[8], array[9], array[10], array[11], array[12], array[13], array[14], + array[15], array[16], array[17], array[18], array[19], + ] + } + /// this ensures that `ADDRESS_LEN` is never accidentally changed to a value + /// that would violate this assumption. + #[allow(clippy::assertions_on_constants)] + const _: () = assert!(ADDRESS_LEN <= 32); + let bytes: [u8; 32] = Sha256::digest(self).into(); + first_20(bytes) } } impl Debug for VerificationKey { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - let mut debug_struct = formatter.debug_struct("VerifyingKey"); + let mut debug_struct = formatter.debug_struct("VerificationKey"); debug_struct.field("key", &BASE64_STANDARD.encode(self.key.as_ref())); - if let Some(address) = self.address.get() { - debug_struct.field("address", address); - } else { - debug_struct.field("address", &"unset"); - } debug_struct.finish() } } @@ -218,7 +211,6 @@ impl TryFrom<&[u8]> for VerificationKey { let key = Ed25519VerificationKey::try_from(slice)?; Ok(Self { key, - address: OnceLock::new(), }) } } @@ -230,7 +222,6 @@ impl TryFrom<[u8; 32]> for VerificationKey { let key = Ed25519VerificationKey::try_from(bytes)?; Ok(Self { key, - address: OnceLock::new(), }) } } @@ -291,22 +282,18 @@ mod tests { // A key which compares greater than "low" ones below, and with its address uninitialized. let high_uninit = VerificationKey { key: SigningKey::from([255; 32]).0.verification_key(), - address: OnceLock::new(), }; // A key equal to `high_uninit`, but with its address initialized. let high_init = VerificationKey { key: high_uninit.key, - address: OnceLock::from(Address::from([255; 20])), }; // A key which compares less than "high" ones above, and with its address uninitialized. let low_uninit = VerificationKey { key: SigningKey::from([0; 32]).0.verification_key(), - address: OnceLock::new(), }; // A key equal to `low_uninit`, but with its address initialized. let low_init = VerificationKey { key: low_uninit.key, - address: OnceLock::from(Address::from([255; 20])), }; assert!(high_uninit.cmp(&high_uninit) == Ordering::Equal); @@ -412,15 +399,12 @@ mod tests { // Check verification keys compare equal if and only if their keys are equal. let key0 = VerificationKey { key: SigningKey::from([0; 32]).0.verification_key(), - address: OnceLock::new(), }; let other_key0 = VerificationKey { key: SigningKey::from([0; 32]).0.verification_key(), - address: OnceLock::from(Address::from([0; 20])), }; let key1 = VerificationKey { key: SigningKey::from([1; 32]).0.verification_key(), - address: OnceLock::new(), }; assert!(key0 == other_key0); diff --git a/crates/astria-core/src/generated/astria.primitive.v1.rs b/crates/astria-core/src/generated/astria.primitive.v1.rs index eab0146c7c..5fd85b6e0f 100644 --- a/crates/astria-core/src/generated/astria.primitive.v1.rs +++ b/crates/astria-core/src/generated/astria.primitive.v1.rs @@ -81,13 +81,24 @@ impl ::prost::Name for RollupId { } /// An Astria `Address`. /// -/// Astria addresses are derived from the ed25519 public key, -/// using the first 20 bytes of the sha256 hash. +/// Astria addresses are bech32m encoded strings, with the data part being the +/// first 20 entries of a sha256-hashed ed25519 public key. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Address { + /// The first 20 bytes of a sha256-hashed ed25519 public key. + /// Implementors must avoid setting this field in favor of `bech32m`. + /// Implementors must not accept both `inner` and `bech32m` being set. + /// DEPRECATED: this field is deprecated and only exists for backward compatibility. + /// Astria services assume an implicit prefix of "astria" if this field is set. + /// Astria services will read this field but will never emit it. + #[deprecated] #[prost(bytes = "bytes", tag = "1")] pub inner: ::prost::bytes::Bytes, + /// A bech32m encoded string. The data are the first 20 bytes of a sha256-hashed ed25519 + /// public key. Implementors must not accept both the `bytes` and `bech32m` being set. + #[prost(string, tag = "2")] + pub bech32m: ::prost::alloc::string::String, } impl ::prost::Name for Address { const NAME: &'static str = "Address"; diff --git a/crates/astria-core/src/generated/astria.primitive.v1.serde.rs b/crates/astria-core/src/generated/astria.primitive.v1.serde.rs index ffa9858f43..915990f21c 100644 --- a/crates/astria-core/src/generated/astria.primitive.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.primitive.v1.serde.rs @@ -9,11 +9,17 @@ impl serde::Serialize for Address { if !self.inner.is_empty() { len += 1; } + if !self.bech32m.is_empty() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("astria.primitive.v1.Address", len)?; if !self.inner.is_empty() { #[allow(clippy::needless_borrow)] struct_ser.serialize_field("inner", pbjson::private::base64::encode(&self.inner).as_str())?; } + if !self.bech32m.is_empty() { + struct_ser.serialize_field("bech32m", &self.bech32m)?; + } struct_ser.end() } } @@ -25,11 +31,13 @@ impl<'de> serde::Deserialize<'de> for Address { { const FIELDS: &[&str] = &[ "inner", + "bech32m", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Inner, + Bech32m, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -52,6 +60,7 @@ impl<'de> serde::Deserialize<'de> for Address { { match value { "inner" => Ok(GeneratedField::Inner), + "bech32m" => Ok(GeneratedField::Bech32m), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -72,6 +81,7 @@ impl<'de> serde::Deserialize<'de> for Address { V: serde::de::MapAccess<'de>, { let mut inner__ = None; + let mut bech32m__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::Inner => { @@ -82,10 +92,17 @@ impl<'de> serde::Deserialize<'de> for Address { Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) ; } + GeneratedField::Bech32m => { + if bech32m__.is_some() { + return Err(serde::de::Error::duplicate_field("bech32m")); + } + bech32m__ = Some(map_.next_value()?); + } } } Ok(Address { inner: inner__.unwrap_or_default(), + bech32m: bech32m__.unwrap_or_default(), }) } } diff --git a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs index 09b82f6408..db19dfc0a7 100644 --- a/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transactions.v1alpha1.rs @@ -186,8 +186,10 @@ pub struct Ics20Withdrawal { pub destination_chain_address: ::prost::alloc::string::String, /// an Astria address to use to return funds from this withdrawal /// in the case it fails. - #[prost(bytes = "vec", tag = "4")] - pub return_address: ::prost::alloc::vec::Vec, + #[prost(message, optional, tag = "4")] + pub return_address: ::core::option::Option< + super::super::super::primitive::v1::Address, + >, /// the height (on Astria) at which this transfer expires. #[prost(message, optional, tag = "5")] pub timeout_height: ::core::option::Option, diff --git a/crates/astria-core/src/primitive/v1/mod.rs b/crates/astria-core/src/primitive/v1/mod.rs index 944d6e40bd..26c958aca3 100644 --- a/crates/astria-core/src/primitive/v1/mod.rs +++ b/crates/astria-core/src/primitive/v1/mod.rs @@ -5,6 +5,7 @@ use base64::{ display::Base64Display, prelude::BASE64_STANDARD, }; +use bytes::Bytes; use sha2::{ Digest as _, Sha256, @@ -16,6 +17,9 @@ use crate::{ }; pub const ADDRESS_LEN: usize = 20; +/// The human readable prefix of astria addresses (also known as bech32 HRP). +pub const ASTRIA_ADDRESS_PREFIX: &str = "astria"; + pub const ROLLUP_ID_LEN: usize = 32; pub const FEE_ASSET_ID_LEN: usize = 32; @@ -243,60 +247,211 @@ pub struct IncorrectRollupIdLength { received: usize, } -/// Indicates that the protobuf response contained an array field that was not 20 bytes long. #[derive(Debug, thiserror::Error)] -#[error("expected 20 bytes, got {received}")] -pub struct IncorrectAddressLength { - received: usize, +#[error(transparent)] +pub struct AddressError(AddressErrorKind); + +impl AddressError { + fn bech32m_decode(source: bech32::DecodeError) -> Self { + Self(AddressErrorKind::Bech32mDecode { + source, + }) + } + + fn invalid_prefix(source: bech32::primitives::hrp::Error) -> Self { + Self(AddressErrorKind::InvalidPrefix { + source, + }) + } + + fn fields_are_mutually_exclusive() -> Self { + Self(AddressErrorKind::FieldsAreMutuallyExclusive) + } + + fn incorrect_address_length(received: usize) -> Self { + Self(AddressErrorKind::IncorrectAddressLength { + received, + }) + } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct Address( - #[cfg_attr(feature = "serde", serde(serialize_with = "crate::serde::base64"))] - [u8; ADDRESS_LEN], -); +#[derive(Debug, thiserror::Error, PartialEq)] +enum AddressErrorKind { + #[error("failed decoding provided bech32m string")] + Bech32mDecode { source: bech32::DecodeError }, + #[error("fields `inner` and `bech32m` are mutually exclusive, only one can be set")] + FieldsAreMutuallyExclusive, + #[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, + }, +} -impl Address { - #[must_use] - pub fn get(self) -> [u8; ADDRESS_LEN] { - self.0 +pub struct NoBytes; +pub struct NoPrefix; +pub struct WithBytes<'a>(BytesInner<'a>); +enum BytesInner<'a> { + Array([u8; ADDRESS_LEN]), + Slice(std::borrow::Cow<'a, [u8]>), +} +pub struct WithPrefix<'a>(std::borrow::Cow<'a, str>); + +pub struct AddressBuilder { + bytes: TBytes, + prefix: TPrefix, +} + +impl AddressBuilder { + const fn new() -> Self { + Self { + bytes: NoBytes, + prefix: NoPrefix, + } } +} - #[must_use] - pub fn to_vec(&self) -> Vec { - self.0.to_vec() +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(BytesInner::Array(array)), + prefix: self.prefix, + } + } + + #[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(BytesInner::Slice(bytes.into())), + prefix: self.prefix, + } } - /// Convert a byte slice to an address. + #[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()), + } + } +} + +impl<'a, 'b> AddressBuilder, WithPrefix<'b>> { + /// Attempts to build an address from the configured prefix and bytes. /// /// # Errors - /// - /// Returns an error if the account buffer was not 20 bytes long. - pub fn try_from_slice(bytes: &[u8]) -> Result { - let inner = <[u8; ADDRESS_LEN]>::try_from(bytes).map_err(|_| IncorrectAddressLength { - received: bytes.len(), - })?; - Ok(Self::from_array(inner)) + /// 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 { + let Self { + bytes: WithBytes(bytes), + prefix: WithPrefix(prefix), + } = self; + let bytes = match bytes { + BytesInner::Array(bytes) => bytes, + BytesInner::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, + }) + } +} + +// Private setters only used within this crate to not leak bech32 +impl AddressBuilder { + pub(crate) fn array__( + self, + array: [u8; ADDRESS_LEN], + ) -> AddressBuilder<[u8; ADDRESS_LEN], TPrefix> { + AddressBuilder { + bytes: array, + prefix: self.prefix, + } + } + + pub(crate) fn hrp__(self, prefix: bech32::Hrp) -> AddressBuilder { + AddressBuilder { + bytes: self.bytes, + prefix, + } + } +} + +// private builder to not leak bech32 +impl AddressBuilder<[u8; ADDRESS_LEN], bech32::Hrp> { + pub(crate) fn build(self) -> Address { + Address { + bytes: self.bytes, + prefix: self.prefix, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(into = "raw::Address"))] +pub struct Address { + bytes: [u8; ADDRESS_LEN], + prefix: bech32::Hrp, +} + +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 const fn from_array(array: [u8; ADDRESS_LEN]) -> Self { - Self(array) + pub fn bytes(self) -> [u8; ADDRESS_LEN] { + self.bytes } + /// Convert a string containing a bech32m string to an astria address. + /// + /// # Errors + /// Returns an error if: + /// + `input` is not bech32m encoded. + /// + the decoded data contained in `input` is not 20 bytes long. + /// + the bech32 hrp prefix exceeds 16 bytes. + pub fn try_from_bech32m(input: &str) -> Result { + let (hrp, bytes) = bech32::decode(input).map_err(AddressError::bech32m_decode)?; + Self::builder() + .slice(bytes) + .prefix(hrp.as_str()) + .try_build() + } + + /// Convert [`Address`] to a [`raw::Address`]. + // allow: panics are checked to not happen + #[allow(clippy::missing_panics_doc)] #[must_use] pub fn to_raw(&self) -> raw::Address { + let bech32m = bech32::encode_lower::(self.prefix, &self.bytes()) + .expect("must not fail because len(prefix) + len(bytes) <= 63 < BECH32M::CODELENGTH"); + // allow: the field is deprecated, but we must still fill it in + #[allow(deprecated)] raw::Address { - inner: self.to_vec().into(), + inner: Bytes::new(), + bech32m, } } #[must_use] pub fn into_raw(self) -> raw::Address { - raw::Address { - inner: self.to_vec().into(), - } + self.to_raw() } /// Convert from protobuf to rust type an address. @@ -304,26 +459,50 @@ impl Address { /// # Errors /// /// Returns an error if the account buffer was not 20 bytes long. - pub fn try_from_raw(raw: &raw::Address) -> Result { - Self::try_from_slice(&raw.inner) + pub fn try_from_raw(raw: &raw::Address) -> Result { + // allow: `Address::inner` field is deprecated, but we must still check it + #[allow(deprecated)] + let raw::Address { + inner, + bech32m, + } = raw; + if bech32m.is_empty() { + return Self::builder() + .slice(inner.as_ref()) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build(); + } + if inner.is_empty() { + return Self::try_from_bech32m(bech32m); + } + Err(AddressError::fields_are_mutually_exclusive()) } } impl AsRef<[u8]> for Address { fn as_ref(&self) -> &[u8] { - &self.0 + &self.bytes } } -impl From<[u8; ADDRESS_LEN]> for Address { - fn from(inner: [u8; ADDRESS_LEN]) -> Self { - Self(inner) +impl From
for raw::Address { + fn from(value: Address) -> Self { + value.into_raw() } } impl std::fmt::Display for Address { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Base64Display::new(self.as_ref(), &BASE64_STANDARD).fmt(f) + use bech32::EncodeError; + match bech32::encode_lower_to_fmt::(f, self.prefix, &self.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 at of bech32-0.11.0) are guaranteed to not \ + happen:\n{err:?}", + ), + } } } @@ -347,41 +526,140 @@ where #[cfg(test)] mod tests { + use bytes::Bytes; use insta::assert_json_snapshot; use super::{ + raw, Address, - IncorrectAddressLength, + AddressError, + AddressErrorKind, + ADDRESS_LEN, + ASTRIA_ADDRESS_PREFIX, }; - #[test] - fn account_of_20_bytes_is_converted_correctly() { - let expected = Address([42; 20]); - let account_vec = expected.0.to_vec(); - let actual = Address::try_from_slice(&account_vec).unwrap(); - assert_eq!(expected, actual); - } - #[track_caller] - fn account_conversion_check(bad_account: &[u8]) { - let error = Address::try_from_slice(bad_account); - assert!( - matches!(error, Err(IncorrectAddressLength { .. })), - "converting form incorrect sized account succeeded where it should have failed" - ); + 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() { - account_conversion_check(&[42; 0]); - account_conversion_check(&[42; 19]); - account_conversion_check(&[42; 21]); - account_conversion_check(&[42; 100]); + 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 snapshots() { - let address = Address([42; 20]); + let address = Address::builder() + .array([42; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); assert_json_snapshot!(address); } + + #[test] + fn can_construct_protobuf_from_address_with_maximally_sized_prefix() { + // 83 is the maximal length of a hrp + let long_prefix = [b'a'; 83]; + let address = Address::builder() + .array([42u8; ADDRESS_LEN]) + .prefix(std::str::from_utf8(&long_prefix).unwrap()) + .try_build() + .unwrap(); + let _ = address.into_raw(); + } + + #[test] + fn bech32m_and_deprecated_bytes_field_are_mutually_exclusive() { + // allow: `Address::inner` field is deprecated, but we must still check it + #![allow(deprecated)] + let bytes = Bytes::copy_from_slice(&[24u8; ADDRESS_LEN]); + let bech32m = [42u8; ADDRESS_LEN]; + let proto = super::raw::Address { + inner: bytes.clone(), + bech32m: bech32::encode_lower::( + bech32::Hrp::parse(ASTRIA_ADDRESS_PREFIX).unwrap(), + &bech32m, + ) + .unwrap(), + }; + let expected = AddressErrorKind::FieldsAreMutuallyExclusive; + let actual = Address::try_from_raw(&proto) + .expect_err("returned a valid address where it should have errored"); + assert_eq!(expected, actual.0); + } + + #[test] + fn proto_with_missing_bech32m_is_accepted_and_assumed_astria() { + // allow: `Address::inner` field is deprecated, but we must still check it + #![allow(deprecated)] + let bytes = [42u8; ADDRESS_LEN]; + let input = raw::Address { + inner: Bytes::copy_from_slice(&bytes), + bech32m: String::new(), + }; + let address = Address::try_from_raw(&input).unwrap(); + assert_eq!("astria", address.prefix.as_str()); + assert_eq!(bytes, address.bytes()); + } + + #[test] + fn proto_with_missing_bytes_is_accepted() { + // allow: `Address::inner` field is deprecated, but we must still check it + #![allow(deprecated)] + let bytes = [42u8; ADDRESS_LEN]; + let input = raw::Address { + inner: Bytes::new(), + bech32m: bech32::encode_lower::( + bech32::Hrp::parse(ASTRIA_ADDRESS_PREFIX).unwrap(), + &bytes, + ) + .unwrap(), + }; + let address = Address::try_from_raw(&input).unwrap(); + assert_eq!(bytes, address.bytes()); + } + + #[test] + fn protobuf_only_has_bech32m_populated() { + // allow: `Address::inner` field is deprecated, but we must still check it + #![allow(deprecated)] + let bytes = [42u8; ADDRESS_LEN]; + let address = Address::builder() + .array(bytes) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(); + let output = address.into_raw(); + assert!( + output.inner.is_empty(), + "the deprecated bytes field must not be set" + ); + assert_eq!( + bech32::encode_lower::( + bech32::Hrp::parse(ASTRIA_ADDRESS_PREFIX).unwrap(), + &bytes + ) + .unwrap(), + output.bech32m + ); + } } diff --git a/crates/astria-core/src/primitive/v1/snapshots/astria_core__primitive__v1__tests__snapshots.snap b/crates/astria-core/src/primitive/v1/snapshots/astria_core__primitive__v1__tests__snapshots.snap index 80054cbee8..7aca827420 100644 --- a/crates/astria-core/src/primitive/v1/snapshots/astria_core__primitive__v1__tests__snapshots.snap +++ b/crates/astria-core/src/primitive/v1/snapshots/astria_core__primitive__v1__tests__snapshots.snap @@ -2,4 +2,6 @@ source: crates/astria-core/src/primitive/v1/mod.rs expression: address --- -"KioqKioqKioqKioqKioqKioqKio=" +{ + "bech32m": "astria19g4z52329g4z52329g4z52329g4z5232ayenag" +} diff --git a/crates/astria-core/src/protocol/test_utils.rs b/crates/astria-core/src/protocol/test_utils.rs index 4fa099e202..8122232846 100644 --- a/crates/astria-core/src/protocol/test_utils.rs +++ b/crates/astria-core/src/protocol/test_utils.rs @@ -106,10 +106,11 @@ impl ConfigureSequencerBlock { } else { let unsigned_transaction = UnsignedTransaction { actions, - params: TransactionParams { - nonce: 1, - chain_id: chain_id.clone(), - }, + params: TransactionParams::builder() + .nonce(1) + .chain_id(chain_id.clone()) + .try_build() + .unwrap(), }; vec![unsigned_transaction.into_signed(&signing_key)] }; diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/action.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/action.rs index 04034202a9..580573a60e 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/action.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/action.rs @@ -16,7 +16,7 @@ use crate::{ Denom, }, Address, - IncorrectAddressLength, + AddressError, IncorrectRollupIdLength, RollupId, }, @@ -467,7 +467,7 @@ impl TransferAction { let Some(to) = to else { return Err(TransferActionError::field_not_set("to")); }; - let to = Address::try_from_raw(&to).map_err(TransferActionError::address_length)?; + let to = Address::try_from_raw(&to).map_err(TransferActionError::address)?; let amount = amount.map_or(0, Into::into); let asset_id = asset::Id::try_from_slice(&asset_id).map_err(TransferActionError::asset_id)?; @@ -492,8 +492,8 @@ impl TransferActionError { Self(TransferActionErrorKind::FieldNotSet(field)) } - fn address_length(inner: IncorrectAddressLength) -> Self { - Self(TransferActionErrorKind::AddressLength(inner)) + fn address(inner: AddressError) -> Self { + Self(TransferActionErrorKind::Address(inner)) } fn asset_id(inner: asset::IncorrectAssetIdLength) -> Self { @@ -510,7 +510,7 @@ enum TransferActionErrorKind { #[error("the expected field in the raw source type was not set: `{0}`")] FieldNotSet(&'static str), #[error("`to` field did not contain a valid address")] - AddressLength(#[source] IncorrectAddressLength), + Address(#[source] AddressError), #[error("`asset_id` field did not contain a valid asset ID")] Asset(#[source] asset::IncorrectAssetIdLength), #[error("`fee_asset_id` field did not contain a valid asset ID")] @@ -576,8 +576,10 @@ impl SudoAddressChangeActionError { Self(SudoAddressChangeActionErrorKind::FieldNotSet(field)) } - fn address(inner: IncorrectAddressLength) -> Self { - Self(SudoAddressChangeActionErrorKind::Address(inner)) + fn address(source: AddressError) -> Self { + Self(SudoAddressChangeActionErrorKind::Address { + source, + }) } } @@ -586,7 +588,7 @@ enum SudoAddressChangeActionErrorKind { #[error("the expected field in the raw source type was not set: `{0}`")] FieldNotSet(&'static str), #[error("`new_address` field did not contain a valid address")] - Address(#[source] IncorrectAddressLength), + Address { source: AddressError }, } /// Represents an IBC withdrawal of an asset from a source chain to a destination chain. @@ -683,7 +685,7 @@ impl Ics20Withdrawal { amount: Some(self.amount.into()), denom: self.denom.to_string(), destination_chain_address: self.destination_chain_address.clone(), - return_address: self.return_address.to_vec(), + return_address: Some(self.return_address.into_raw()), timeout_height: Some(self.timeout_height.into_raw()), timeout_time: self.timeout_time, source_channel: self.source_channel.to_string(), @@ -698,7 +700,7 @@ impl Ics20Withdrawal { amount: Some(self.amount.into()), denom: self.denom.to_string(), destination_chain_address: self.destination_chain_address, - return_address: self.return_address.to_vec(), + return_address: Some(self.return_address.into_raw()), timeout_height: Some(self.timeout_height.into_raw()), timeout_time: self.timeout_time, source_channel: self.source_channel.to_string(), @@ -713,16 +715,22 @@ impl Ics20Withdrawal { /// /// - if the `amount` field is missing /// - if the `denom` field is invalid - /// - if the `return_address` field is invalid + /// - if the `return_address` field is invalid or missing /// - if the `timeout_height` field is missing /// - if the `source_channel` field is invalid pub fn try_from_raw(proto: raw::Ics20Withdrawal) -> Result { - let amount = proto.amount.ok_or(Ics20WithdrawalError::missing_amount())?; - let return_address = Address::try_from_slice(&proto.return_address) - .map_err(Ics20WithdrawalError::invalid_return_address)?; + let amount = proto + .amount + .ok_or(Ics20WithdrawalError::field_not_set("amount"))?; + let return_address = Address::try_from_raw( + &proto + .return_address + .ok_or(Ics20WithdrawalError::field_not_set("return_address"))?, + ) + .map_err(Ics20WithdrawalError::return_address)?; let timeout_height = proto .timeout_height - .ok_or(Ics20WithdrawalError::missing_timeout_height())? + .ok_or(Ics20WithdrawalError::field_not_set("timeout_height"))? .into(); Ok(Self { @@ -777,18 +785,17 @@ pub struct Ics20WithdrawalError(Ics20WithdrawalErrorKind); impl Ics20WithdrawalError { #[must_use] - fn missing_amount() -> Self { - Self(Ics20WithdrawalErrorKind::MissingAmount) - } - - #[must_use] - fn invalid_return_address(err: IncorrectAddressLength) -> Self { - Self(Ics20WithdrawalErrorKind::InvalidReturnAddress(err)) + fn field_not_set(field: &'static str) -> Self { + Self(Ics20WithdrawalErrorKind::FieldNotSet { + field, + }) } #[must_use] - fn missing_timeout_height() -> Self { - Self(Ics20WithdrawalErrorKind::MissingTimeoutHeight) + fn return_address(source: AddressError) -> Self { + Self(Ics20WithdrawalErrorKind::ReturnAddress { + source, + }) } #[must_use] @@ -804,12 +811,10 @@ impl Ics20WithdrawalError { #[derive(Debug, thiserror::Error)] enum Ics20WithdrawalErrorKind { - #[error("`amount` field was missing")] - MissingAmount, + #[error("expected field `{field}` was not set`")] + FieldNotSet { field: &'static str }, #[error("`return_address` field was invalid")] - InvalidReturnAddress(#[source] IncorrectAddressLength), - #[error("`timeout_height` field was missing")] - MissingTimeoutHeight, + ReturnAddress { source: AddressError }, #[error("`source_channel` field was invalid")] InvalidSourceChannel(#[source] IdentifierError), #[error("`fee_asset_id` field was invalid")] @@ -868,15 +873,15 @@ impl IbcRelayerChangeAction { raw::IbcRelayerChangeAction { value: Some(raw::ibc_relayer_change_action::Value::Addition(address)), } => { - let address = Address::try_from_raw(address) - .map_err(IbcRelayerChangeActionError::invalid_address)?; + let address = + Address::try_from_raw(address).map_err(IbcRelayerChangeActionError::address)?; Ok(IbcRelayerChangeAction::Addition(address)) } raw::IbcRelayerChangeAction { value: Some(raw::ibc_relayer_change_action::Value::Removal(address)), } => { - let address = Address::try_from_raw(address) - .map_err(IbcRelayerChangeActionError::invalid_address)?; + let address = + Address::try_from_raw(address).map_err(IbcRelayerChangeActionError::address)?; Ok(IbcRelayerChangeAction::Removal(address)) } _ => Err(IbcRelayerChangeActionError::missing_address()), @@ -890,8 +895,10 @@ pub struct IbcRelayerChangeActionError(IbcRelayerChangeActionErrorKind); impl IbcRelayerChangeActionError { #[must_use] - fn invalid_address(err: IncorrectAddressLength) -> Self { - Self(IbcRelayerChangeActionErrorKind::InvalidAddress(err)) + fn address(source: AddressError) -> Self { + Self(IbcRelayerChangeActionErrorKind::Address { + source, + }) } #[must_use] @@ -902,9 +909,9 @@ impl IbcRelayerChangeActionError { #[derive(Debug, thiserror::Error)] enum IbcRelayerChangeActionErrorKind { - #[error("the address was invalid")] - InvalidAddress(#[source] IncorrectAddressLength), - #[error("the address was missing")] + #[error("the `address` was invalid")] + Address { source: AddressError }, + #[error("the `address` was not set")] MissingAddress, } @@ -1147,7 +1154,7 @@ impl BridgeLockAction { let Some(to) = proto.to else { return Err(BridgeLockActionError::field_not_set("to")); }; - let to = Address::try_from_raw(&to).map_err(BridgeLockActionError::invalid_address)?; + let to = Address::try_from_raw(&to).map_err(BridgeLockActionError::address)?; let amount = proto .amount .ok_or(BridgeLockActionError::missing_amount())?; @@ -1176,8 +1183,10 @@ impl BridgeLockActionError { } #[must_use] - fn invalid_address(err: IncorrectAddressLength) -> Self { - Self(BridgeLockActionErrorKind::InvalidAddress(err)) + fn address(source: AddressError) -> Self { + Self(BridgeLockActionErrorKind::Address { + source, + }) } #[must_use] @@ -1201,7 +1210,7 @@ enum BridgeLockActionErrorKind { #[error("the expected field in the raw source type was not set: `{0}`")] FieldNotSet(&'static str), #[error("the `to` field was invalid")] - InvalidAddress(#[source] IncorrectAddressLength), + Address { source: AddressError }, #[error("the `amount` field was not set")] MissingAmount, #[error("the `asset_id` field was invalid")] @@ -1254,7 +1263,7 @@ impl BridgeUnlockAction { let Some(to) = proto.to else { return Err(BridgeUnlockActionError::field_not_set("to")); }; - let to = Address::try_from_raw(&to).map_err(BridgeUnlockActionError::invalid_address)?; + let to = Address::try_from_raw(&to).map_err(BridgeUnlockActionError::address)?; let amount = proto .amount .ok_or(BridgeUnlockActionError::missing_amount())?; @@ -1280,8 +1289,10 @@ impl BridgeUnlockActionError { } #[must_use] - fn invalid_address(err: IncorrectAddressLength) -> Self { - Self(BridgeUnlockActionErrorKind::InvalidAddress(err)) + fn address(source: AddressError) -> Self { + Self(BridgeUnlockActionErrorKind::Address { + source, + }) } #[must_use] @@ -1300,7 +1311,7 @@ enum BridgeUnlockActionErrorKind { #[error("the expected field in the raw source type was not set: `{0}`")] FieldNotSet(&'static str), #[error("the `to` field was invalid")] - InvalidAddress(#[source] IncorrectAddressLength), + Address { source: AddressError }, #[error("the `amount` field was not set")] MissingAmount, #[error("the `fee_asset_id` field was invalid")] diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs index 25a0d5dcb4..9e40c91e99 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs @@ -4,11 +4,14 @@ use prost::{ }; use super::raw; -use crate::crypto::{ - self, - Signature, - SigningKey, - VerificationKey, +use crate::{ + crypto::{ + self, + Signature, + SigningKey, + VerificationKey, + }, + primitive::v1::Address, }; pub mod action; @@ -76,6 +79,13 @@ pub struct SignedTransaction { } impl SignedTransaction { + pub fn address(&self) -> Address { + crate::primitive::v1::Address::builder() + .array__(self.verification_key.address_bytes()) + .hrp__(self.transaction.hrp()) + .build() + } + /// Returns the transaction hash. /// /// The transaction hash is calculated by protobuf-encoding the transaction @@ -203,9 +213,13 @@ impl SignedTransaction { &self.transaction } + pub fn chain_id(&self) -> &str { + self.transaction.chain_id() + } + #[must_use] pub fn nonce(&self) -> u32 { - self.transaction.params.nonce + self.transaction.nonce() } } @@ -217,6 +231,20 @@ pub struct UnsignedTransaction { } impl UnsignedTransaction { + fn hrp(&self) -> bech32::Hrp { + self.params.hrp + } + + #[must_use] + pub fn nonce(&self) -> u32 { + self.params.nonce + } + + #[must_use] + pub fn chain_id(&self) -> &str { + &self.params.chain_id + } + #[must_use] pub fn into_signed(self, signing_key: &SigningKey) -> SignedTransaction { let bytes = self.to_raw().encode_to_vec(); @@ -283,7 +311,8 @@ impl UnsignedTransaction { let Some(params) = params else { return Err(UnsignedTransactionError::unset_params()); }; - let params = TransactionParams::from_raw(params); + let params = TransactionParams::try_from_raw(params) + .map_err(UnsignedTransactionError::transaction_params)?; let actions: Vec<_> = actions .into_iter() .map(Action::try_from_raw) @@ -335,6 +364,12 @@ impl UnsignedTransactionError { fn decode_any(inner: prost::DecodeError) -> Self { Self(UnsignedTransactionErrorKind::DecodeAny(inner)) } + + fn transaction_params(source: TransactionParamsError) -> Self { + Self(UnsignedTransactionErrorKind::TransactionParams { + source, + }) + } } #[derive(Debug, thiserror::Error)] @@ -354,21 +389,106 @@ enum UnsignedTransactionErrorKind { raw::UnsignedTransaction::type_url() )] DecodeAny(#[source] prost::DecodeError), + #[error("`params` field was invalid")] + TransactionParams { source: TransactionParamsError }, +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct TransactionParamsError(TransactionParamsErrorKind); + +impl TransactionParamsError { + fn chain_id_not_bech32_compatible(source: bech32::primitives::hrp::Error) -> Self { + Self(TransactionParamsErrorKind::ChainIdNotBech32Compatible { + source, + }) + } +} + +#[derive(Debug, thiserror::Error)] +enum TransactionParamsErrorKind { + #[error("the name extracted from the chain ID cannot be used as an address prefix")] + ChainIdNotBech32Compatible { + source: bech32::primitives::hrp::Error, + }, +} + +pub struct TransactionParamsBuilder> { + nonce: u32, + chain_id: TChainId, +} + +impl TransactionParamsBuilder { + fn new() -> Self { + Self { + nonce: 0, + chain_id: "".into(), + } + } +} + +impl TransactionParamsBuilder { + #[must_use = "the transaction params builder must be built to be useful"] + pub fn chain_id<'a, T: Into>>( + self, + chain_id: T, + ) -> TransactionParamsBuilder> { + TransactionParamsBuilder { + chain_id: chain_id.into(), + nonce: self.nonce, + } + } + + #[must_use = "the transaction params builder must be built to be useful"] + pub fn nonce(self, nonce: u32) -> Self { + Self { + nonce, + ..self + } + } +} + +impl<'a> TransactionParamsBuilder> { + /// Constructs a [`TransactionParams`] from the configured builder. + /// + /// # Errors + /// Returns an error if the set chain ID does not contain a chain name that can be turned into + /// a bech32 human readable prefix (everything before the first dash i.e. `-`). + pub fn try_build(self) -> Result { + let Self { + nonce, + chain_id, + } = self; + let chain_id = chain_id.as_ref().trim().to_string(); + let hrp = bech32::Hrp::parse(chain_id.split_once('-').map_or(&chain_id, |tup| tup.0)) + .map_err(TransactionParamsError::chain_id_not_bech32_compatible)?; + Ok(TransactionParams { + nonce, + chain_id, + hrp, + }) + } } #[derive(Clone, Debug)] -#[allow(clippy::module_name_repetitions)] pub struct TransactionParams { - pub nonce: u32, - pub chain_id: String, + nonce: u32, + chain_id: String, + hrp: bech32::Hrp, } impl TransactionParams { + #[must_use = "the transaction params builder must be built to be useful"] + pub fn builder() -> TransactionParamsBuilder { + TransactionParamsBuilder::new() + } + #[must_use] pub fn into_raw(self) -> raw::TransactionParams { let Self { nonce, chain_id, + .. } = self; raw::TransactionParams { nonce, @@ -377,16 +497,15 @@ impl TransactionParams { } /// Convert from a raw protobuf [`raw::UnsignedTransaction`]. - #[must_use] - pub fn from_raw(proto: raw::TransactionParams) -> Self { + /// + /// # Errors + /// See [`TransactionParamsBuilder::try_build`] for errors returned by this method. + pub fn try_from_raw(proto: raw::TransactionParams) -> Result { let raw::TransactionParams { nonce, chain_id, } = proto; - Self { - nonce, - chain_id, - } + Self::builder().nonce(nonce).chain_id(chain_id).try_build() } } @@ -397,6 +516,7 @@ mod test { primitive::v1::{ asset::default_native_asset_id, Address, + ASTRIA_ADDRESS_PREFIX, }, protocol::transaction::v1alpha1::action::TransferAction, }; @@ -416,16 +536,21 @@ mod test { ]); let transfer = TransferAction { - to: Address::from([0; 20]), + to: Address::builder() + .array([0; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 0, asset_id: default_native_asset_id(), fee_asset_id: default_native_asset_id(), }; - let params = TransactionParams { + let params = TransactionParams::try_from_raw(raw::TransactionParams { nonce: 1, chain_id: "test-1".to_string(), - }; + }) + .unwrap(); let unsigned = UnsignedTransaction { actions: vec![transfer.into()], params, @@ -449,16 +574,21 @@ mod test { ]); let transfer = TransferAction { - to: Address::from([0; 20]), + to: Address::builder() + .array([0; 20]) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap(), amount: 0, asset_id: default_native_asset_id(), fee_asset_id: default_native_asset_id(), }; - let params = TransactionParams { + let params = TransactionParams::try_from_raw(raw::TransactionParams { nonce: 1, chain_id: "test-1".to_string(), - }; + }) + .unwrap(); let unsigned = UnsignedTransaction { actions: vec![transfer.into()], params, diff --git a/crates/astria-core/src/protocol/transaction/v1alpha1/snapshots/astria_core__protocol__transaction__v1alpha1__test__signed_transaction_hash.snap b/crates/astria-core/src/protocol/transaction/v1alpha1/snapshots/astria_core__protocol__transaction__v1alpha1__test__signed_transaction_hash.snap index 2856a679ec..5a65b1ec82 100644 --- a/crates/astria-core/src/protocol/transaction/v1alpha1/snapshots/astria_core__protocol__transaction__v1alpha1__test__signed_transaction_hash.snap +++ b/crates/astria-core/src/protocol/transaction/v1alpha1/snapshots/astria_core__protocol__transaction__v1alpha1__test__signed_transaction_hash.snap @@ -3,36 +3,36 @@ source: crates/astria-core/src/protocol/transaction/v1alpha1/mod.rs expression: tx.sha256_of_proto_encoding() --- [ - 108, - 44, - 224, - 168, - 98, - 240, - 215, - 197, - 83, - 115, - 206, - 104, - 180, - 159, - 248, - 74, - 224, - 209, - 176, - 215, - 243, - 132, - 197, + 34, + 82, + 88, + 30, 41, - 11, - 211, - 188, - 188, + 64, + 178, 49, - 43, - 168, - 198 + 45, + 195, + 239, + 10, + 174, + 39, + 237, + 81, + 217, + 210, + 116, + 141, + 114, + 137, + 209, + 236, + 54, + 17, + 110, + 147, + 150, + 117, + 135, + 115 ] diff --git a/crates/astria-core/src/sequencerblock/v1alpha1/block.rs b/crates/astria-core/src/sequencerblock/v1alpha1/block.rs index 90ef2f9bfc..27c9503034 100644 --- a/crates/astria-core/src/sequencerblock/v1alpha1/block.rs +++ b/crates/astria-core/src/sequencerblock/v1alpha1/block.rs @@ -22,7 +22,7 @@ use crate::{ asset, derive_merkle_tree_from_rollup_txs, Address, - IncorrectAddressLength, + AddressError, IncorrectRollupIdLength, RollupId, }, @@ -1355,8 +1355,8 @@ impl Deposit { let Some(bridge_address) = bridge_address else { return Err(DepositError::field_not_set("bridge_address")); }; - let bridge_address = Address::try_from_raw(&bridge_address) - .map_err(DepositError::incorrect_address_length)?; + let bridge_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")); @@ -1380,8 +1380,10 @@ impl Deposit { pub struct DepositError(DepositErrorKind); impl DepositError { - fn incorrect_address_length(source: IncorrectAddressLength) -> Self { - Self(DepositErrorKind::IncorrectAddressLength(source)) + fn address(source: AddressError) -> Self { + Self(DepositErrorKind::Address { + source, + }) } fn field_not_set(field: &'static str) -> Self { @@ -1399,8 +1401,8 @@ impl DepositError { #[derive(Debug, thiserror::Error)] enum DepositErrorKind { - #[error("the address length is not 20 bytes")] - IncorrectAddressLength(#[source] IncorrectAddressLength), + #[error("the address is invalid")] + Address { source: AddressError }, #[error("the expected field in the raw source type was not set: `{0}`")] FieldNotSet(&'static str), #[error("the rollup ID length is not 32 bytes")] diff --git a/crates/astria-sequencer-client/src/extension_trait.rs b/crates/astria-sequencer-client/src/extension_trait.rs index 51b98ee9d5..5c8966c72e 100644 --- a/crates/astria-sequencer-client/src/extension_trait.rs +++ b/crates/astria-sequencer-client/src/extension_trait.rs @@ -8,11 +8,21 @@ //! The example below works with the feature `"http"` set. //! ```no_run //! # tokio_test::block_on(async { +//! use astria_core::primitive::v1::{ +//! Address, +//! ASTRIA_ADDRESS_PREFIX, +//! }; //! use astria_sequencer_client::SequencerClientExt as _; //! use tendermint_rpc::HttpClient; //! //! let client = HttpClient::new("http://127.0.0.1:26657")?; -//! let address: [u8; 20] = hex_literal::hex!("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"); +//! let address = Address::builder() +//! .array(hex_literal::hex!( +//! "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" +//! )) +//! .prefix(ASTRIA_ADDRESS_PREFIX) +//! .try_build() +//! .unwrap(); //! let height = 5u32; //! let balance = client.get_balance(address, height).await?; //! println!("{balance:?}"); @@ -417,18 +427,17 @@ pub trait SequencerClientExt: Client { /// - If calling tendermint `abci_query` RPC fails. /// - If the bytes contained in the abci query response cannot be read as an /// `astria.sequencer.v1.BalanceResponse`. - async fn get_balance( + async fn get_balance( &self, - address: AddressT, + address: Address, height: HeightT, ) -> Result where - AddressT: Into
+ Send, HeightT: Into + Send, { const PREFIX: &[u8] = b"accounts/balance/"; - let path = make_path_from_prefix_and_address(PREFIX, address.into().get()); + let path = make_path_from_prefix_and_address(PREFIX, address.bytes()); let response = self .abci_query(Some(path), vec![], Some(height.into()), false) @@ -454,10 +463,7 @@ pub trait SequencerClientExt: Client { /// # Errors /// /// This has the same error conditions as [`SequencerClientExt::get_balance`]. - async fn get_latest_balance + Send>( - &self, - address: A, - ) -> Result { + async fn get_latest_balance(&self, address: Address) -> Result { // This makes use of the fact that a height `None` and `Some(0)` are // treated the same. self.get_balance(address, 0u32).await @@ -503,18 +509,17 @@ pub trait SequencerClientExt: Client { /// - If calling tendermint `abci_query` RPC fails. /// - If the bytes contained in the abci query response cannot be read as an /// `astria.sequencer.v1.NonceResponse`. - async fn get_nonce( + async fn get_nonce( &self, - address: AddressT, + address: Address, height: HeightT, ) -> Result where - AddressT: Into
+ Send, HeightT: Into + Send, { const PREFIX: &[u8] = b"accounts/nonce/"; - let path = make_path_from_prefix_and_address(PREFIX, address.into().get()); + let path = make_path_from_prefix_and_address(PREFIX, address.bytes()); let response = self .abci_query(Some(path), vec![], Some(height.into()), false) @@ -536,22 +541,19 @@ pub trait SequencerClientExt: Client { /// # Errors /// /// This has the same error conditions as [`SequencerClientExt::get_nonce`]. - async fn get_latest_nonce + Send>( - &self, - address: A, - ) -> Result { + async fn get_latest_nonce(&self, address: Address) -> Result { // This makes use of the fact that a height `None` and `Some(0)` are // treated the same. self.get_nonce(address, 0u32).await } - async fn get_bridge_account_last_transaction_hash + Send>( + async fn get_bridge_account_last_transaction_hash( &self, - address: A, + address: Address, ) -> Result { const PREFIX: &[u8] = b"bridge/account_last_tx_hash/"; - let path = make_path_from_prefix_and_address(PREFIX, address.into().get()); + let path = make_path_from_prefix_and_address(PREFIX, address.bytes()); let response = self .abci_query(Some(path), vec![], None, false) diff --git a/crates/astria-sequencer-client/src/tests/http.rs b/crates/astria-sequencer-client/src/tests/http.rs index 944bbcad3e..f4bbca6ebd 100644 --- a/crates/astria-sequencer-client/src/tests/http.rs +++ b/crates/astria-sequencer-client/src/tests/http.rs @@ -7,6 +7,7 @@ use astria_core::{ default_native_asset_id, }, Address, + ASTRIA_ADDRESS_PREFIX, }, protocol::transaction::v1alpha1::{ action::TransferAction, @@ -43,8 +44,23 @@ use crate::{ SequencerClientExt as _, }; -const ALICE_ADDRESS: [u8; 20] = hex!("1c0c490f1b5528d8173c5de46d131160e4b2c0c3"); -const BOB_ADDRESS: Address = Address::from_array(hex!("34fec43c7fcab9aef3b3cf8aba855e41ee69ca3a")); +const ALICE_ADDRESS_BYTES: [u8; 20] = hex!("1c0c490f1b5528d8173c5de46d131160e4b2c0c3"); +const BOB_ADDRESS_BYTES: [u8; 20] = hex!("34fec43c7fcab9aef3b3cf8aba855e41ee69ca3a"); + +fn alice_address() -> Address { + Address::builder() + .array(ALICE_ADDRESS_BYTES) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap() +} +fn bob_address() -> Address { + Address::builder() + .array(BOB_ADDRESS_BYTES) + .prefix(ASTRIA_ADDRESS_PREFIX) + .try_build() + .unwrap() +} struct MockSequencer { server: MockServer, @@ -134,7 +150,7 @@ fn create_signed_transaction() -> SignedTransaction { let actions = vec![ TransferAction { - to: BOB_ADDRESS, + to: bob_address(), amount: 333_333, asset_id: default_native_asset_id(), fee_asset_id: default_native_asset_id(), @@ -142,10 +158,11 @@ fn create_signed_transaction() -> SignedTransaction { .into(), ]; UnsignedTransaction { - params: TransactionParams { - nonce: 1, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(1) + .chain_id("test") + .try_build() + .unwrap(), actions, } .into_signed(&alice_key) @@ -167,7 +184,7 @@ async fn get_latest_nonce() { register_abci_query_response(&server, "accounts/nonce/", expected_response.clone()).await; let actual_response = client - .get_latest_nonce(ALICE_ADDRESS) + .get_latest_nonce(alice_address()) .await .unwrap() .into_raw(); @@ -197,7 +214,7 @@ async fn get_latest_balance() { register_abci_query_response(&server, "accounts/balance/", expected_response.clone()).await; let actual_response = client - .get_latest_balance(ALICE_ADDRESS) + .get_latest_balance(alice_address()) .await .unwrap() .into_raw(); @@ -256,7 +273,7 @@ async fn get_bridge_account_last_transaction_hash() { .await; let actual_response = client - .get_bridge_account_last_transaction_hash(ALICE_ADDRESS) + .get_bridge_account_last_transaction_hash(alice_address()) .await .unwrap() .into_raw(); diff --git a/crates/astria-sequencer/src/accounts/query.rs b/crates/astria-sequencer/src/accounts/query.rs index 2ac08081b1..c3cfb259c4 100644 --- a/crates/astria-sequencer/src/accounts/query.rs +++ b/crates/astria-sequencer/src/accounts/query.rs @@ -143,7 +143,7 @@ async fn preprocess_request( let address = hex::decode(address) .context("failed decoding hex encoded bytes") .and_then(|addr| { - Address::try_from_slice(&addr).context("failed constructing address from bytes") + crate::try_astria_address(&addr).context("failed constructing address from bytes") }) .map_err(|err| response::Query { code: AbciErrorCode::INVALID_PARAMETER.into(), diff --git a/crates/astria-sequencer/src/accounts/state_ext.rs b/crates/astria-sequencer/src/accounts/state_ext.rs index b1053ea602..138ba985a1 100644 --- a/crates/astria-sequencer/src/accounts/state_ext.rs +++ b/crates/astria-sequencer/src/accounts/state_ext.rs @@ -232,13 +232,10 @@ impl StateWriteExt for T {} #[cfg(test)] mod test { use astria_core::{ - primitive::v1::{ - asset::{ - Denom, - Id, - DEFAULT_NATIVE_ASSET_DENOM, - }, - Address, + primitive::v1::asset::{ + Denom, + Id, + DEFAULT_NATIVE_ASSET_DENOM, }, protocol::account::v1alpha1::AssetBalance, }; @@ -257,7 +254,7 @@ mod test { let state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let nonce_expected = 0u32; // uninitialized accounts return zero @@ -278,7 +275,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let nonce_expected = 0u32; // can write new @@ -316,7 +313,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let nonce_expected = 2u32; // can write new @@ -333,7 +330,7 @@ mod test { ); // writing additional account preserves first account's values - let address_1 = Address::try_from_slice(&[41u8; 20]).unwrap(); + let address_1 = crate::astria_address([41u8; 20]); let nonce_expected_1 = 3u32; state @@ -364,7 +361,7 @@ mod test { let state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let amount_expected = 0u128; @@ -386,7 +383,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let mut amount_expected = 1u128; @@ -428,7 +425,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let amount_expected = 1u128; @@ -448,7 +445,7 @@ mod test { // writing to other accounts does not affect original account // create needed variables - let address_1 = Address::try_from_slice(&[41u8; 20]).unwrap(); + let address_1 = crate::astria_address([41u8; 20]); let amount_expected_1 = 2u128; state @@ -481,7 +478,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset_0 = Id::from_denom("asset_0"); let asset_1 = Id::from_denom("asset_1"); let amount_expected_0 = 1u128; @@ -520,7 +517,7 @@ mod test { let state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); // see that call was ok let balances = state @@ -564,7 +561,7 @@ mod test { .expect("should be able to call other trait method on state object"); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let amount_expected_0 = 1u128; let amount_expected_1 = 2u128; let amount_expected_2 = 3u128; @@ -611,7 +608,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let amount_increase = 2u128; @@ -652,7 +649,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let amount_increase = 2u128; @@ -694,7 +691,7 @@ mod test { let mut state = StateDelta::new(snapshot); // create needed variables - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); let asset = Id::from_denom("asset_0"); let amount_increase = 2u128; diff --git a/crates/astria-sequencer/src/api_state_ext.rs b/crates/astria-sequencer/src/api_state_ext.rs index c7cc067f2a..57880c2624 100644 --- a/crates/astria-sequencer/src/api_state_ext.rs +++ b/crates/astria-sequencer/src/api_state_ext.rs @@ -383,10 +383,7 @@ impl StateWriteExt for T {} #[cfg(test)] mod test { use astria_core::{ - primitive::v1::{ - asset::Id, - Address, - }, + primitive::v1::asset::Id, protocol::test_utils::ConfigureSequencerBlock, sequencerblock::v1alpha1::block::Deposit, }; @@ -404,7 +401,7 @@ mod test { let mut deposits = vec![]; for _ in 0..2 { let rollup_id = RollupId::new(rng.gen()); - let bridge_address = Address::try_from_slice(&[rng.gen(); 20]).unwrap(); + let bridge_address = crate::astria_address([rng.gen(); 20]); let amount = rng.gen::(); let asset_id = Id::from_denom(&rng.gen::().to_string()); let destination_chain_address = rng.gen::().to_string(); diff --git a/crates/astria-sequencer/src/app/mod.rs b/crates/astria-sequencer/src/app/mod.rs index bff3d5764c..996a786ea4 100644 --- a/crates/astria-sequencer/src/app/mod.rs +++ b/crates/astria-sequencer/src/app/mod.rs @@ -961,7 +961,7 @@ impl App { /// Executes a signed transaction. #[instrument(name = "App::execute_transaction", skip_all, fields( signed_transaction_hash = %telemetry::display::base64(&signed_tx.sha256_of_proto_encoding()), - sender = %signed_tx.verification_key().address(), + sender = %signed_tx.address(), ))] pub(crate) async fn execute_transaction( &mut self, diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap index e06db4b717..22384b1ee9 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap @@ -1,39 +1,38 @@ --- source: crates/astria-sequencer/src/app/tests_breaking_changes.rs -assertion_line: 274 expression: app.app_hash.as_bytes() --- [ - 30, - 232, - 210, - 6, - 194, - 12, - 154, - 179, - 145, - 105, - 40, - 101, - 30, - 224, - 10, - 195, - 119, - 124, - 207, - 182, - 132, - 175, - 9, - 191, - 104, - 213, + 98, + 92, + 234, + 52, + 36, + 227, + 123, + 177, + 50, + 149, + 185, + 20, + 226, + 143, + 234, + 255, + 184, + 222, + 123, + 23, + 23, + 173, + 178, 200, - 146, - 180, - 192, - 229, - 97 + 225, + 125, + 222, + 140, + 120, + 187, + 188, + 65 ] diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap index 9c81d4d7ae..69b08aa1d1 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_finalize_block_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 171, - 98, - 3, - 172, - 104, - 125, - 77, - 86, - 230, - 236, - 153, - 226, - 227, - 158, - 59, - 19, - 108, - 136, + 219, + 243, + 219, + 31, + 187, + 190, + 97, + 198, + 149, 6, - 56, - 175, - 113, - 196, + 242, + 31, 108, - 171, - 66, - 211, - 241, - 217, - 82, - 191, - 152 + 0, + 130, + 193, + 167, + 251, + 111, + 245, + 62, + 243, + 252, + 170, + 209, + 246, + 243, + 161, + 231, + 196, + 145, + 64 ] diff --git a/crates/astria-sequencer/src/app/test_utils.rs b/crates/astria-sequencer/src/app/test_utils.rs index 09c42c2f01..72e27e0a85 100644 --- a/crates/astria-sequencer/src/app/test_utils.rs +++ b/crates/astria-sequencer/src/app/test_utils.rs @@ -30,7 +30,7 @@ use crate::{ pub(crate) fn address_from_hex_string(s: &str) -> Address { let bytes = hex::decode(s).unwrap(); let arr: [u8; ADDRESS_LEN] = bytes.try_into().unwrap(); - Address::from_array(arr) + crate::astria_address(arr) } pub(crate) const ALICE_ADDRESS: &str = "1c0c490f1b5528d8173c5de46d131160e4b2c0c3"; @@ -47,7 +47,7 @@ pub(crate) fn get_alice_signing_key_and_address() -> (SigningKey, Address) { .try_into() .unwrap(); let alice_signing_key = SigningKey::from(alice_secret_bytes); - let alice = *alice_signing_key.verification_key().address(); + let alice = crate::astria_address(alice_signing_key.verification_key().address_bytes()); (alice_signing_key, alice) } @@ -58,7 +58,7 @@ pub(crate) fn get_bridge_signing_key_and_address() -> (SigningKey, Address) { .try_into() .unwrap(); let bridge_signing_key = SigningKey::from(bridge_secret_bytes); - let bridge = *bridge_signing_key.verification_key().address(); + let bridge = crate::astria_address(bridge_signing_key.verification_key().address_bytes()); (bridge_signing_key, bridge) } @@ -136,10 +136,11 @@ pub(crate) async fn initialize_app( pub(crate) fn get_mock_tx(nonce: u32) -> SignedTransaction { let (alice_signing_key, _) = get_alice_signing_key_and_address(); let tx = UnsignedTransaction { - params: TransactionParams { - nonce, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(nonce) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes([0; 32]), diff --git a/crates/astria-sequencer/src/app/tests_app.rs b/crates/astria-sequencer/src/app/tests_app.rs index 9e718df325..6317c3232f 100644 --- a/crates/astria-sequencer/src/app/tests_app.rs +++ b/crates/astria-sequencer/src/app/tests_app.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use astria_core::{ primitive::v1::{ asset::DEFAULT_NATIVE_ASSET_DENOM, - Address, RollupId, }, protocol::transaction::v1alpha1::{ @@ -237,10 +236,11 @@ async fn app_transfer_block_fees_to_sudo() { let bob_address = address_from_hex_string(BOB_ADDRESS); let amount = 333_333; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: bob_address, @@ -300,7 +300,7 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { let (alice_signing_key, _) = get_alice_signing_key_and_address(); let (mut app, storage) = initialize_app_with_storage(None, vec![]).await; - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let asset_id = get_native_asset().id(); @@ -327,10 +327,11 @@ async fn app_create_sequencer_block_with_sequenced_data_and_deposits() { fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![lock_action.into(), sequence_action.into()], }; @@ -390,7 +391,7 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { let (alice_signing_key, _) = get_alice_signing_key_and_address(); let (mut app, storage) = initialize_app_with_storage(None, vec![]).await; - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let asset_id = get_native_asset().id(); @@ -417,10 +418,11 @@ async fn app_execution_results_match_proposal_vs_after_proposal() { fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![lock_action.into(), sequence_action.into()], }; @@ -541,10 +543,11 @@ async fn app_prepare_proposal_cometbft_max_bytes_overflow_ok() { // create txs which will cause cometBFT overflow let (alice_signing_key, _) = get_alice_signing_key_and_address(); let tx_pass = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from([1u8; 32]), @@ -556,10 +559,11 @@ async fn app_prepare_proposal_cometbft_max_bytes_overflow_ok() { } .into_signed(&alice_signing_key); let tx_overflow = UnsignedTransaction { - params: TransactionParams { - nonce: 1, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(1) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from([1u8; 32]), @@ -614,10 +618,11 @@ async fn app_prepare_proposal_sequencer_max_bytes_overflow_ok() { // create txs which will cause sequencer overflow (max is currently 256_000 bytes) let (alice_signing_key, _) = get_alice_signing_key_and_address(); let tx_pass = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from([1u8; 32]), @@ -629,10 +634,11 @@ async fn app_prepare_proposal_sequencer_max_bytes_overflow_ok() { } .into_signed(&alice_signing_key); let tx_overflow = UnsignedTransaction { - params: TransactionParams { - nonce: 1, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(1) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from([1u8; 32]), @@ -698,7 +704,7 @@ async fn app_end_block_validator_updates() { ]; let mut app = initialize_app(None, initial_validator_set).await; - let proposer_address = Address::try_from_slice([0u8; 20].as_ref()).unwrap(); + let proposer_address = crate::astria_address([0u8; 20]); let validator_updates = vec![ validator::Update { diff --git a/crates/astria-sequencer/src/app/tests_breaking_changes.rs b/crates/astria-sequencer/src/app/tests_breaking_changes.rs index cccf427a2b..b48110445a 100644 --- a/crates/astria-sequencer/src/app/tests_breaking_changes.rs +++ b/crates/astria-sequencer/src/app/tests_breaking_changes.rs @@ -17,7 +17,6 @@ use std::{ use astria_core::{ primitive::v1::{ asset::DEFAULT_NATIVE_ASSET_DENOM, - Address, RollupId, }, protocol::transaction::v1alpha1::{ @@ -74,7 +73,7 @@ async fn app_finalize_block_snapshot() { let (alice_signing_key, _) = get_alice_signing_key_and_address(); let (mut app, storage) = initialize_app_with_storage(None, vec![]).await; - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let asset_id = get_native_asset().id(); @@ -104,10 +103,11 @@ async fn app_finalize_block_snapshot() { fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![lock_action.into(), sequence_action.into()], }; @@ -197,10 +197,11 @@ async fn app_execute_transaction_with_every_action_snapshot() { app.apply(state_tx); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: bob_address, @@ -249,10 +250,11 @@ async fn app_execute_transaction_with_every_action_snapshot() { // execute BridgeUnlock action let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ BridgeUnlockAction { to: bob_address, diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 7ea28736c9..716ad2a663 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -5,7 +5,6 @@ use astria_core::{ primitive::v1::{ asset, asset::DEFAULT_NATIVE_ASSET_DENOM, - Address, RollupId, }, protocol::transaction::v1alpha1::{ @@ -54,10 +53,11 @@ async fn app_execute_transaction_transfer() { let bob_address = address_from_hex_string(BOB_ADDRESS); let value = 333_333; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: bob_address, @@ -111,10 +111,11 @@ async fn app_execute_transaction_transfer_not_native_token() { // transfer funds from Alice to Bob; use native token for fee payment let bob_address = address_from_hex_string(BOB_ADDRESS); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: bob_address, @@ -177,10 +178,11 @@ async fn app_execute_transaction_transfer_balance_too_low_for_fee() { // 0-value transfer; only fee is deducted from sender let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: bob, @@ -217,10 +219,11 @@ async fn app_execute_transaction_sequence() { let fee = calculate_fee_from_state(&data, &app.state).await.unwrap(); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -254,10 +257,11 @@ async fn app_execute_transaction_invalid_fee_asset() { let fee_asset_id = asset::Id::from_denom("test"); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -295,10 +299,11 @@ async fn app_execute_transaction_validator_update() { }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::ValidatorUpdate(update.clone())], }; @@ -328,10 +333,11 @@ async fn app_execute_transaction_ibc_relayer_change_addition() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![IbcRelayerChangeAction::Addition(alice_address).into()], }; @@ -358,10 +364,11 @@ async fn app_execute_transaction_ibc_relayer_change_deletion() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![IbcRelayerChangeAction::Removal(alice_address).into()], }; @@ -378,7 +385,7 @@ async fn app_execute_transaction_ibc_relayer_change_invalid() { let genesis_state = GenesisState { accounts: default_genesis_accounts(), authority_sudo_address: alice_address, - ibc_sudo_address: Address::from([0; 20]), + ibc_sudo_address: crate::astria_address([0; 20]), ibc_relayer_addresses: vec![alice_address], native_asset_base_denomination: DEFAULT_NATIVE_ASSET_DENOM.to_string(), allowed_fee_assets: vec![DEFAULT_NATIVE_ASSET_DENOM.to_owned().into()], @@ -388,10 +395,11 @@ async fn app_execute_transaction_ibc_relayer_change_invalid() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![IbcRelayerChangeAction::Removal(alice_address).into()], }; @@ -418,10 +426,11 @@ async fn app_execute_transaction_sudo_address_change() { let new_address = address_from_hex_string(BOB_ADDRESS); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::SudoAddressChange(SudoAddressChangeAction { new_address, })], @@ -443,7 +452,7 @@ async fn app_execute_transaction_sudo_address_change_error() { let genesis_state = GenesisState { accounts: default_genesis_accounts(), authority_sudo_address: sudo_address, - ibc_sudo_address: [0u8; 20].into(), + ibc_sudo_address: crate::astria_address([0u8; 20]), ibc_relayer_addresses: vec![], native_asset_base_denomination: DEFAULT_NATIVE_ASSET_DENOM.to_string(), ibc_params: IBCParameters::default(), @@ -453,10 +462,11 @@ async fn app_execute_transaction_sudo_address_change_error() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::SudoAddressChange(SudoAddressChangeAction { new_address: alice_address, })], @@ -493,10 +503,11 @@ async fn app_execute_transaction_fee_asset_change_addition() { let new_asset = asset::Id::from_denom("test"); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::FeeAssetChange(FeeAssetChangeAction::Addition( new_asset, ))], @@ -532,10 +543,11 @@ async fn app_execute_transaction_fee_asset_change_removal() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::FeeAssetChange(FeeAssetChangeAction::Removal( test_asset.id(), ))], @@ -572,10 +584,11 @@ async fn app_execute_transaction_fee_asset_change_invalid() { let mut app = initialize_app(Some(genesis_state), vec![]).await; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![Action::FeeAssetChange(FeeAssetChangeAction::Removal( get_native_asset().id(), ))], @@ -610,10 +623,11 @@ async fn app_execute_transaction_init_bridge_account_ok() { fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -665,10 +679,11 @@ async fn app_execute_transaction_init_bridge_account_account_already_registered( fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -681,10 +696,11 @@ async fn app_execute_transaction_init_bridge_account_account_already_registered( fee_asset_id: asset_id, }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 1, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -697,7 +713,7 @@ async fn app_execute_transaction_bridge_lock_action_ok() { let (alice_signing_key, alice_address) = get_alice_signing_key_and_address(); let mut app = initialize_app(None, vec![]).await; - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let asset_id = get_native_asset().id(); @@ -717,10 +733,11 @@ async fn app_execute_transaction_bridge_lock_action_ok() { destination_chain_address: "nootwashere".to_string(), }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -783,7 +800,7 @@ async fn app_execute_transaction_bridge_lock_action_invalid_for_eoa() { let mut app = initialize_app(None, vec![]).await; // don't actually register this address as a bridge address - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let asset_id = get_native_asset().id(); let amount = 100; @@ -795,10 +812,11 @@ async fn app_execute_transaction_bridge_lock_action_invalid_for_eoa() { destination_chain_address: "nootwashere".to_string(), }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -815,10 +833,11 @@ async fn app_execute_transaction_invalid_nonce() { // create tx with invalid nonce 1 let data = b"hello world".to_vec(); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 1, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(1) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -861,10 +880,11 @@ async fn app_execute_transaction_invalid_chain_id() { // create tx with invalid nonce 1 let data = b"hello world".to_vec(); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "wrong-chain".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("wrong-chain") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -910,7 +930,7 @@ async fn app_stateful_check_fails_insufficient_total_balance() { // create a new key; will have 0 balance let keypair = SigningKey::new(OsRng); - let keypair_address = *keypair.verification_key().address(); + let keypair_address = crate::astria_address(keypair.verification_key().address_bytes()); // figure out needed fee for a single transfer let data = b"hello world".to_vec(); @@ -920,10 +940,11 @@ async fn app_stateful_check_fails_insufficient_total_balance() { // transfer just enough to cover single sequence fee with data let signed_tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ TransferAction { to: keypair_address, @@ -941,10 +962,11 @@ async fn app_stateful_check_fails_insufficient_total_balance() { // build double transfer exceeding balance let signed_tx_fail = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -972,10 +994,11 @@ async fn app_stateful_check_fails_insufficient_total_balance() { // build single transfer to see passes let signed_tx_pass = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -1027,10 +1050,11 @@ async fn app_execute_transaction_bridge_lock_unlock_action_ok() { destination_chain_address: "nootwashere".to_string(), }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; @@ -1048,10 +1072,11 @@ async fn app_execute_transaction_bridge_lock_unlock_action_ok() { }; let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![action.into()], }; diff --git a/crates/astria-sequencer/src/authority/action.rs b/crates/astria-sequencer/src/authority/action.rs index 6c5386e0b4..d70fa3ce76 100644 --- a/crates/astria-sequencer/src/authority/action.rs +++ b/crates/astria-sequencer/src/authority/action.rs @@ -191,7 +191,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!(state.get_transfer_base_fee().await.unwrap(), 10); @@ -205,7 +205,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!(state.get_sequence_action_base_fee().await.unwrap(), 3); @@ -219,7 +219,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!( @@ -239,7 +239,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!(state.get_init_bridge_account_base_fee().await.unwrap(), 2); @@ -253,7 +253,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!( @@ -272,7 +272,7 @@ mod test { }; fee_change - .execute(&mut state, Address::from([1; 20])) + .execute(&mut state, crate::astria_address([1; 20])) .await .unwrap(); assert_eq!(state.get_ics20_withdrawal_base_fee().await.unwrap(), 2); diff --git a/crates/astria-sequencer/src/authority/state_ext.rs b/crates/astria-sequencer/src/authority/state_ext.rs index c1da439891..35aceb3c5b 100644 --- a/crates/astria-sequencer/src/authority/state_ext.rs +++ b/crates/astria-sequencer/src/authority/state_ext.rs @@ -98,9 +98,9 @@ pub(crate) trait StateReadExt: StateRead { // return error because sudo key must be set bail!("sudo key not found"); }; - let SudoAddress(address) = + let SudoAddress(address_bytes) = SudoAddress::try_from_slice(&bytes).context("invalid sudo key bytes")?; - Ok(Address::from(address)) + Ok(crate::astria_address(address_bytes)) } #[instrument(skip(self))] @@ -144,7 +144,7 @@ pub(crate) trait StateWriteExt: StateWrite { fn put_sudo_address(&mut self, address: Address) -> Result<()> { self.put_raw( SUDO_STORAGE_KEY.to_string(), - borsh::to_vec(&SudoAddress(address.get())) + borsh::to_vec(&SudoAddress(address.bytes())) .context("failed to convert sudo address to vec")?, ); Ok(()) @@ -179,7 +179,6 @@ impl StateWriteExt for T {} #[cfg(test)] mod test { - use astria_core::primitive::v1::Address; use cnidarium::StateDelta; use tendermint::{ validator, @@ -206,7 +205,7 @@ mod test { .expect_err("no sudo address should exist at first"); // can write new - let mut address_expected = Address::try_from_slice(&[42u8; 20]).unwrap(); + let mut address_expected = crate::astria_address([42u8; 20]); state .put_sudo_address(address_expected) .expect("writing sudo address should not fail"); @@ -220,7 +219,7 @@ mod test { ); // can rewrite with new value - address_expected = Address::try_from_slice(&[41u8; 20]).unwrap(); + address_expected = crate::astria_address([41u8; 20]); state .put_sudo_address(address_expected) .expect("writing sudo address should not fail"); diff --git a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs index dd356054b7..62330d93cd 100644 --- a/crates/astria-sequencer/src/bridge/bridge_lock_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_lock_action.rs @@ -167,7 +167,7 @@ mod test { state.put_transfer_base_fee(transfer_fee).unwrap(); state.put_bridge_lock_byte_cost_multiplier(2); - let bridge_address = Address::from([1; 20]); + let bridge_address = crate::astria_address([1; 20]); let asset_id = asset::Id::from_denom("test"); let bridge_lock = BridgeLockAction { to: bridge_address, @@ -184,7 +184,7 @@ mod test { .unwrap(); state.put_allowed_fee_asset(asset_id); - let from_address = Address::from([2; 20]); + let from_address = crate::astria_address([2; 20]); // not enough balance; should fail state @@ -226,7 +226,7 @@ mod test { state.put_transfer_base_fee(transfer_fee).unwrap(); state.put_bridge_lock_byte_cost_multiplier(2); - let bridge_address = Address::from([1; 20]); + let bridge_address = crate::astria_address([1; 20]); let asset_id = asset::Id::from_denom("test"); let bridge_lock = BridgeLockAction { to: bridge_address, @@ -243,7 +243,7 @@ mod test { .unwrap(); state.put_allowed_fee_asset(asset_id); - let from_address = Address::from([2; 20]); + let from_address = crate::astria_address([2; 20]); // not enough balance; should fail state diff --git a/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs b/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs index ba6abcdfad..076d775629 100644 --- a/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs +++ b/crates/astria-sequencer/src/bridge/bridge_unlock_action.rs @@ -94,8 +94,8 @@ mod test { let asset_id = asset::Id::from_denom("test"); let transfer_amount = 100; - let address = Address::from([1; 20]); - let to_address = Address::from([2; 20]); + let address = crate::astria_address([1; 20]); + let to_address = crate::astria_address([2; 20]); let bridge_unlock = BridgeUnlockAction { to: to_address, @@ -126,8 +126,8 @@ mod test { let transfer_amount = 100; state.put_transfer_base_fee(transfer_fee).unwrap(); - let bridge_address = Address::from([1; 20]); - let to_address = Address::from([2; 20]); + let bridge_address = crate::astria_address([1; 20]); + let to_address = crate::astria_address([2; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"test_rollup_id"); state.put_bridge_account_rollup_id(&bridge_address, &rollup_id); @@ -177,8 +177,8 @@ mod test { let transfer_amount = 100; state.put_transfer_base_fee(transfer_fee).unwrap(); - let bridge_address = Address::from([1; 20]); - let to_address = Address::from([2; 20]); + let bridge_address = crate::astria_address([1; 20]); + let to_address = crate::astria_address([2; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"test_rollup_id"); state.put_bridge_account_rollup_id(&bridge_address, &rollup_id); diff --git a/crates/astria-sequencer/src/bridge/query.rs b/crates/astria-sequencer/src/bridge/query.rs index b14f76c5fb..850e2f23ab 100644 --- a/crates/astria-sequencer/src/bridge/query.rs +++ b/crates/astria-sequencer/src/bridge/query.rs @@ -96,7 +96,7 @@ fn preprocess_request(params: &[(String, String)]) -> anyhow::Result = hex::serde::deserialize(deserializer)?; - Address::try_from_slice(&bytes) - .map_err(|e| D::Error::custom(format!("failed constructing address from bytes: {e}"))) + crate::try_astria_address(&bytes) + .map_err(|e| D::Error::custom(format!("failed constructing address from bytes: {e:?}"))) } fn deserialize_addresses<'de, D>(deserializer: D) -> Result, D::Error> @@ -68,8 +68,8 @@ where let s = s.as_str().ok_or(D::Error::custom("expected string"))?; let bytes: Vec = hex::decode(s) .map_err(|e| D::Error::custom(format!("failed decoding hex string: {e}")))?; - Address::try_from_slice(&bytes).map_err(|e| { - D::Error::custom(format!("failed constructing address from bytes: {e}")) + crate::try_astria_address(&bytes).map_err(|e| { + D::Error::custom(format!("failed constructing address from bytes: {e:?}")) }) }) .collect() diff --git a/crates/astria-sequencer/src/ibc/ics20_transfer.rs b/crates/astria-sequencer/src/ibc/ics20_transfer.rs index a477a8e236..a96fc40bf7 100644 --- a/crates/astria-sequencer/src/ibc/ics20_transfer.rs +++ b/crates/astria-sequencer/src/ibc/ics20_transfer.rs @@ -440,7 +440,7 @@ async fn execute_ics20_transfer( packet_data.receiver }; - let recipient = Address::try_from_slice( + let recipient = crate::try_astria_address( &hex::decode(recipient).context("failed to decode recipient as hex string")?, ) .context("invalid recipient address")?; @@ -655,7 +655,7 @@ mod test { .await .expect("valid ics20 transfer to user account; recipient, memo, and asset ID are valid"); - let recipient = Address::try_from_slice( + let recipient = crate::try_astria_address( &hex::decode("1c0c490f1b5528d8173c5de46d131160e4b2c0c3").unwrap(), ) .unwrap(); @@ -676,7 +676,7 @@ mod test { let snapshot = storage.latest_snapshot(); let mut state_tx = StateDelta::new(snapshot.clone()); - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let denom: Denom = "dest_port/dest_channel/nootasset".to_string().into(); @@ -729,7 +729,7 @@ mod test { let snapshot = storage.latest_snapshot(); let mut state_tx = StateDelta::new(snapshot.clone()); - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let denom: Denom = "dest_port/dest_channel/nootasset".to_string().into(); @@ -767,7 +767,7 @@ mod test { let snapshot = storage.latest_snapshot(); let mut state_tx = StateDelta::new(snapshot.clone()); - let bridge_address = Address::from([99; 20]); + let bridge_address = crate::astria_address([99; 20]); let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); let denom: Denom = "dest_port/dest_channel/nootasset".to_string().into(); @@ -837,7 +837,7 @@ mod test { .await .expect("valid ics20 transfer to user account; recipient, memo, and asset ID are valid"); - let recipient = Address::try_from_slice(&hex::decode(address_string).unwrap()).unwrap(); + let recipient = crate::try_astria_address(&hex::decode(address_string).unwrap()).unwrap(); let balance = state_tx .get_account_balance(recipient, base_denom.id()) .await @@ -891,7 +891,7 @@ mod test { .await .expect("valid ics20 refund to user account; recipient, memo, and asset ID are valid"); - let recipient = Address::try_from_slice(&hex::decode(address_string).unwrap()).unwrap(); + let recipient = crate::try_astria_address(&hex::decode(address_string).unwrap()).unwrap(); let balance = state_tx .get_account_balance(recipient, base_denom.id()) .await diff --git a/crates/astria-sequencer/src/ibc/state_ext.rs b/crates/astria-sequencer/src/ibc/state_ext.rs index 5e02e0193c..9a417d5055 100644 --- a/crates/astria-sequencer/src/ibc/state_ext.rs +++ b/crates/astria-sequencer/src/ibc/state_ext.rs @@ -76,9 +76,9 @@ pub(crate) trait StateReadExt: StateRead { // ibc sudo key must be set bail!("ibc sudo key not found"); }; - let SudoAddress(address) = + let SudoAddress(address_bytes) = SudoAddress::try_from_slice(&bytes).context("invalid ibc sudo key bytes")?; - Ok(Address::from(address)) + Ok(crate::astria_address(address_bytes)) } #[instrument(skip(self))] @@ -124,7 +124,7 @@ pub(crate) trait StateWriteExt: StateWrite { fn put_ibc_sudo_address(&mut self, address: Address) -> Result<()> { self.put_raw( IBC_SUDO_STORAGE_KEY.to_string(), - borsh::to_vec(&SudoAddress(address.get())) + borsh::to_vec(&SudoAddress(address.bytes())) .context("failed to convert sudo address to vec")?, ); Ok(()) @@ -154,10 +154,7 @@ impl StateWriteExt for T {} #[cfg(test)] mod test { - use astria_core::primitive::v1::{ - asset::Id, - Address, - }; + use astria_core::primitive::v1::asset::Id; use cnidarium::StateDelta; use ibc_types::core::channel::ChannelId; @@ -186,7 +183,7 @@ mod test { let mut state = StateDelta::new(snapshot); // can write new - let mut address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let mut address = crate::astria_address([42u8; 20]); state .put_ibc_sudo_address(address) .expect("writing sudo address should not fail"); @@ -200,7 +197,7 @@ mod test { ); // can rewrite with new value - address = Address::try_from_slice(&[41u8; 20]).unwrap(); + address = crate::astria_address([41u8; 20]); state .put_ibc_sudo_address(address) .expect("writing sudo address should not fail"); @@ -221,7 +218,7 @@ mod test { let state = StateDelta::new(snapshot); // unset address returns false - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); assert!( !state .is_ibc_relayer(&address) @@ -238,7 +235,7 @@ mod test { let mut state = StateDelta::new(snapshot); // can write - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); state.put_ibc_relayer_address(&address); assert!( state @@ -266,7 +263,7 @@ mod test { let mut state = StateDelta::new(snapshot); // can write - let address = Address::try_from_slice(&[42u8; 20]).unwrap(); + let address = crate::astria_address([42u8; 20]); state.put_ibc_relayer_address(&address); assert!( state @@ -277,7 +274,7 @@ mod test { ); // can write multiple - let address_1 = Address::try_from_slice(&[41u8; 20]).unwrap(); + let address_1 = crate::astria_address([41u8; 20]); state.put_ibc_relayer_address(&address_1); assert!( state diff --git a/crates/astria-sequencer/src/lib.rs b/crates/astria-sequencer/src/lib.rs index 96e0077613..ebbe0bf43c 100644 --- a/crates/astria-sequencer/src/lib.rs +++ b/crates/astria-sequencer/src/lib.rs @@ -21,7 +21,32 @@ pub(crate) mod state_ext; pub(crate) mod transaction; mod utils; +use astria_core::primitive::v1::{ + Address, + AddressError, +}; pub use build_info::BUILD_INFO; pub use config::Config; +pub(crate) use config::ADDRESS_PREFIX; pub use sequencer::Sequencer; pub use telemetry; + +/// Constructs an [`Address`] prefixed by `"astria"`. +pub(crate) fn astria_address(array: [u8; astria_core::primitive::v1::ADDRESS_LEN]) -> Address { + Address::builder() + .array(array) + .prefix(ADDRESS_PREFIX) + .try_build() + .unwrap() +} + +/// Tries to construct an [`Address`] prefixed by `"astria"` from a byte slice. +/// +/// # Errors +/// Fails if the slice does not contain 20 bytes. +pub(crate) fn try_astria_address(slice: &[u8]) -> Result { + Address::builder() + .slice(slice) + .prefix(ADDRESS_PREFIX) + .try_build() +} diff --git a/crates/astria-sequencer/src/mempool.rs b/crates/astria-sequencer/src/mempool.rs index 5cd5addc6e..1da336b3c1 100644 --- a/crates/astria-sequencer/src/mempool.rs +++ b/crates/astria-sequencer/src/mempool.rs @@ -62,13 +62,16 @@ impl PartialOrd for TransactionPriority { pub(crate) struct EnqueuedTransaction { tx_hash: [u8; 32], signed_tx: Arc, + address: Address, } impl EnqueuedTransaction { fn new(signed_tx: SignedTransaction) -> Self { + let address = crate::astria_address(signed_tx.verification_key().address_bytes()); Self { tx_hash: signed_tx.sha256_of_proto_encoding(), signed_tx: Arc::new(signed_tx), + address, } } @@ -94,7 +97,7 @@ impl EnqueuedTransaction { } pub(crate) fn address(&self) -> &Address { - self.signed_tx.verification_key().address() + &self.address } } @@ -175,9 +178,11 @@ impl Mempool { /// removes a transaction from the mempool pub(crate) async fn remove(&self, tx_hash: [u8; 32]) { + let (signed_tx, address) = dummy_signed_tx(); let enqueued_tx = EnqueuedTransaction { tx_hash, - signed_tx: dummy_signed_tx().clone(), + signed_tx, + address, }; self.inner.write().await.remove(&enqueued_tx); } @@ -253,21 +258,24 @@ impl Mempool { /// this `signed_tx` field is ignored in the `PartialEq` and `Hash` impls of `EnqueuedTransaction` - /// only the tx hash is considered. So we create an `EnqueuedTransaction` on the fly with the /// correct tx hash and this dummy signed tx when removing from the queue. -fn dummy_signed_tx() -> &'static Arc { - static TX: OnceLock> = OnceLock::new(); - TX.get_or_init(|| { +fn dummy_signed_tx() -> (Arc, Address) { + static TX: OnceLock<(Arc, Address)> = OnceLock::new(); + let (signed_tx, address) = TX.get_or_init(|| { let actions = vec![]; - let params = TransactionParams { - nonce: 0, - chain_id: String::new(), - }; + let params = TransactionParams::builder() + .nonce(0) + .chain_id("dummy") + .try_build() + .expect("all params are valid"); let signing_key = SigningKey::from([0; 32]); + let address = crate::astria_address(signing_key.verification_key().address_bytes()); let unsigned_tx = UnsignedTransaction { actions, params, }; - Arc::new(unsigned_tx.into_signed(&signing_key)) - }) + (Arc::new(unsigned_tx.into_signed(&signing_key)), address) + }); + (signed_tx.clone(), *address) } #[cfg(test)] @@ -346,14 +354,17 @@ mod test { let tx0 = EnqueuedTransaction { tx_hash: [0; 32], signed_tx: Arc::new(get_mock_tx(0)), + address: crate::astria_address(get_mock_tx(0).verification_key().address_bytes()), }; let other_tx0 = EnqueuedTransaction { tx_hash: [0; 32], signed_tx: Arc::new(get_mock_tx(1)), + address: crate::astria_address(get_mock_tx(1).verification_key().address_bytes()), }; let tx1 = EnqueuedTransaction { tx_hash: [1; 32], signed_tx: Arc::new(get_mock_tx(0)), + address: crate::astria_address(get_mock_tx(0).verification_key().address_bytes()), }; assert!(tx0 == other_tx0); assert!(tx0 != tx1); @@ -452,10 +463,11 @@ mod test { let other_mock_tx = |nonce: u32| -> SignedTransaction { let actions = get_mock_tx(0).actions().to_vec(); UnsignedTransaction { - params: TransactionParams { - nonce, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(nonce) + .chain_id("test") + .try_build() + .unwrap(), actions, } .into_signed(&other_signing_key) @@ -467,7 +479,8 @@ mod test { let (alice_signing_key, alice_address) = crate::app::test_utils::get_alice_signing_key_and_address(); - let other_address = *other_signing_key.verification_key().address(); + let other_address = + crate::astria_address(other_signing_key.verification_key().address_bytes()); // Create a getter fn which will returns 1 for alice's current account nonce, and 101 for // the other signer's. @@ -522,10 +535,11 @@ mod test { let other_mock_tx = |nonce: u32| -> SignedTransaction { let actions = get_mock_tx(0).actions().to_vec(); UnsignedTransaction { - params: TransactionParams { - nonce, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(nonce) + .chain_id("test") + .try_build() + .unwrap(), actions, } .into_signed(&other_signing_key) @@ -538,15 +552,23 @@ mod test { // Check the pending nonce for alice is 1 and for the other signer is 101. let alice_address = crate::app::test_utils::get_alice_signing_key_and_address().1; assert_eq!(mempool.pending_nonce(&alice_address).await.unwrap(), 1); - let other_address = *other_signing_key.verification_key().address(); + let other_address = + crate::astria_address(other_signing_key.verification_key().address_bytes()); assert_eq!(mempool.pending_nonce(&other_address).await.unwrap(), 101); // Check the pending nonce for an address with no enqueued txs is `None`. assert!( mempool - .pending_nonce(&Address::from([1; 20])) + .pending_nonce(&crate::astria_address([1; 20])) .await .is_none() ); } + + #[test] + fn enqueued_transaction_can_be_instantiated() { + // This just tests that the constructor does not fail. + let signed_tx = crate::app::test_utils::get_mock_tx(0); + let _ = EnqueuedTransaction::new(signed_tx); + } } diff --git a/crates/astria-sequencer/src/proposal/commitment.rs b/crates/astria-sequencer/src/proposal/commitment.rs index 65914b8492..46f018a9f4 100644 --- a/crates/astria-sequencer/src/proposal/commitment.rs +++ b/crates/astria-sequencer/src/proposal/commitment.rs @@ -89,12 +89,9 @@ pub(crate) fn generate_rollup_datas_commitment( mod test { use astria_core::{ crypto::SigningKey, - primitive::v1::{ - asset::{ - Denom, - DEFAULT_NATIVE_ASSET_DENOM, - }, - Address, + primitive::v1::asset::{ + Denom, + DEFAULT_NATIVE_ASSET_DENOM, }, protocol::transaction::v1alpha1::{ action::{ @@ -123,7 +120,7 @@ mod test { fee_asset_id: get_native_asset().id(), }; let transfer_action = TransferAction { - to: Address::from([0u8; 20]), + to: crate::astria_address([0u8; 20]), amount: 1, asset_id: get_native_asset().id(), fee_asset_id: get_native_asset().id(), @@ -131,10 +128,11 @@ mod test { let signing_key = SigningKey::new(OsRng); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test-chain-1".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test-chain-1") + .try_build() + .unwrap(), actions: vec![sequence_action.clone().into(), transfer_action.into()], }; @@ -147,10 +145,11 @@ mod test { let signing_key = SigningKey::new(OsRng); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test-chain-1".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test-chain-1") + .try_build() + .unwrap(), actions: vec![sequence_action.into()], }; @@ -178,7 +177,7 @@ mod test { fee_asset_id: get_native_asset().id(), }; let transfer_action = TransferAction { - to: Address::from([0u8; 20]), + to: crate::astria_address([0u8; 20]), amount: 1, asset_id: get_native_asset().id(), fee_asset_id: get_native_asset().id(), @@ -186,10 +185,11 @@ mod test { let signing_key = SigningKey::new(OsRng); let tx = UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test-chain-1".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test-chain-1") + .try_build() + .unwrap(), actions: vec![sequence_action.into(), transfer_action.into()], }; diff --git a/crates/astria-sequencer/src/service/consensus.rs b/crates/astria-sequencer/src/service/consensus.rs index 993cf74d98..acac6ed0db 100644 --- a/crates/astria-sequencer/src/service/consensus.rs +++ b/crates/astria-sequencer/src/service/consensus.rs @@ -225,7 +225,6 @@ mod test { }, primitive::v1::{ asset::DEFAULT_NATIVE_ASSET_DENOM, - Address, RollupId, }, protocol::transaction::v1alpha1::{ @@ -253,10 +252,11 @@ mod test { fn make_unsigned_tx() -> UnsignedTransaction { UnsignedTransaction { - params: TransactionParams { - nonce: 0, - chain_id: "test".to_string(), - }, + params: TransactionParams::builder() + .nonce(0) + .chain_id("test") + .try_build() + .unwrap(), actions: vec![ SequenceAction { rollup_id: RollupId::from_unhashed_bytes(b"testchainid"), @@ -461,8 +461,8 @@ mod test { fn default() -> Self { Self { accounts: vec![], - authority_sudo_address: Address::from([0; 20]), - ibc_sudo_address: Address::from([0; 20]), + authority_sudo_address: crate::astria_address([0; 20]), + ibc_sudo_address: crate::astria_address([0; 20]), ibc_relayer_addresses: vec![], native_asset_base_denomination: DEFAULT_NATIVE_ASSET_DENOM.to_string(), ibc_params: penumbra_ibc::params::IBCParameters::default(), @@ -475,7 +475,7 @@ mod test { async fn new_consensus_service(funded_key: Option) -> (Consensus, Mempool) { let accounts = if funded_key.is_some() { vec![crate::genesis::Account { - address: *funded_key.unwrap().address(), + address: crate::astria_address(funded_key.unwrap().address_bytes()), balance: 10u128.pow(19), }] } else { diff --git a/crates/astria-sequencer/src/service/info/mod.rs b/crates/astria-sequencer/src/service/info/mod.rs index 1c462cb734..ebc3c712cb 100644 --- a/crates/astria-sequencer/src/service/info/mod.rs +++ b/crates/astria-sequencer/src/service/info/mod.rs @@ -166,7 +166,6 @@ mod test { Denom, DEFAULT_NATIVE_ASSET_DENOM, }, - Address, }; use cnidarium::StateDelta; use prost::Message as _; @@ -207,7 +206,7 @@ mod test { initialize_native_asset(DEFAULT_NATIVE_ASSET_DENOM); - let address = Address::try_from_slice( + let address = crate::try_astria_address( &hex::decode("a034c743bed8f26cb8ee7b8db2230fd8347ae131").unwrap(), ) .unwrap(); diff --git a/crates/astria-sequencer/src/service/mempool.rs b/crates/astria-sequencer/src/service/mempool.rs index b688bf376a..4cc1b4b029 100644 --- a/crates/astria-sequencer/src/service/mempool.rs +++ b/crates/astria-sequencer/src/service/mempool.rs @@ -189,7 +189,9 @@ async fn handle_check_tx( // tx is valid, push to mempool let current_account_nonce = state - .get_account_nonce(*signed_tx.verification_key().address()) + .get_account_nonce(crate::astria_address( + signed_tx.verification_key().address_bytes(), + )) .await .expect("can fetch account nonce"); diff --git a/crates/astria-sequencer/src/transaction/checks.rs b/crates/astria-sequencer/src/transaction/checks.rs index a2074e0e2b..f0db1ecbef 100644 --- a/crates/astria-sequencer/src/transaction/checks.rs +++ b/crates/astria-sequencer/src/transaction/checks.rs @@ -31,15 +31,12 @@ pub(crate) async fn check_nonce_mempool( tx: &SignedTransaction, state: &S, ) -> anyhow::Result<()> { - let signer_address = *tx.verification_key().address(); + let signer_address = crate::astria_address(tx.verification_key().address_bytes()); let curr_nonce = state .get_account_nonce(signer_address) .await .context("failed to get account nonce")?; - ensure!( - tx.unsigned_transaction().params.nonce >= curr_nonce, - "nonce already used by account" - ); + ensure!(tx.nonce() >= curr_nonce, "nonce already used by account"); Ok(()) } @@ -51,10 +48,7 @@ pub(crate) async fn check_chain_id_mempool( .get_chain_id() .await .context("failed to get chain id")?; - ensure!( - tx.unsigned_transaction().params.chain_id == chain_id.as_str(), - "chain id mismatch" - ); + ensure!(tx.chain_id() == chain_id.as_str(), "chain id mismatch"); Ok(()) } @@ -62,7 +56,7 @@ pub(crate) async fn check_balance_mempool( tx: &SignedTransaction, state: &S, ) -> anyhow::Result<()> { - let signer_address = *tx.verification_key().address(); + let signer_address = crate::astria_address(tx.verification_key().address_bytes()); check_balance_for_total_fees(tx.unsigned_transaction(), signer_address, state).await?; Ok(()) } @@ -335,7 +329,7 @@ mod test { asset_id: other_asset, amount, fee_asset_id: native_asset, - to: [0; ADDRESS_LEN].into(), + to: crate::astria_address([0; ADDRESS_LEN]), }), Action::Sequence(SequenceAction { rollup_id: RollupId::from_unhashed_bytes([0; 32]), @@ -344,10 +338,11 @@ mod test { }), ]; - let params = TransactionParams { - nonce: 0, - chain_id: "test-chain-id".to_string(), - }; + let params = TransactionParams::builder() + .nonce(0) + .chain_id("test-chain-id") + .try_build() + .unwrap(); let tx = UnsignedTransaction { actions, params, @@ -397,7 +392,7 @@ mod test { asset_id: other_asset, amount, fee_asset_id: native_asset, - to: [0; ADDRESS_LEN].into(), + to: crate::astria_address([0; ADDRESS_LEN]), }), Action::Sequence(SequenceAction { rollup_id: RollupId::from_unhashed_bytes([0; 32]), @@ -406,10 +401,11 @@ mod test { }), ]; - let params = TransactionParams { - nonce: 0, - chain_id: "test-chain-id".to_string(), - }; + let params = TransactionParams::builder() + .nonce(0) + .chain_id("test-chain-id") + .try_build() + .unwrap(); let tx = UnsignedTransaction { actions, params, diff --git a/crates/astria-sequencer/src/transaction/mod.rs b/crates/astria-sequencer/src/transaction/mod.rs index c7a5ce3d7c..cd1d0b63f2 100644 --- a/crates/astria-sequencer/src/transaction/mod.rs +++ b/crates/astria-sequencer/src/transaction/mod.rs @@ -47,7 +47,7 @@ pub(crate) async fn check_stateful( tx: &SignedTransaction, state: &S, ) -> anyhow::Result<()> { - let signer_address = *tx.verification_key().address(); + let signer_address = crate::astria_address(tx.verification_key().address_bytes()); tx.unsigned_transaction() .check_stateful(state, signer_address) .await @@ -62,7 +62,7 @@ pub(crate) async fn execute( StateWriteExt as _, }; - let signer_address = *tx.verification_key().address(); + let signer_address = crate::astria_address(tx.verification_key().address_bytes()); if state .get_bridge_account_rollup_id(&signer_address) @@ -184,17 +184,14 @@ impl ActionHandler for UnsignedTransaction { // Transactions must match the chain id of the node. let chain_id = state.get_chain_id().await?; ensure!( - self.params.chain_id == chain_id.as_str(), - InvalidChainId(self.params.chain_id.clone()) + self.chain_id() == chain_id.as_str(), + InvalidChainId(self.chain_id().to_string()) ); // Nonce should be equal to the number of executed transactions before this tx. // First tx has nonce 0. let curr_nonce = state.get_account_nonce(from).await?; - ensure!( - curr_nonce == self.params.nonce, - InvalidNonce(self.params.nonce) - ); + ensure!(curr_nonce == self.nonce(), InvalidNonce(self.nonce())); // Should have enough balance to cover all actions. check_balance_for_total_fees(self, from, state).await?; @@ -263,8 +260,8 @@ impl ActionHandler for UnsignedTransaction { #[instrument( skip_all, fields( - nonce = self.params.nonce, - from = from.to_string(), + nonce = self.nonce(), + from = %from, ) )] async fn execute(&self, state: &mut S, from: Address) -> anyhow::Result<()> { diff --git a/proto/primitives/astria/primitive/v1/types.proto b/proto/primitives/astria/primitive/v1/types.proto index c38f0c1a03..e9fef40064 100644 --- a/proto/primitives/astria/primitive/v1/types.proto +++ b/proto/primitives/astria/primitive/v1/types.proto @@ -44,8 +44,18 @@ message RollupId { // An Astria `Address`. // -// Astria addresses are derived from the ed25519 public key, -// using the first 20 bytes of the sha256 hash. +// Astria addresses are bech32m encoded strings, with the data part being the +// first 20 entries of a sha256-hashed ed25519 public key. message Address { - bytes inner = 1; + // The first 20 bytes of a sha256-hashed ed25519 public key. + // Implementors must avoid setting this field in favor of `bech32m`. + // Implementors must not accept both `inner` and `bech32m` being set. + // DEPRECATED: this field is deprecated and only exists for backward compatibility. + // Astria services assume an implicit prefix of "astria" if this field is set. + // Astria services will read this field but will never emit it. + bytes inner = 1 [deprecated = true]; + + // A bech32m encoded string. The data are the first 20 bytes of a sha256-hashed ed25519 + // public key. Implementors must not accept both the `bytes` and `bech32m` being set. + string bech32m = 2; } diff --git a/proto/protocolapis/astria/protocol/transactions/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/transactions/v1alpha1/types.proto index 17d462f2ff..34a6c1073c 100644 --- a/proto/protocolapis/astria/protocol/transactions/v1alpha1/types.proto +++ b/proto/protocolapis/astria/protocol/transactions/v1alpha1/types.proto @@ -108,7 +108,7 @@ message Ics20Withdrawal { string destination_chain_address = 3; // an Astria address to use to return funds from this withdrawal // in the case it fails. - bytes return_address = 4; + astria.primitive.v1.Address return_address = 4; // the height (on Astria) at which this transfer expires. IbcHeight timeout_height = 5; // the unix timestamp (in nanoseconds) at which this transfer expires.