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_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}