miden_agglayer/config_note.rs
1//! CONFIG_AGG_BRIDGE note creation utilities.
2//!
3//! This module provides helpers for creating CONFIG_AGG_BRIDGE notes,
4//! which are used to register faucets in the bridge's faucet registry.
5
6extern crate alloc;
7
8use alloc::string::ToString;
9use alloc::vec;
10use alloc::vec::Vec;
11
12use miden_core::Felt;
13use miden_protocol::account::AccountId;
14use miden_protocol::crypto::rand::FeltRng;
15use miden_protocol::errors::NoteError;
16use miden_protocol::note::{
17 Note,
18 NoteAssets,
19 NoteAttachment,
20 NoteAttachments,
21 NoteRecipient,
22 NoteScript,
23 NoteScriptRoot,
24 NoteStorage,
25 NoteType,
26 PartialNoteMetadata,
27};
28use miden_standards::interop::eth::EthAddress;
29use miden_standards::note::costs::NoteConsumptionCost;
30use miden_standards::note::{NetworkAccountTarget, NoteExecutionHint};
31use miden_utils_sync::LazyLock;
32
33use crate::costs::CONFIG_AGG_BRIDGE_CONSUMPTION_CYCLES;
34use crate::{MetadataHash, note_script};
35
36// NOTE SCRIPT
37// ================================================================================================
38
39/// Path to the CONFIG_AGG_BRIDGE note script procedure in the agglayer package.
40const CONFIG_AGG_BRIDGE_SCRIPT_PATH: &str = "::agglayer::notes::config_agg_bridge::main";
41
42// Initialize the CONFIG_AGG_BRIDGE note script only once
43static CONFIG_AGG_BRIDGE_SCRIPT: LazyLock<NoteScript> =
44 LazyLock::new(|| note_script(CONFIG_AGG_BRIDGE_SCRIPT_PATH));
45
46// CONVERSION METADATA
47// ================================================================================================
48
49/// The conversion metadata registered on the bridge for a single faucet.
50///
51/// Encapsulates the origin-chain identity and bridge-side policy of a faucet: the EVM token
52/// address, network id, decimal scale, whether the faucet is Miden-native (lock/unlock) or
53/// bridge-owned (burn/mint), and the keccak256 metadata hash.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct ConversionMetadata {
56 /// Account ID of the faucet being registered.
57 pub faucet_account_id: AccountId,
58 /// Origin EVM token address the faucet wraps.
59 pub origin_token_address: EthAddress,
60 /// Decimal scaling factor between the origin-chain unit and the Miden-side unit
61 /// (e.g. 0 for USDC, 8 for ETH).
62 pub scale: u8,
63 /// Origin network / chain ID the token lives on.
64 pub origin_network: u32,
65 /// `true` for Miden-native faucets (bridge-in unlocks from the bridge vault, bridge-out
66 /// locks into it); `false` for bridge-owned faucets (bridge-in mints via the faucet,
67 /// bridge-out burns via the faucet).
68 pub is_native: bool,
69 /// keccak256 hash of the ABI-encoded token metadata (`name`, `symbol`, `decimals`).
70 pub metadata_hash: MetadataHash,
71}
72
73impl ConversionMetadata {
74 /// Serializes the metadata to the 18-felt layout consumed by `CONFIG_AGG_BRIDGE`.
75 ///
76 /// `origin_network` is written in raw u32 form (no byte swap). The bridge stores it as-is
77 /// in `faucet_metadata_map`; `bridge_out::convert_asset` later applies `swap_u32_bytes` to
78 /// produce the leaf-side representation. The token-registry side of registration applies
79 /// the matching swap inside `register_faucet`'s MASM before hashing, keeping the hash
80 /// byte-identical with the leaf-side `lookup_faucet_by_token_address` input.
81 pub fn to_elements(&self) -> Vec<Felt> {
82 let mut v = Vec::with_capacity(ConfigAggBridgeNote::NUM_STORAGE_ITEMS);
83 v.extend(self.origin_token_address.to_elements());
84 v.push(self.faucet_account_id.suffix());
85 v.push(self.faucet_account_id.prefix().as_felt());
86 v.push(Felt::from(self.scale));
87 v.push(Felt::from(self.origin_network));
88 v.push(Felt::from(u8::from(self.is_native)));
89 v.extend(self.metadata_hash.to_elements());
90 v
91 }
92}
93
94// CONFIG_AGG_BRIDGE NOTE
95// ================================================================================================
96
97/// CONFIG_AGG_BRIDGE note.
98///
99/// This note is used to register a faucet in the bridge's faucet and token registries,
100/// and to store full conversion metadata (origin address, origin network, scale, metadata hash)
101/// in the bridge's faucet metadata map.
102pub struct ConfigAggBridgeNote;
103
104impl ConfigAggBridgeNote {
105 // CONSTANTS
106 // --------------------------------------------------------------------------------------------
107
108 /// Expected number of storage items for a CONFIG_AGG_BRIDGE note.
109 ///
110 /// Layout (18 felts):
111 /// - `[0..4]` origin_token_addr (5 felts)
112 /// - `[5]` faucet_id_suffix
113 /// - `[6]` faucet_id_prefix
114 /// - `[7]` scale
115 /// - `[8]` origin_network (raw u32; the MASM register flow byte-swaps it before hashing
116 /// into the token-registry key, and `bridge_out` byte-swaps it before placing it in the LET
117 /// leaf)
118 /// - `[9]` is_native (0 or 1)
119 /// - `[10..13]` METADATA_HASH_LO (4 felts)
120 /// - `[14..17]` METADATA_HASH_HI (4 felts)
121 pub const NUM_STORAGE_ITEMS: usize = 18;
122
123 // PUBLIC ACCESSORS
124 // --------------------------------------------------------------------------------------------
125
126 /// Returns the CONFIG_AGG_BRIDGE note script.
127 pub fn script() -> NoteScript {
128 CONFIG_AGG_BRIDGE_SCRIPT.clone()
129 }
130
131 /// Returns the CONFIG_AGG_BRIDGE note script root.
132 pub fn script_root() -> NoteScriptRoot {
133 CONFIG_AGG_BRIDGE_SCRIPT.root()
134 }
135
136 // BUILDERS
137 // --------------------------------------------------------------------------------------------
138
139 /// Creates a CONFIG_AGG_BRIDGE note to register a faucet in the bridge's registry.
140 ///
141 /// # Parameters
142 /// - `metadata`: The conversion metadata to register for the faucet.
143 /// - `sender_account_id`: The account ID of the note creator.
144 /// - `target_account_id`: The bridge account ID that will consume this note.
145 /// - `rng`: Random number generator for creating the note serial number.
146 ///
147 /// # Errors
148 /// Returns an error if note creation fails.
149 pub fn create<R: FeltRng>(
150 metadata: ConversionMetadata,
151 sender_account_id: AccountId,
152 target_account_id: AccountId,
153 rng: &mut R,
154 ) -> Result<Note, NoteError> {
155 let storage_values = metadata.to_elements();
156
157 debug_assert_eq!(
158 storage_values.len(),
159 Self::NUM_STORAGE_ITEMS,
160 "CONFIG_AGG_BRIDGE storage must have exactly {} felts",
161 Self::NUM_STORAGE_ITEMS
162 );
163
164 let note_storage = NoteStorage::new(storage_values)?;
165
166 // Generate a serial number for the note
167 let serial_num = rng.draw_word();
168
169 let recipient = NoteRecipient::new(serial_num, Self::script(), note_storage);
170
171 let attachment = NetworkAccountTarget::new(target_account_id, NoteExecutionHint::Always)
172 .map_err(|e| NoteError::other(e.to_string()))?;
173 let attachments = NoteAttachments::from(NoteAttachment::from(attachment));
174 let metadata = PartialNoteMetadata::new(sender_account_id, NoteType::Public);
175
176 // CONFIG_AGG_BRIDGE notes don't carry assets
177 let assets = NoteAssets::new(vec![])?;
178
179 Ok(Note::with_attachments(assets, metadata, recipient, attachments))
180 }
181}
182
183// NOTE CONSUMPTION COST
184// ================================================================================================
185
186impl NoteConsumptionCost for ConfigAggBridgeNote {
187 fn consumption_cycles() -> u32 {
188 CONFIG_AGG_BRIDGE_CONSUMPTION_CYCLES
189 }
190}
191
192// TESTS
193// ================================================================================================
194
195#[cfg(test)]
196mod tests {
197 use miden_protocol::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
198
199 use super::*;
200
201 /// Locks in the 18-felt wire layout of `CONFIG_AGG_BRIDGE` note storage. Any reordering in
202 /// `to_elements` would silently desync from the indices the MASM `CONFIG_AGG_BRIDGE` script
203 /// reads from (`ORIGIN_TOKEN_ADDR_0..4`, `FAUCET_ID_SUFFIX=5`, ... `METADATA_HASH_HI_3=17`).
204 #[test]
205 fn to_elements_layout_matches_masm_storage_indices() {
206 let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)
207 .expect("valid faucet account id");
208 let origin_token_address =
209 EthAddress::from_hex("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
210 let metadata_hash = MetadataHash::from_token_info("USD Coin", "USDC", 6);
211
212 let metadata = ConversionMetadata {
213 faucet_account_id: faucet,
214 origin_token_address,
215 scale: 6,
216 origin_network: 42,
217 is_native: true,
218 metadata_hash,
219 };
220
221 let elements = metadata.to_elements();
222
223 assert_eq!(elements.len(), ConfigAggBridgeNote::NUM_STORAGE_ITEMS);
224 assert_eq!(&elements[0..5], origin_token_address.to_elements().as_slice());
225 assert_eq!(elements[5], faucet.suffix());
226 assert_eq!(elements[6], faucet.prefix().as_felt());
227 assert_eq!(elements[7], Felt::from(6_u8));
228 // origin_network is stored raw (the MASM bridge-side does any required byte-swap
229 // before hashing into the token-registry or placing into the LET leaf).
230 assert_eq!(elements[8], Felt::from(42_u32));
231 assert_eq!(elements[9], Felt::from(1_u8));
232 assert_eq!(&elements[10..18], metadata_hash.to_elements().as_slice());
233 }
234}