Skip to main content

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}