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}