Skip to main content

miden_agglayer/
lib.rs

1#![no_std]
2
3extern crate alloc;
4
5use alloc::collections::BTreeMap;
6
7use miden_core::{Felt, Word};
8use miden_protocol::account::{AccountBuilder, AccountComponent, AccountId};
9use miden_protocol::assembly::Path;
10use miden_protocol::asset::TokenSymbol;
11use miden_protocol::note::NoteScript;
12use miden_protocol::vm::Package;
13use miden_standards::account::access::{
14    Authority,
15    Ownable2Step,
16    Pausable,
17    PausableManager,
18    RoleBasedAccessControl,
19    RoleConfig,
20};
21use miden_standards::account::auth::NetworkAccount;
22use miden_standards::account::fees::{
23    BasicConstantFeePolicy,
24    ConstantFeeManager,
25    FeePolicyManager,
26};
27use miden_standards::account::policies::{
28    BurnPolicy,
29    MintPolicy,
30    TokenPolicyManager,
31    TransferPolicy,
32};
33use miden_utils_sync::LazyLock;
34
35pub mod agglayer_note;
36pub mod b2agg_note;
37pub mod bridge;
38pub mod claim_note;
39pub mod config_note;
40pub mod costs;
41pub mod deregister_note;
42pub mod errors;
43pub mod eth_types;
44pub mod faucet;
45mod ger_note;
46pub mod remove_ger_note;
47#[cfg(any(feature = "testing", test))]
48pub mod testing;
49pub mod update_ger_note;
50pub mod utils;
51
52pub use agglayer_note::AgglayerNote;
53pub use b2agg_note::B2AggNote;
54pub use bridge::{AggLayerBridge, AgglayerBridgeError, BridgeRoles, RemovedGerHashChain};
55pub use claim_note::{
56    CgiChainHash,
57    ClaimNote,
58    ClaimNoteStorage,
59    ExitRoot,
60    LeafData,
61    LeafValue,
62    ProofData,
63    SmtNode,
64};
65pub use config_note::{ConfigAggBridgeNote, ConversionMetadata};
66pub use deregister_note::DeregisterAggFaucetNote;
67#[cfg(any(test, feature = "testing"))]
68pub use eth_types::GlobalIndexExt;
69pub use eth_types::{GlobalIndex, GlobalIndexError, MetadataHash};
70pub use faucet::{AggLayerFaucet, AgglayerFaucetError};
71pub use remove_ger_note::RemoveGerNote;
72pub use update_ger_note::UpdateGerNote;
73pub use utils::Keccak256Output;
74
75// AGGLAYER ACCOUNT COMPONENTS
76// ================================================================================================
77
78static AGGLAYER_PACKAGE: LazyLock<Package> = LazyLock::new(|| {
79    let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/assets/miden-agglayer.masp"));
80    Package::read_from_bytes_trusted(bytes).expect("shipped AggLayer package is well-formed")
81});
82
83static BRIDGE_COMPONENT_PACKAGE: LazyLock<Package> = LazyLock::new(|| {
84    let bytes =
85        include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/miden-agglayer-bridge.masp"));
86    Package::read_from_bytes_trusted(bytes)
87        .expect("shipped bridge component package is well-formed")
88});
89
90static FAUCET_COMPONENT_PACKAGE: LazyLock<Package> = LazyLock::new(|| {
91    let bytes =
92        include_bytes!(concat!(env!("OUT_DIR"), "/assets/components/miden-agglayer-faucet.masp"));
93    Package::read_from_bytes_trusted(bytes)
94        .expect("shipped faucet component package is well-formed")
95});
96
97/// Returns the AggLayer package containing all agglayer modules, including the note scripts.
98///
99/// The note scripts this crate builds are external references into this package rather than
100/// self-contained copies of it, so it must be registered with the MAST store of any executor that
101/// runs AggLayer notes. This mirrors the standard note scripts, which are external references into
102/// the standards library. `TransactionMastStore::new` preloads both packages, so the in-repo
103/// prover and test executors resolve AggLayer notes automatically; a downstream executor that
104/// supplies its own `DataStore` must register this package into it (e.g. via
105/// `TransactionMastStore::insert_package`), exactly as it must already register the standards
106/// package to run standard notes.
107pub fn agglayer_package() -> Package {
108    AGGLAYER_PACKAGE.clone()
109}
110
111/// Resolves the note script exported at `path` from the AggLayer package.
112///
113/// `path` must be the fully qualified path of a procedure carrying the `@note_script` attribute,
114/// e.g. `::agglayer::notes::claim::main`.
115pub(crate) fn note_script(path: &str) -> NoteScript {
116    NoteScript::from_package_reference(&AGGLAYER_PACKAGE, Path::new(path))
117        .expect("agglayer package contains the note script procedure")
118}
119
120/// Returns the Bridge component package.
121fn agglayer_bridge_component_package() -> Package {
122    BRIDGE_COMPONENT_PACKAGE.clone()
123}
124
125/// Returns the Faucet component package.
126fn agglayer_faucet_component_package() -> Package {
127    FAUCET_COMPONENT_PACKAGE.clone()
128}
129
130// AGGLAYER ACCOUNT CREATION HELPERS
131// ================================================================================================
132
133/// Creates an agglayer faucet account component with the specified configuration.
134///
135/// The faucet holds only token metadata; conversion metadata (origin address, origin network,
136/// scale, metadata hash) lives on the bridge and is populated at registration time.
137///
138/// # Parameters
139/// - `token_symbol`: The symbol for the fungible token (e.g., "AGG")
140/// - `decimals`: Number of decimal places for the token
141/// - `max_supply`: Maximum supply of the token
142/// - `initial_supply`: Initial outstanding token supply (0 for new faucets)
143///
144/// # Returns
145/// Returns an [`AccountComponent`] configured for agglayer faucet operations.
146///
147/// # Panics
148/// Panics if the token symbol is invalid or metadata validation fails.
149fn create_agglayer_faucet_component(
150    token_symbol: &str,
151    decimals: u8,
152    max_supply: Felt,
153    initial_supply: Felt,
154) -> AccountComponent {
155    let symbol = TokenSymbol::new(token_symbol).expect("token symbol should be valid");
156    AggLayerFaucet::new(symbol, decimals, max_supply, initial_supply)
157        .expect("agglayer faucet metadata should be valid")
158        .into()
159}
160
161fn assert_basic_constant_fee_policy_manager(fee_policy_manager: &FeePolicyManager) {
162    let policy_root = BasicConstantFeePolicy::root();
163    assert_eq!(
164        fee_policy_manager.active_fee_policy(),
165        policy_root,
166        "AggLayer accounts require BasicConstantFeePolicy as the active fee policy"
167    );
168    assert_eq!(
169        fee_policy_manager.allowed_fee_policies().as_slice(),
170        &[policy_root],
171        "AggLayer accounts do not support additional fee policies"
172    );
173}
174
175impl AggLayerBridge {
176    /// Returns an [`AccountBuilder`] for a bridge account with the standard configuration.
177    ///
178    /// `bridge_admin` is the initial member of the bridge's built-in `ADMIN` role. The fee policy
179    /// manager must contain only an active [`BasicConstantFeePolicy`] with entries for
180    /// [`AggLayerBridge::allowed_notes`].
181    ///
182    /// # Panics
183    ///
184    /// Panics if the fee policy manager contains a different or additional fee policy.
185    pub fn account_builder(
186        seed: Word,
187        bridge_admin: AccountId,
188        roles: BridgeRoles,
189        network_id: u32,
190        fee_policy_manager: FeePolicyManager,
191    ) -> AccountBuilder {
192        assert_basic_constant_fee_policy_manager(&fee_policy_manager);
193        NetworkAccount::builder(seed.into(), AggLayerBridge::allowed_notes(), fee_policy_manager)
194            .expect("bridge note allowlist is non-empty")
195            .with_component(AggLayerBridge::new(network_id))
196            .with_component(
197                RoleBasedAccessControl::builder()
198                    .role(
199                        RoleConfig::new(RoleBasedAccessControl::admin_role())
200                            .with_member(bridge_admin),
201                    )
202                    .roles(roles)
203                    .build()
204                    .expect("the bridge seeds distinct non-empty roles administered by ADMIN"),
205            )
206            .with_component(Authority::RbacControlled {
207                procedure_roles: AggLayerBridge::procedure_roles(),
208            })
209            .with_component(Pausable::unpaused())
210            .with_component(PausableManager)
211            .with_component(ConstantFeeManager::for_basic_constant_fee_policy())
212    }
213}
214
215impl AggLayerFaucet {
216    /// Returns an [`AccountBuilder`] for a faucet account with the specified deployment
217    /// configuration.
218    ///
219    /// `faucet_admin` is the initial member of the faucet's built-in `ADMIN` role;
220    /// `bridge_account_id` is its [`Ownable2Step`] owner. The fee policy manager must contain only
221    /// an active [`BasicConstantFeePolicy`] with entries for [`AggLayerFaucet::allowed_notes`].
222    ///
223    /// # Panics
224    ///
225    /// Panics if the token metadata is invalid or the fee policy manager contains a different or
226    /// additional fee policy.
227    #[allow(clippy::too_many_arguments)]
228    pub fn account_builder(
229        seed: Word,
230        token_symbol: &str,
231        decimals: u8,
232        max_supply: Felt,
233        initial_supply: Felt,
234        faucet_admin: AccountId,
235        bridge_account_id: AccountId,
236        fee_policy_manager: FeePolicyManager,
237    ) -> AccountBuilder {
238        assert_basic_constant_fee_policy_manager(&fee_policy_manager);
239        let agglayer_component =
240            create_agglayer_faucet_component(token_symbol, decimals, max_supply, initial_supply);
241
242        let token_policy_manager = TokenPolicyManager::builder()
243            .active_mint_policy(MintPolicy::owner_only())
244            .active_burn_policy(BurnPolicy::owner_only())
245            .active_send_policy(TransferPolicy::allow_all())
246            .active_receive_policy(TransferPolicy::allow_all())
247            .build();
248
249        NetworkAccount::builder(seed.into(), AggLayerFaucet::allowed_notes(), fee_policy_manager)
250            .expect("faucet note allowlist is non-empty")
251            .with_component(agglayer_component)
252            .with_component(Ownable2Step::new(bridge_account_id))
253            .with_component(
254                RoleBasedAccessControl::with_admins([faucet_admin])
255                    .expect("the faucet seeds a non-empty ADMIN role"),
256            )
257            .with_component(Authority::RbacControlled { procedure_roles: BTreeMap::new() })
258            .with_components(token_policy_manager)
259            .with_component(ConstantFeeManager::for_basic_constant_fee_policy())
260    }
261}
262
263// TESTS
264// ================================================================================================
265
266#[cfg(test)]
267mod tests {
268    use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
269    use miden_standards::account::fees::FeePolicy;
270    use miden_standards::tx_script::ExpirationTransactionScript;
271
272    use super::*;
273    use crate::testing::{
274        create_existing_agglayer_faucet,
275        create_existing_bridge_account_with_roles,
276    };
277
278    /// Both agglayer network accounts allowlist the canonical [`ExpirationTransactionScript`],
279    /// which the network transaction builder attaches to every network transaction.
280    #[test]
281    fn agglayer_accounts_allowlist_expiration_tx_script() {
282        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
283
284        let bridge = create_existing_bridge_account_with_roles(Word::default(), id, id, id, id, 77);
285        let faucet = create_existing_agglayer_faucet(
286            Word::default(),
287            "AGG",
288            6,
289            Felt::from(1000u32),
290            Felt::ZERO,
291            id,
292        );
293
294        for account in [bridge, faucet] {
295            let network_account = NetworkAccount::try_from(account).unwrap();
296            assert!(network_account.allows_tx_script(&ExpirationTransactionScript::script_root()));
297        }
298    }
299
300    #[test]
301    #[should_panic(expected = "require BasicConstantFeePolicy")]
302    fn agglayer_accounts_reject_a_different_active_fee_policy() {
303        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
304        let policy = FeePolicy::custom(PausableManager::pause_root(), [PausableManager]).unwrap();
305        let manager =
306            FeePolicyManager::builder().fee_faucet_id(id).active_fee_policy(policy).build();
307
308        assert_basic_constant_fee_policy_manager(&manager);
309    }
310
311    #[test]
312    #[should_panic(expected = "do not support additional fee policies")]
313    fn agglayer_accounts_reject_additional_fee_policies() {
314        let id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE).unwrap();
315        let policy = FeePolicy::custom(PausableManager::pause_root(), [PausableManager]).unwrap();
316        let manager = FeePolicyManager::builder()
317            .fee_faucet_id(id)
318            .active_fee_policy(BasicConstantFeePolicy::new().into())
319            .allowed_fee_policy(policy)
320            .build();
321
322        assert_basic_constant_fee_policy_manager(&manager);
323    }
324}