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