diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aefc8673..393dcaa8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Refactored NTX Builder startup and introduced `NtxBuilderConfig` with configurable parameters ([#1610](https://github.com/0xMiden/miden-node/pull/1610)). - Refactored NTX Builder actor state into `AccountDeltaTracker` and `NotePool` for clarity, and added tracing instrumentation to event broadcasting ([#1611](https://github.com/0xMiden/miden-node/pull/1611)). - Add #[track_caller] to tracing/logging helpers ([#1651](https://github.com/0xMiden/miden-node/pull/1651)). +- Added support for generic account loading at genesis ([#1624](https://github.com/0xMiden/miden-node/pull/1624)). ## v0.13.5 (TBD) diff --git a/Cargo.lock b/Cargo.lock index 65360401b..48cf96a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2925,6 +2925,7 @@ dependencies = [ "futures", "hex", "indexmap 2.13.0", + "miden-agglayer", "miden-block-prover", "miden-crypto", "miden-node-proto", @@ -2940,6 +2941,7 @@ dependencies = [ "rand_chacha 0.9.0", "regex", "serde", + "tempfile", "termtree", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index caccabc5d..e15410822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ rand = { version = "0.9" } rand_chacha = { version = "0.9" } rstest = { version = "0.26" } serde = { features = ["derive"], version = "1" } +tempfile = { version = "3" } thiserror = { default-features = false, version = "2.0" } tokio = { features = ["rt-multi-thread"], version = "1.46" } tokio-stream = { version = "0.1" } diff --git a/bin/node/src/commands/store.rs b/bin/node/src/commands/store.rs index a78655cd9..e06693243 100644 --- a/bin/node/src/commands/store.rs +++ b/bin/node/src/commands/store.rs @@ -200,8 +200,7 @@ impl StoreCommand { // Parse genesis config (or default if not given). let config = genesis_config .map(|file_path| { - let toml_str = fs_err::read_to_string(file_path)?; - GenesisConfig::read_toml(toml_str.as_str()).with_context(|| { + GenesisConfig::read_toml_file(file_path).with_context(|| { format!("failed to parse genesis config from file {}", file_path.display()) }) }) diff --git a/crates/block-producer/Cargo.toml b/crates/block-producer/Cargo.toml index 023a7a448..474190ca6 100644 --- a/crates/block-producer/Cargo.toml +++ b/crates/block-producer/Cargo.toml @@ -52,6 +52,6 @@ pretty_assertions = "1.4" rand_chacha = { default-features = false, version = "0.9" } rstest = { workspace = true } serial_test = "3.2" -tempfile = { version = "3.20" } +tempfile = { workspace = true } tokio = { features = ["test-util"], workspace = true } winterfell = { version = "0.13" } diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 30ec4dcb8..654c7e29b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -44,4 +44,4 @@ miden-protocol = { default-features = true, features = ["testing"], workspace miden-standards = { workspace = true } reqwest = { version = "0.12" } rstest = { workspace = true } -tempfile = { version = "3.20" } +tempfile = { workspace = true } diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index bbdc9ef41..c8ee4b093 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -50,7 +50,10 @@ tracing = { workspace = true } url = { workspace = true } [build-dependencies] +fs-err = { workspace = true } +miden-agglayer = { branch = "next", features = ["testing"], git = "https://github.com/0xMiden/miden-base" } miden-node-rocksdb-cxx-linkage-fix = { workspace = true } +miden-protocol = { features = ["std"], workspace = true } [dev-dependencies] assert_matches = { workspace = true } @@ -62,6 +65,7 @@ miden-protocol = { default-features = true, features = ["testing"], works miden-standards = { features = ["testing"], workspace = true } rand = { workspace = true } regex = { version = "1.11" } +tempfile = { workspace = true } termtree = { version = "0.5" } [features] diff --git a/crates/store/build.rs b/crates/store/build.rs index a911bea19..bf19fbcb4 100644 --- a/crates/store/build.rs +++ b/crates/store/build.rs @@ -1,6 +1,13 @@ // This build.rs is required to trigger the `diesel_migrations::embed_migrations!` proc-macro in // `store/src/db/migrations.rs` to include the latest version of the migrations into the binary, see . +use std::path::PathBuf; +use std::sync::Arc; + +use miden_agglayer::{create_existing_agglayer_faucet, create_existing_bridge_account}; +use miden_protocol::account::{Account, AccountCode, AccountFile}; +use miden_protocol::{Felt, Word}; + fn main() { println!("cargo:rerun-if-changed=./src/db/migrations"); // If we do one re-write, the default rules are disabled, @@ -8,5 +15,92 @@ fn main() { // println!("cargo:rerun-if-changed=Cargo.toml"); + // Generate sample agglayer account files for genesis config samples. + generate_agglayer_sample_accounts(); miden_node_rocksdb_cxx_linkage_fix::configure(); } + +/// Generates sample agglayer account files for the `02-with-account-files` genesis config sample. +/// +/// Creates: +/// - `bridge.mac` - agglayer bridge account +/// - `agglayer_faucet_eth.mac` - agglayer faucet for wrapped ETH +/// - `agglayer_faucet_usdc.mac` - agglayer faucet for wrapped USDC +fn generate_agglayer_sample_accounts() { + // Use CARGO_MANIFEST_DIR to get the absolute path to the crate root + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"); + let samples_dir: PathBuf = + [&manifest_dir, "src", "genesis", "config", "samples", "02-with-account-files"] + .iter() + .collect(); + + // Create the directory if it doesn't exist + fs_err::create_dir_all(&samples_dir).expect("Failed to create samples directory"); + + // Use deterministic seeds for reproducible builds + // WARNING: DO NOT USE THIS IN PRODUCTION + let bridge_seed: Word = Word::new([Felt::new(1u64); 4]); + let eth_faucet_seed: Word = Word::new([Felt::new(2u64); 4]); + let usdc_faucet_seed: Word = Word::new([Felt::new(3u64); 4]); + + // Create the bridge account first (faucets need to reference it) + // Use "existing" variant so accounts have nonce > 0 (required for genesis) + let bridge_account = create_existing_bridge_account(bridge_seed); + let bridge_account_id = bridge_account.id(); + + // Create AggLayer faucets using "existing" variant + // ETH: 18 decimals, max supply of 1 billion tokens + let eth_faucet = create_existing_agglayer_faucet( + eth_faucet_seed, + "ETH", + 18, + Felt::new(1_000_000_000), + bridge_account_id, + ); + + // USDC: 6 decimals, max supply of 10 billion tokens + let usdc_faucet = create_existing_agglayer_faucet( + usdc_faucet_seed, + "USDC", + 6, + Felt::new(10_000_000_000), + bridge_account_id, + ); + + // Strip source location decorators from account code to ensure deterministic output. + let bridge_account = strip_code_decorators(bridge_account); + let eth_faucet = strip_code_decorators(eth_faucet); + let usdc_faucet = strip_code_decorators(usdc_faucet); + + // Save account files (without secret keys since these use NoAuth) + let bridge_file = AccountFile::new(bridge_account, vec![]); + let eth_faucet_file = AccountFile::new(eth_faucet, vec![]); + let usdc_faucet_file = AccountFile::new(usdc_faucet, vec![]); + + // Write files + bridge_file + .write(samples_dir.join("bridge.mac")) + .expect("Failed to write bridge.mac"); + eth_faucet_file + .write(samples_dir.join("agglayer_faucet_eth.mac")) + .expect("Failed to write agglayer_faucet_eth.mac"); + usdc_faucet_file + .write(samples_dir.join("agglayer_faucet_usdc.mac")) + .expect("Failed to write agglayer_faucet_usdc.mac"); +} + +/// Strips source location decorators from an account's code MAST forest. +/// +/// This is necessary because the MAST forest embeds absolute file paths from the Cargo build +/// directory, which include a hash that differs between `cargo check` and `cargo build`. Stripping +/// decorators ensures the serialized `.mac` files are identical regardless of which cargo command +/// is used (CI or local builds or tests). +fn strip_code_decorators(account: Account) -> Account { + let (id, vault, storage, code, nonce, seed) = account.into_parts(); + + let mut mast = code.mast(); + Arc::make_mut(&mut mast).strip_decorators(); + let code = AccountCode::from_parts(mast, code.procedures().to_vec()); + + Account::new_unchecked(id, vault, storage, code, nonce, seed) +} diff --git a/crates/store/src/genesis/config/errors.rs b/crates/store/src/genesis/config/errors.rs index b39495c87..c7a07f1c7 100644 --- a/crates/store/src/genesis/config/errors.rs +++ b/crates/store/src/genesis/config/errors.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use miden_protocol::account::AccountId; use miden_protocol::errors::{ AccountDeltaError, @@ -17,13 +19,21 @@ use crate::genesis::config::TokenSymbolStr; pub enum GenesisConfigError { #[error(transparent)] Toml(#[from] toml::de::Error), + #[error("failed to read config file at {path}: {reason}")] + ConfigFileRead { path: PathBuf, reason: String }, + #[error("failed to read account file at {path}: {reason}")] + AccountFileRead { path: PathBuf, reason: String }, + #[error("native faucet from file {path} is not a fungible faucet")] + NativeFaucetNotFungible { path: PathBuf }, #[error("account translation from config to state failed")] Account(#[from] AccountError), #[error("asset translation from config to state failed")] Asset(#[from] AssetError), #[error("adding assets to account failed")] AccountDelta(#[from] AccountDeltaError), - #[error("the defined asset {symbol:?} has no corresponding faucet")] + #[error( + "the defined asset {symbol:?} has no corresponding faucet, or the faucet was provided as an account file" + )] MissingFaucetDefinition { symbol: TokenSymbolStr }, #[error("account with id {account_id} was referenced but is not part of given genesis state")] MissingGenesisAccount { account_id: AccountId }, diff --git a/crates/store/src/genesis/config/mod.rs b/crates/store/src/genesis/config/mod.rs index e7abe8b58..1277037b3 100644 --- a/crates/store/src/genesis/config/mod.rs +++ b/crates/store/src/genesis/config/mod.rs @@ -1,6 +1,7 @@ //! Describe a subset of the genesis manifest in easily human readable format use std::cmp::Ordering; +use std::path::Path; use std::str::FromStr; use indexmap::IndexMap; @@ -42,22 +43,61 @@ use self::errors::GenesisConfigError; #[cfg(test)] mod tests; +// TOML PARSING STRUCTS (INTERMEDIATE) +// ================================================================================================ + +/// Intermediate struct for TOML parsing - native faucet can be params or file path +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(untagged)] +enum NativeFaucetToml { + /// Build from parameters (dev/testing) + Parameters { + symbol: TokenSymbolStr, + decimals: u8, + max_supply: u64, + }, + /// Load from pre-built account file (production/multisig) + File { path: std::path::PathBuf }, +} + +/// Intermediate struct for TOML parsing - arbitrary account from file +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct AccountToml { + /// Path to .mac file (relative to genesis config directory) + path: std::path::PathBuf, +} + +/// Intermediate struct for full TOML parsing +#[derive(Debug, Clone, serde::Deserialize)] +struct GenesisConfigToml { + version: u32, + timestamp: u32, + native_faucet: NativeFaucetToml, + fee_parameters: FeeParameterConfig, + #[serde(default)] + wallet: Vec, + #[serde(default)] + fungible_faucet: Vec, + #[serde(default)] + account: Vec, +} + // GENESIS CONFIG // ================================================================================================ /// Specify a set of faucets and wallets with assets for easier test deployments. /// /// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone)] pub struct GenesisConfig { version: u32, timestamp: u32, native_faucet: NativeFaucet, fee_parameters: FeeParameterConfig, - #[serde(default)] wallet: Vec, - #[serde(default)] fungible_faucet: Vec, + accounts: Vec, } impl Default for GenesisConfig { @@ -73,24 +113,92 @@ impl Default for GenesisConfig { ) .expect("Timestamp should fit into u32"), wallet: vec![], - native_faucet: NativeFaucet { + native_faucet: NativeFaucet::Parameters { max_supply: 100_000_000_000_000_000u64, decimals: 6u8, symbol: miden.clone(), }, fee_parameters: FeeParameterConfig { verification_base_fee: 0 }, fungible_faucet: vec![], + accounts: vec![], } } } impl GenesisConfig { - /// Read the genesis accounts from a toml formatted string + /// Read the genesis config from a TOML file. + /// + /// The parent directory of `path` is used to resolve relative paths for account files + /// referenced in the configuration (e.g., `[[account]]` entries with `path` fields). /// /// Notice: It will generate the specified case during [`fn into_state`]. - pub fn read_toml(toml_str: &str) -> Result { - let me = toml::from_str::(toml_str)?; - Ok(me) + pub fn read_toml_file(path: &Path) -> Result { + let toml_str = + fs_err::read_to_string(path).map_err(|e| GenesisConfigError::ConfigFileRead { + path: path.to_path_buf(), + reason: e.to_string(), + })?; + let config_dir = path.parent().unwrap_or_else(|| Path::new(".")); + Self::read_toml(&toml_str, config_dir) + } + + /// Read the genesis accounts from a TOML formatted string. + /// + /// The `config_dir` parameter is used to resolve relative paths for account files + /// referenced in the configuration (e.g., `[[account]]` entries with `path` fields). + fn read_toml(toml_str: &str, config_dir: &Path) -> Result { + // Parse TOML into intermediate struct + let toml_config: GenesisConfigToml = toml::from_str(toml_str)?; + + // Handle native faucet (params or file) + let native_faucet = match toml_config.native_faucet { + NativeFaucetToml::Parameters { symbol, decimals, max_supply } => { + NativeFaucet::Parameters { symbol, decimals, max_supply } + }, + NativeFaucetToml::File { path } => { + let full_path = config_dir.join(&path); + let account_file = AccountFile::read(&full_path).map_err(|e| { + GenesisConfigError::AccountFileRead { + path: full_path.clone(), + reason: e.to_string(), + } + })?; + let account = account_file.account; + + // Validate it's a fungible faucet + if account.id().account_type() != AccountType::FungibleFaucet { + return Err(GenesisConfigError::NativeFaucetNotFungible { path: full_path }); + } + + NativeFaucet::Account { account: Box::new(account) } + }, + }; + + // Load all account files + let accounts = toml_config + .account + .into_iter() + .map(|acc| { + let full_path = config_dir.join(&acc.path); + let account_file = AccountFile::read(&full_path).map_err(|e| { + GenesisConfigError::AccountFileRead { + path: full_path.clone(), + reason: e.to_string(), + } + })?; + Ok(account_file.account) + }) + .collect::, GenesisConfigError>>()?; + + Ok(Self { + version: toml_config.version, + timestamp: toml_config.timestamp, + native_faucet, + fee_parameters: toml_config.fee_parameters, + wallet: toml_config.wallet, + fungible_faucet: toml_config.fungible_faucet, + accounts, + }) } /// Convert the in memory representation into the new genesis state @@ -108,10 +216,10 @@ impl GenesisConfig { fee_parameters, fungible_faucet: fungible_faucet_configs, wallet: wallet_configs, - .. + accounts: file_loaded_accounts, } = self; - let symbol = native_faucet.symbol.clone(); + let symbol = native_faucet.symbol(); let mut wallet_accounts = Vec::::new(); // Every asset sitting in a wallet, has to reference a faucet for that asset @@ -121,10 +229,26 @@ impl GenesisConfig { // accounts/sign transactions let mut secrets = Vec::new(); - // First setup all the faucets - for fungible_faucet_config in std::iter::once(native_faucet.to_faucet_config()) - .chain(fungible_faucet_configs.into_iter()) - { + // Handle native faucet: use pre-loaded account if available, otherwise build from params + let native_faucet_account = match native_faucet { + NativeFaucet::Parameters { .. } => { + let (faucet_account, secret_key) = + native_faucet.to_faucet_config().build_account()?; + faucet_accounts.insert(symbol.clone(), faucet_account.clone()); + secrets.push(( + format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()), + faucet_account.id(), + secret_key, + )); + faucet_account + }, + NativeFaucet::Account { account } => *account, + }; + let native_faucet_account_id = native_faucet_account.id(); + faucet_accounts.insert(symbol.clone(), native_faucet_account); + + // Setup additional fungible faucets from parameters + for fungible_faucet_config in fungible_faucet_configs { let symbol = fungible_faucet_config.symbol.clone(); let (faucet_account, secret_key) = fungible_faucet_config.build_account()?; @@ -141,11 +265,6 @@ impl GenesisConfig { // we know the remaining supply in the faucets. } - let native_faucet_account_id = faucet_accounts - .get(&symbol) - .expect("Parsing guarantees the existence of a native faucet.") - .id(); - let fee_parameters = FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee)?; @@ -264,6 +383,9 @@ impl GenesisConfig { // Ensure the faucets always precede the wallets referencing them all_accounts.extend(wallet_accounts); + // Append file-loaded accounts as-is + all_accounts.extend(file_loaded_accounts); + Ok(( GenesisState { fee_parameters, @@ -281,28 +403,46 @@ impl GenesisConfig { // ================================================================================================ /// Declare the native fungible asset -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(deny_unknown_fields)] -pub struct NativeFaucet { - /// Token symbol to use for fees. - symbol: TokenSymbolStr, - - decimals: u8, - /// Max supply in full token units - /// - /// It will be converted internally to the smallest representable unit, - /// using based `10.powi(decimals)` as a multiplier. - max_supply: u64, +#[derive(Debug, Clone)] +enum NativeFaucet { + Parameters { + symbol: TokenSymbolStr, + decimals: u8, + max_supply: u64, + }, + Account { + account: Box, + }, } impl NativeFaucet { fn to_faucet_config(&self) -> FungibleFaucetConfig { - let NativeFaucet { symbol, decimals, max_supply, .. } = self; - FungibleFaucetConfig { - symbol: symbol.clone(), - decimals: *decimals, - max_supply: *max_supply, - storage_mode: StorageMode::Public, + match self { + NativeFaucet::Parameters { symbol, decimals, max_supply } => FungibleFaucetConfig { + symbol: symbol.clone(), + decimals: *decimals, + max_supply: *max_supply, + storage_mode: StorageMode::Public, + }, + NativeFaucet::Account { .. } => { + panic!( + "conversion to fungible faucet config should not happen when an account file is provided" + ); + }, + } + } +} + +impl NativeFaucet { + fn symbol(&self) -> TokenSymbolStr { + match self { + NativeFaucet::Parameters { symbol, .. } => symbol.clone(), + NativeFaucet::Account { account } => { + // this is safe since we validate the account type when reading the genesis config + let faucet = BasicFungibleFaucet::try_from(account.as_ref()) + .expect("native faucet account should be a fungible faucet"); + TokenSymbolStr::from(faucet.symbol()) + }, } } } @@ -548,6 +688,14 @@ impl From for TokenSymbol { } } +impl From for TokenSymbolStr { + fn from(symbol: TokenSymbol) -> Self { + // TokenSymbol guarantees valid format, so to_string should not fail + let raw = symbol.to_string().expect("TokenSymbol should always produce valid string"); + Self { raw, encoded: symbol } + } +} + impl Ord for TokenSymbolStr { fn cmp(&self, other: &Self) -> Ordering { self.raw.cmp(&other.raw) diff --git a/crates/store/src/genesis/config/samples/02-with-account-files.toml b/crates/store/src/genesis/config/samples/02-with-account-files.toml new file mode 100644 index 000000000..c5d59896b --- /dev/null +++ b/crates/store/src/genesis/config/samples/02-with-account-files.toml @@ -0,0 +1,36 @@ +# Genesis configuration example with AggLayer account files +# +# This example demonstrates how to include pre-built accounts from .mac files +# in the genesis block. The account files are generated by the build script +# using deterministic seeds for reproducibility. +# They demonstrate interdependencies between accounts: +# - bridge.mac: AggLayer bridge account for cross-chain asset transfers +# - agglayer_faucet_eth.mac: AggLayer faucet for wrapped ETH, depends on the bridge account. +# - agglayer_faucet_usdc.mac: AggLayer faucet for wrapped USDC, depends on the bridge account. +# +# Paths are relative to the directory containing this configuration file. + +timestamp = 1717344256 +version = 1 + +[fee_parameters] +verification_base_fee = 0 + +# Native faucet for the chain's native token (e.g., MIDEN) +# This uses parameters to build a standard fungible faucet +[native_faucet] +decimals = 6 +max_supply = 100_000_000_000 +symbol = "MIDEN" + +# AggLayer bridge account for bridging assets to/from AggLayer +[[account]] +path = "02-with-account-files/bridge.mac" + +# AggLayer ETH faucet for wrapped ETH tokens +[[account]] +path = "02-with-account-files/agglayer_faucet_eth.mac" + +# AggLayer USDC faucet for wrapped USDC tokens +[[account]] +path = "02-with-account-files/agglayer_faucet_usdc.mac" diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac new file mode 100644 index 000000000..ed79a49b1 Binary files /dev/null and b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_eth.mac differ diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac new file mode 100644 index 000000000..13c71956c Binary files /dev/null and b/crates/store/src/genesis/config/samples/02-with-account-files/agglayer_faucet_usdc.mac differ diff --git a/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac b/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac new file mode 100644 index 000000000..57b462715 Binary files /dev/null and b/crates/store/src/genesis/config/samples/02-with-account-files/bridge.mac differ diff --git a/crates/store/src/genesis/config/tests.rs b/crates/store/src/genesis/config/tests.rs index 23e2daa43..8d9848efc 100644 --- a/crates/store/src/genesis/config/tests.rs +++ b/crates/store/src/genesis/config/tests.rs @@ -1,3 +1,6 @@ +use std::io::Write; +use std::path::Path; + use assert_matches::assert_matches; use miden_protocol::ONE; use miden_protocol::crypto::dsa::ecdsa_k256_keccak::SecretKey; @@ -6,11 +9,23 @@ use super::*; type TestResult = Result<(), Box>; +/// Helper to write TOML content to a file and return the path +fn write_toml_file(dir: &Path, content: &str) -> std::path::PathBuf { + let path = dir.join("genesis.toml"); + let mut file = std::fs::File::create(&path).unwrap(); + file.write_all(content.as_bytes()).unwrap(); + path +} + #[test] #[miden_node_test_macro::enable_logging] fn parsing_yields_expected_default_values() -> TestResult { - let s = include_str!("./samples/01-simple.toml"); - let gcfg = GenesisConfig::read_toml(s)?; + // Copy sample file to temp dir since read_toml_file needs a real file path + let temp_dir = tempfile::tempdir()?; + let sample_content = include_str!("./samples/01-simple.toml"); + let config_path = write_toml_file(temp_dir.path(), sample_content); + + let gcfg = GenesisConfig::read_toml_file(&config_path)?; let (state, _secrets) = gcfg.into_state(SecretKey::new())?; let _ = state; // faucets always precede wallet accounts @@ -67,3 +82,304 @@ fn genesis_accounts_have_nonce_one() -> TestResult { let _block = state.into_block()?; Ok(()) } + +#[test] +fn parsing_account_from_file() -> TestResult { + use miden_protocol::account::{AccountFile, AccountStorageMode, AccountType}; + use miden_standards::AuthScheme; + use miden_standards::account::wallets::create_basic_wallet; + use tempfile::tempdir; + + // Create a temporary directory for our test files + let temp_dir = tempdir()?; + let config_dir = temp_dir.path(); + + // Create a test wallet account and save it to a .mac file + let init_seed: [u8; 32] = rand::random(); + let mut rng = rand_chacha::ChaCha20Rng::from_seed(rand::random()); + let secret_key = miden_protocol::crypto::dsa::falcon512_rpo::SecretKey::with_rng( + &mut miden_node_utils::crypto::get_rpo_random_coin(&mut rng), + ); + let auth = AuthScheme::Falcon512Rpo { pub_key: secret_key.public_key().into() }; + + let test_account = create_basic_wallet( + init_seed, + auth, + AccountType::RegularAccountUpdatableCode, + AccountStorageMode::Public, + )?; + + let account_id = test_account.id(); + + // Save to file + let account_file_path = config_dir.join("test_account.mac"); + let account_file = AccountFile::new(test_account, vec![]); + account_file.write(&account_file_path)?; + + // Create a genesis config TOML that references the account file + let toml_content = r#" +timestamp = 1717344256 +version = 1 + +[native_faucet] +decimals = 6 +max_supply = 100_000_000 +symbol = "TEST" + +[fee_parameters] +verification_base_fee = 0 + +[[account]] +path = "test_account.mac" +"#; + let config_path = write_toml_file(config_dir, toml_content); + + // Parse the config + let gcfg = GenesisConfig::read_toml_file(&config_path)?; + + // Convert to state and verify the account is included + let (state, _secrets) = gcfg.into_state(SecretKey::new())?; + assert!(state.accounts.iter().any(|a| a.id() == account_id)); + + Ok(()) +} + +#[test] +fn parsing_native_faucet_from_file() -> TestResult { + use miden_protocol::account::{AccountBuilder, AccountFile, AccountStorageMode, AccountType}; + use miden_standards::account::auth::AuthFalcon512Rpo; + use tempfile::tempdir; + + // Create a temporary directory for our test files + let temp_dir = tempdir()?; + let config_dir = temp_dir.path(); + + // Create a faucet account and save it to a .mac file + let init_seed: [u8; 32] = rand::random(); + let mut rng = rand_chacha::ChaCha20Rng::from_seed(rand::random()); + let secret_key = miden_protocol::crypto::dsa::falcon512_rpo::SecretKey::with_rng( + &mut miden_node_utils::crypto::get_rpo_random_coin(&mut rng), + ); + let auth = AuthFalcon512Rpo::new(secret_key.public_key().into()); + + let faucet_component = + BasicFungibleFaucet::new(TokenSymbol::new("MIDEN").unwrap(), 6, Felt::new(1_000_000_000))?; + + let faucet_account = AccountBuilder::new(init_seed) + .account_type(AccountType::FungibleFaucet) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(auth) + .with_component(faucet_component) + .build()?; + + let faucet_id = faucet_account.id(); + + // Save to file + let faucet_file_path = config_dir.join("native_faucet.mac"); + let account_file = AccountFile::new(faucet_account, vec![]); + account_file.write(&faucet_file_path)?; + + // Create a genesis config TOML that references the faucet file + let toml_content = r#" +timestamp = 1717344256 +version = 1 + +[native_faucet] +path = "native_faucet.mac" + +[fee_parameters] +verification_base_fee = 0 +"#; + let config_path = write_toml_file(config_dir, toml_content); + + // Parse the config + let gcfg = GenesisConfig::read_toml_file(&config_path)?; + + // Convert to state and verify the native faucet is included + let (state, secrets) = gcfg.into_state(SecretKey::new())?; + assert!(state.accounts.iter().any(|a| a.id() == faucet_id)); + + // No secrets should be generated for file-loaded native faucet + assert!(secrets.secrets.is_empty()); + + Ok(()) +} + +#[test] +fn native_faucet_from_file_must_be_faucet_type() -> TestResult { + use miden_protocol::account::{AccountFile, AccountStorageMode, AccountType}; + use miden_standards::AuthScheme; + use miden_standards::account::wallets::create_basic_wallet; + use tempfile::tempdir; + + // Create a temporary directory for our test files + let temp_dir = tempdir()?; + let config_dir = temp_dir.path(); + + // Create a regular wallet account (not a faucet) and try to use it as native faucet + let init_seed: [u8; 32] = rand::random(); + let mut rng = rand_chacha::ChaCha20Rng::from_seed(rand::random()); + let secret_key = miden_protocol::crypto::dsa::falcon512_rpo::SecretKey::with_rng( + &mut miden_node_utils::crypto::get_rpo_random_coin(&mut rng), + ); + let auth = AuthScheme::Falcon512Rpo { pub_key: secret_key.public_key().into() }; + + let regular_account = create_basic_wallet( + init_seed, + auth, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Public, + )?; + + // Save to file + let account_file_path = config_dir.join("not_a_faucet.mac"); + let account_file = AccountFile::new(regular_account, vec![]); + account_file.write(&account_file_path)?; + + // Create a genesis config TOML that tries to use a non-faucet as native faucet + let toml_content = r#" +timestamp = 1717344256 +version = 1 + +[native_faucet] +path = "not_a_faucet.mac" + +[fee_parameters] +verification_base_fee = 0 +"#; + let config_path = write_toml_file(config_dir, toml_content); + + // Parse should fail with NativeFaucetNotFungible error + let result = GenesisConfig::read_toml_file(&config_path); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, GenesisConfigError::NativeFaucetNotFungible { .. }), + "Expected NativeFaucetNotFungible error, got: {err:?}" + ); + + Ok(()) +} + +#[test] +fn missing_account_file_returns_error() { + // Create a genesis config TOML that references a non-existent file + let toml_content = r#" +timestamp = 1717344256 +version = 1 + +[native_faucet] +decimals = 6 +max_supply = 100_000_000 +symbol = "TEST" + +[fee_parameters] +verification_base_fee = 0 + +[[account]] +path = "does_not_exist.mac" +"#; + + // Use temp dir as config dir + let temp_dir = tempfile::tempdir().unwrap(); + let config_path = write_toml_file(temp_dir.path(), toml_content); + let result = GenesisConfig::read_toml_file(&config_path); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!( + matches!(err, GenesisConfigError::AccountFileRead { .. }), + "Expected AccountFileRead error, got: {err:?}" + ); +} + +#[test] +fn missing_native_faucet_not_allowed() -> TestResult { + let toml_content = r" +timestamp = 1717344256 +version = 1 + +[fee_parameters] +verification_base_fee = 0 +"; + + let temp_dir = tempfile::tempdir()?; + let config_path = write_toml_file(temp_dir.path(), toml_content); + let result = GenesisConfig::read_toml_file(&config_path); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_matches!(err, GenesisConfigError::Toml(toml_err) => { + let msg = toml_err.message(); + assert!( + msg.contains("missing field `native_faucet`"), + "Expected error message to mention 'native_faucet', got: {msg}" + ); + }); + Ok(()) +} + +#[test] +#[miden_node_test_macro::enable_logging] +fn parsing_agglayer_sample_with_account_files() -> TestResult { + use miden_protocol::account::AccountType; + + // Use the actual sample file path since it references relative .mac files + let sample_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/genesis/config/samples/02-with-account-files.toml"); + + let gcfg = GenesisConfig::read_toml_file(&sample_path)?; + let (state, secrets) = gcfg.into_state(SecretKey::new())?; + + // Should have 4 accounts: + // 1. Native faucet (MIDEN) - built from parameters + // 2. Bridge account (bridge.mac) - loaded from file + // 3. ETH faucet (agglayer_faucet_eth.mac) - loaded from file + // 4. USDC faucet (agglayer_faucet_usdc.mac) - loaded from file + assert_eq!(state.accounts.len(), 4, "Expected 4 accounts in genesis state"); + + // Verify account types + let native_faucet = &state.accounts[0]; + let bridge_account = &state.accounts[1]; + let eth_faucet = &state.accounts[2]; + let usdc_faucet = &state.accounts[3]; + + // Native faucet should be a fungible faucet (built from parameters) + assert_eq!( + native_faucet.id().account_type(), + AccountType::FungibleFaucet, + "Native faucet should be a FungibleFaucet" + ); + + // Verify native faucet symbol + { + let faucet = BasicFungibleFaucet::try_from(native_faucet.clone()).unwrap(); + assert_eq!(faucet.symbol(), TokenSymbol::new("MIDEN").unwrap()); + } + + // Bridge account is a regular account (not a faucet) + assert!( + bridge_account.is_regular_account(), + "Bridge account should be a regular account" + ); + + // ETH faucet should be a fungible faucet (AggLayer faucet loaded from file) + assert_eq!( + eth_faucet.id().account_type(), + AccountType::FungibleFaucet, + "ETH faucet should be a FungibleFaucet" + ); + + // USDC faucet should be a fungible faucet (AggLayer faucet loaded from file) + assert_eq!( + usdc_faucet.id().account_type(), + AccountType::FungibleFaucet, + "USDC faucet should be a FungibleFaucet" + ); + + // Only the native faucet generates a secret (built from parameters) + assert_eq!(secrets.secrets.len(), 1, "Only native faucet should generate a secret"); + + // Verify the genesis state can be converted to a block + let _block = state.into_block()?; + + Ok(()) +}