Skip to main content

miden_standards/interop/eth/
embedded_account_id.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::fmt;
4
5use miden_protocol::Felt;
6use miden_protocol::account::AccountId;
7
8use super::address::{AddressConversionError, EthAddress};
9
10// ================================================================================================
11// ETH EMBEDDED ACCOUNT ID
12// ================================================================================================
13
14/// Represents a Miden [`AccountId`] that can be encoded in the 20-byte Ethereum address format.
15///
16/// This type wraps an [`AccountId`] and provides conversions to/from the Ethereum address
17/// encoding used in the bridge-in flow. In this encoding, the 20-byte Ethereum address format
18/// stores a Miden [`AccountId`] as: `0x00000000 || prefix(8) || suffix(8)`, where:
19/// - prefix = bytes[4..12] as a big-endian u64
20/// - suffix = bytes[12..20] as a big-endian u64
21///
22/// Note: prefix/suffix are *conceptual* 64-bit words; when converting to [`Felt`], we must ensure
23/// `Felt::new_unchecked(u64)` does not reduce mod p (checked explicitly in
24/// [`Self::try_from_eth_address`]).
25///
26/// This type is used by bridge integrators to convert between Miden AccountIds and the Ethereum
27/// address format, e.g. when constructing claim notes or calling an EVM bridge contract.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub struct EthEmbeddedAccountId(AccountId);
30
31impl EthEmbeddedAccountId {
32    // CONSTRUCTORS
33    // --------------------------------------------------------------------------------------------
34
35    /// Creates an [`EthEmbeddedAccountId`] from a 20-byte array.
36    ///
37    /// The bytes are interpreted as an Ethereum-encoded Miden [`AccountId`] (big-endian):
38    /// `0x00000000 || prefix(8) || suffix(8)`.
39    ///
40    /// # Errors
41    ///
42    /// Returns an error if:
43    /// - the first 4 bytes (i.e., the most significant bytes) are not zero,
44    /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p,
45    /// - or the resulting felts do not form a valid [`AccountId`].
46    pub fn new(bytes: [u8; 20]) -> Result<Self, AddressConversionError> {
47        Self::try_from_eth_address(EthAddress::new(bytes))
48    }
49
50    /// Creates an [`EthEmbeddedAccountId`] from a hex string (with or without "0x" prefix).
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the hex string is invalid, the hex part is not exactly 40 characters,
55    /// or the decoded bytes do not represent a valid embedded [`AccountId`].
56    pub fn from_hex(hex_str: &str) -> Result<Self, AddressConversionError> {
57        let addr = EthAddress::from_hex(hex_str)?;
58        Self::try_from_eth_address(addr)
59    }
60
61    /// Creates an [`EthEmbeddedAccountId`] from an [`AccountId`].
62    ///
63    /// This conversion is infallible: an [`AccountId`] is always valid.
64    ///
65    /// # Example
66    /// ```ignore
67    /// let embedded = EthEmbeddedAccountId::from_account_id(destination_account_id);
68    /// let address_bytes = embedded.to_eth_address().into_bytes();
69    /// // then construct the CLAIM note with address_bytes...
70    /// ```
71    pub const fn from_account_id(account_id: AccountId) -> Self {
72        Self(account_id)
73    }
74
75    /// Creates an [`EthEmbeddedAccountId`] from an [`EthAddress`].
76    ///
77    /// Validates that the address contains a properly encoded Miden [`AccountId`].
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if:
82    /// - the first 4 bytes are not zero (not in the embedded AccountId format),
83    /// - packing the 8-byte prefix/suffix into [`Felt`] would reduce mod p,
84    /// - or the resulting felts do not form a valid [`AccountId`].
85    pub fn try_from_eth_address(addr: EthAddress) -> Result<Self, AddressConversionError> {
86        let bytes = addr.into_bytes();
87        let (prefix, suffix) = bytes20_to_prefix_suffix(bytes)?;
88
89        let prefix_felt =
90            Felt::try_from(prefix).map_err(|_| AddressConversionError::FeltOutOfField)?;
91
92        let suffix_felt =
93            Felt::try_from(suffix).map_err(|_| AddressConversionError::FeltOutOfField)?;
94
95        let account_id = AccountId::try_from_elements(suffix_felt, prefix_felt)
96            .map_err(|_| AddressConversionError::InvalidAccountId)?;
97
98        Ok(Self(account_id))
99    }
100
101    // PUBLIC ACCESSORS
102    // --------------------------------------------------------------------------------------------
103
104    /// Returns a reference to the inner [`AccountId`].
105    pub const fn to_account_id(&self) -> &AccountId {
106        &self.0
107    }
108
109    /// Consumes self and returns the inner [`AccountId`].
110    pub const fn into_account_id(self) -> AccountId {
111        self.0
112    }
113
114    /// Converts the embedded account ID to an [`EthAddress`].
115    ///
116    /// The resulting 20-byte address has the format:
117    /// `0x00000000 || prefix(8) || suffix(8)` (big-endian byte ordering).
118    pub fn to_eth_address(&self) -> EthAddress {
119        let mut out = [0u8; 20];
120        out[4..12].copy_from_slice(&self.0.prefix().as_u64().to_be_bytes());
121        out[12..20].copy_from_slice(&self.0.suffix().as_canonical_u64().to_be_bytes());
122
123        EthAddress::new(out)
124    }
125
126    /// Returns the raw 20-byte Ethereum address encoding.
127    pub fn to_bytes(&self) -> [u8; 20] {
128        self.to_eth_address().into_bytes()
129    }
130
131    /// Returns the bytes32-embedded encoding: the 20-byte Ethereum address encoding left-padded
132    /// to 32 bytes (bytes 0..12 zero, address in bytes 12..32).
133    pub fn to_bytes32(&self) -> [u8; 32] {
134        let mut out = [0u8; 32];
135        out[12..32].copy_from_slice(&self.to_bytes());
136        out
137    }
138
139    /// Converts the address to a hex string (lowercase, 0x-prefixed).
140    pub fn to_hex(&self) -> String {
141        self.to_eth_address().to_hex()
142    }
143
144    /// Converts the address into an array of 5 [`Felt`] values for Miden VM.
145    ///
146    /// See [`EthAddress::to_elements`] for details on the encoding.
147    pub fn to_elements(&self) -> Vec<Felt> {
148        self.to_eth_address().to_elements()
149    }
150}
151
152impl fmt::Display for EthEmbeddedAccountId {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "{}", self.to_eth_address())
155    }
156}
157
158impl TryFrom<EthAddress> for EthEmbeddedAccountId {
159    type Error = AddressConversionError;
160
161    fn try_from(addr: EthAddress) -> Result<Self, Self::Error> {
162        Self::try_from_eth_address(addr)
163    }
164}
165
166impl From<EthEmbeddedAccountId> for EthAddress {
167    fn from(embedded: EthEmbeddedAccountId) -> Self {
168        embedded.to_eth_address()
169    }
170}
171
172impl TryFrom<[u8; 20]> for EthEmbeddedAccountId {
173    type Error = AddressConversionError;
174
175    fn try_from(bytes: [u8; 20]) -> Result<Self, Self::Error> {
176        Self::new(bytes)
177    }
178}
179
180impl From<EthEmbeddedAccountId> for [u8; 20] {
181    fn from(embedded: EthEmbeddedAccountId) -> Self {
182        embedded.to_bytes()
183    }
184}
185
186impl From<AccountId> for EthEmbeddedAccountId {
187    fn from(account_id: AccountId) -> Self {
188        EthEmbeddedAccountId::from_account_id(account_id)
189    }
190}
191
192impl From<EthEmbeddedAccountId> for AccountId {
193    fn from(embedded: EthEmbeddedAccountId) -> Self {
194        embedded.0
195    }
196}
197
198// ================================================================================================
199// HELPER FUNCTIONS
200// ================================================================================================
201
202/// Convert `[u8; 20]` -> `(prefix, suffix)` by extracting the last 16 bytes.
203/// Requires the first 4 bytes be zero.
204/// Returns prefix and suffix values that match the MASM little-endian limb byte encoding:
205/// - prefix = bytes[4..12] as big-endian u64 = (addr3 << 32) | addr2
206/// - suffix = bytes[12..20] as big-endian u64 = (addr1 << 32) | addr0
207fn bytes20_to_prefix_suffix(bytes: [u8; 20]) -> Result<(u64, u64), AddressConversionError> {
208    if bytes[0..4] != [0, 0, 0, 0] {
209        return Err(AddressConversionError::NonZeroBytePrefix);
210    }
211
212    let prefix = u64::from_be_bytes(bytes[4..12].try_into().unwrap());
213    let suffix = u64::from_be_bytes(bytes[12..20].try_into().unwrap());
214
215    Ok((prefix, suffix))
216}