1use std::cmp::Ordering;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_node_tracing::debug;
9use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
10use miden_protocol::account::{Account, AccountBuilder, AccountFile, AccountId, AccountType};
11use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol};
12use miden_protocol::block::{FeeParameters, ValidatorKeys};
13use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
14use miden_protocol::errors::TokenSymbolError;
15use miden_protocol::{Felt, ONE};
16use miden_standards::account::auth::{Approver, AuthSingleSig, NetworkAccountNoteAllowlist};
17use miden_standards::account::faucets::{
18 FungibleFaucet,
19 TokenName,
20 create_native_fungible_faucet_for_genesis,
21};
22use miden_standards::account::fees::BasicConstantFeePolicy;
23use miden_standards::account::policies::{
24 BurnPolicy,
25 MintPolicy,
26 TokenPolicyManager,
27 TransferPolicy,
28};
29use miden_standards::account::wallets::create_basic_wallet;
30use miden_standards::note::{BurnNote, FeeSponsorshipNote, MintNote};
31use rand::distr::weighted::Weight;
32use rand::{RngExt, SeedableRng};
33use rand_chacha::ChaCha20Rng;
34use serde::{Deserialize, Serialize};
35
36use crate::{GenesisState, LOG_TARGET};
37
38mod errors;
39use self::errors::GenesisConfigError;
40
41#[cfg(test)]
42mod tests;
43
44const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
45const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
46const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
47const DEFAULT_FAUCET_OPERATOR_BALANCE: u64 = 1_000_000_000;
49
50pub const NATIVE_FAUCET_FILE_NAME: &str = "native_faucet.mac";
52
53pub const FAUCET_OPERATOR_FILE_NAME: &str = "faucet_operator.mac";
55
56#[derive(Debug, Clone, serde::Deserialize)]
64#[serde(deny_unknown_fields)]
65struct GenericAccountConfig {
66 path: PathBuf,
67}
68
69#[derive(Debug, Clone, serde::Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct GenesisConfig {
75 version: u32,
76 timestamp: u32,
77 #[serde(default)]
91 native_faucet: Option<PathBuf>,
92 fee_parameters: FeeParameterConfig,
93 #[serde(default)]
94 wallet: Vec<WalletConfig>,
95 #[serde(default)]
96 fungible_faucet: Vec<FungibleFaucetConfig>,
97 #[serde(default)]
98 account: Vec<GenericAccountConfig>,
99 #[serde(skip)]
100 config_dir: PathBuf,
101}
102
103impl Default for GenesisConfig {
104 fn default() -> Self {
105 Self {
106 version: 1_u32,
107 timestamp: u32::try_from(
108 std::time::SystemTime::now()
109 .duration_since(std::time::UNIX_EPOCH)
110 .expect("Time does not go backwards")
111 .as_secs(),
112 )
113 .expect("Timestamp should fit into u32"),
114 wallet: vec![],
115 native_faucet: None,
116 fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
117 fungible_faucet: vec![],
118 account: vec![],
119 config_dir: PathBuf::from("."),
120 }
121 }
122}
123
124impl GenesisConfig {
125 pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
132 let toml_str = fs_err::read_to_string(path)
133 .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
134 let config_dir = path.parent().expect("config file path must have a parent directory");
135 Self::read_toml(&toml_str, config_dir)
136 }
137
138 fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
144 let mut config: Self = toml::from_str(toml_str)?;
145 config.config_dir = config_dir.to_path_buf();
146 Ok(config)
147 }
148
149 #[expect(clippy::too_many_lines)]
157 pub fn into_state(
158 self,
159 validator_keys: ValidatorKeys,
160 ) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
161 let GenesisConfig {
162 version,
163 timestamp,
164 native_faucet,
165 fee_parameters,
166 fungible_faucet: fungible_faucet_configs,
167 wallet: wallet_configs,
168 account: account_entries,
169 config_dir,
170 } = self;
171
172 let file_loaded_accounts = account_entries
174 .into_iter()
175 .map(|acc| {
176 let full_path = config_dir.join(&acc.path);
177 let account_file = AccountFile::read(&full_path)
178 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
179 Ok(account_file.account)
180 })
181 .collect::<Result<Vec<_>, GenesisConfigError>>()?;
182
183 let mut wallet_accounts = Vec::<Account>::new();
184 let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
186
187 let mut secrets = Vec::new();
190
191 let NativeFaucet {
193 account: native_faucet_account,
194 symbol,
195 operator,
196 } = NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
197 let native_faucet_account_id = native_faucet_account.id();
198
199 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
203 if operator.is_some() {
204 faucet_issuance.insert(native_faucet_account_id, DEFAULT_FAUCET_OPERATOR_BALANCE);
205 }
206
207 let operator_account = match operator {
208 Some((operator, operator_secret)) => {
209 secrets.push((
213 NATIVE_FAUCET_FILE_NAME.to_string(),
214 native_faucet_account.id(),
215 None,
216 ));
217 secrets.push((
218 FAUCET_OPERATOR_FILE_NAME.to_string(),
219 operator.id(),
220 Some(operator_secret),
221 ));
222 Some(operator)
223 },
224 None => None,
225 };
226
227 faucet_accounts.insert(symbol.clone(), native_faucet_account);
228
229 for fungible_faucet_config in fungible_faucet_configs {
231 let symbol = fungible_faucet_config.symbol.clone();
232 let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
233
234 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
235 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
236 }
237
238 secrets.push((
239 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
240 faucet_account.id(),
241 Some(secret_key),
242 ));
243 }
246
247 let fee_parameters =
248 FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee);
249
250 let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
251
252 for (index, WalletConfig { account_type, assets }) in wallet_configs.into_iter().enumerate()
254 {
255 debug!(
256 target: LOG_TARGET,
257 "Adding wallet account",
258 account.index = index,
259 account.assets.count = assets.len()
260 );
261
262 let mut rng = ChaCha20Rng::from_seed(rand::random());
263 let secret_key = RpoSecretKey::with_rng(&mut rng);
264 let auth =
265 Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
266 let init_seed: [u8; 32] = rng.random();
267
268 let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
269
270 let wallet_assets =
272 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
273 for asset in wallet_assets {
274 wallet_account.vault_mut().add_asset(asset)?;
275 }
276
277 wallet_account.set_nonce(ONE)?;
286
287 debug_assert_eq!(wallet_account.nonce(), ONE);
288
289 secrets.push((
290 format!("wallet_{index:0zero_padding_width$}.mac"),
291 wallet_account.id(),
292 Some(secret_key),
293 ));
294
295 wallet_accounts.push(wallet_account);
296 }
297
298 let mut all_accounts = Vec::<Account>::new();
299 for (symbol, mut faucet_account) in faucet_accounts {
301 let faucet_id = faucet_account.id();
302 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
304
305 if total_issuance != 0 {
306 let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
307 let new_token_supply = AssetAmount::new(total_issuance)?;
308 let max_supply = current_faucet.max_supply().as_u64();
309 if max_supply < total_issuance {
310 return Err(GenesisConfigError::MaxIssuanceExceeded {
311 max_supply,
312 symbol: symbol.clone(),
313 total_issuance,
314 });
315 }
316 let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
317 let slot = updated_faucet.token_config_slot_value();
318 faucet_account.storage_mut().set_item(slot.name(), slot.value())?;
319 debug!(
320 target: LOG_TARGET,
321 "Reducing faucet account issuance",
322 account.id = faucet_id,
323 asset.symbol = symbol.to_string(),
324 asset.amount = total_issuance
325 );
326 } else {
327 debug!(
328 target: LOG_TARGET,
329 "No wallet references faucet asset",
330 account.id = faucet_id,
331 asset.symbol = symbol.to_string()
332 );
333 }
334
335 faucet_account.set_nonce(ONE)?;
337
338 debug_assert_eq!(faucet_account.nonce(), ONE);
339
340 let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
342 let max_supply = faucet.max_supply().as_u64();
343 if max_supply < total_issuance {
344 return Err(GenesisConfigError::MaxIssuanceExceeded {
345 max_supply,
346 symbol,
347 total_issuance,
348 });
349 }
350
351 all_accounts.push(faucet_account);
352 }
353 all_accounts.extend(operator_account);
355
356 all_accounts.extend(wallet_accounts);
358
359 all_accounts.extend(file_loaded_accounts);
361
362 Ok((
363 GenesisState {
364 fee_parameters,
365 accounts: all_accounts,
366 version,
367 timestamp,
368 validator_keys,
369 },
370 AccountSecrets { secrets },
371 ))
372 }
373}
374
375#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
382#[serde(deny_unknown_fields)]
383pub struct FeeParameterConfig {
384 verification_base_fee: u32,
386}
387
388struct NativeFaucet {
393 account: Account,
394 symbol: TokenSymbolStr,
395 operator: Option<(Account, RpoSecretKey)>,
398}
399
400struct NativeFaucetConfig(Option<PathBuf>);
405
406impl NativeFaucetConfig {
407 fn build_account(self, config_dir: &Path) -> Result<NativeFaucet, GenesisConfigError> {
414 match self.0 {
415 None => {
416 let (mut operator, operator_secret) = build_faucet_operator()?;
418 let (account, symbol) = build_native_faucet(operator.id())?;
419 let operator_asset =
422 FungibleAsset::new(account.id(), DEFAULT_FAUCET_OPERATOR_BALANCE)?;
423 operator.vault_mut().add_asset(operator_asset.into())?;
424 Ok(NativeFaucet {
425 account,
426 symbol,
427 operator: Some((operator, operator_secret)),
428 })
429 },
430 Some(path) => {
431 let full_path = config_dir.join(&path);
432 let account_file = AccountFile::read(&full_path)
433 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
434 let account = account_file.account;
435
436 let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
437 GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
438 })?;
439 let symbol = TokenSymbolStr::from(faucet.symbol().clone());
440 Ok(NativeFaucet { account, symbol, operator: None })
441 },
442 }
443 }
444}
445
446fn build_faucet_operator() -> Result<(Account, RpoSecretKey), GenesisConfigError> {
454 let mut rng = ChaCha20Rng::from_seed(rand::random());
455
456 let secret_key = RpoSecretKey::with_rng(&mut rng);
457 let auth = Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
458 let init_seed: [u8; 32] = rng.random();
459 let mut operator = create_basic_wallet(init_seed, auth, AccountType::Public)?;
460 operator.set_nonce(ONE)?;
461
462 Ok((operator, secret_key))
463}
464
465fn build_native_faucet(
473 operator_id: AccountId,
474) -> Result<(Account, TokenSymbolStr), GenesisConfigError> {
475 let mut rng = ChaCha20Rng::from_seed(rand::random());
476
477 let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL)?;
478 let faucet_component = FungibleFaucet::builder()
479 .name(
480 TokenName::new(DEFAULT_NATIVE_FAUCET_SYMBOL)
481 .expect("token symbol fits within token name byte limit"),
482 )
483 .symbol(symbol.as_ref().clone())
484 .decimals(DEFAULT_NATIVE_FAUCET_DECIMALS)
485 .max_supply(AssetAmount::new(DEFAULT_NATIVE_FAUCET_MAX_SUPPLY)?)
486 .build()?;
487
488 let policies = TokenPolicyManager::builder()
489 .active_mint_policy(MintPolicy::owner_only())
490 .active_burn_policy(BurnPolicy::allow_all())
491 .active_send_policy(TransferPolicy::allow_all())
492 .active_receive_policy(TransferPolicy::allow_all())
493 .build();
494
495 let fee_policy = BasicConstantFeePolicy::new().with_fees([
496 (MintNote::script_root(), AssetAmount::ZERO),
497 (BurnNote::script_root(), AssetAmount::ZERO),
498 ]);
499
500 let faucet_seed: [u8; 32] = rng.random();
503 let faucet = create_native_fungible_faucet_for_genesis(
504 faucet_seed,
505 faucet_component,
506 operator_id,
507 policies,
508 fee_policy,
509 )?;
510
511 debug_assert_eq!(faucet.nonce(), ONE);
512
513 debug_assert!(
516 NetworkAccountNoteAllowlist::try_from(faucet.storage()).is_ok_and(|allowlist| {
517 allowlist.allowed_script_roots().contains(&FeeSponsorshipNote::script_root())
518 }),
519 "network faucet's note allowlist must cover the fee sponsorship note"
520 );
521
522 Ok((faucet, symbol))
523}
524
525#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
530#[serde(deny_unknown_fields)]
531pub struct FungibleFaucetConfig {
532 symbol: TokenSymbolStr,
533 decimals: u8,
534 max_supply: u64,
539 #[serde(default)]
540 account_type: AccountTypeConfig,
541}
542
543impl FungibleFaucetConfig {
544 fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
546 let FungibleFaucetConfig {
547 symbol,
548 decimals,
549 max_supply,
550 account_type,
551 } = self;
552 let mut rng = ChaCha20Rng::from_seed(rand::random());
553 let secret_key = RpoSecretKey::with_rng(&mut rng);
554 let auth = AuthSingleSig::new(Approver::new(
555 secret_key.public_key().into(),
556 AuthScheme::Falcon512Poseidon2,
557 ));
558 let init_seed: [u8; 32] = rng.random();
559
560 let faucet = FungibleFaucet::builder()
561 .name(
562 TokenName::new(&symbol.to_string())
563 .expect("token symbol fits within token name byte limit"),
564 )
565 .symbol(symbol.as_ref().clone())
566 .decimals(decimals)
567 .max_supply(AssetAmount::new(max_supply)?)
568 .build()?;
569
570 let faucet_account = AccountBuilder::new(init_seed)
572 .account_type(account_type.into())
573 .with_component(auth)
574 .with_component(faucet)
575 .with_components(
576 TokenPolicyManager::builder()
577 .active_mint_policy(MintPolicy::allow_all())
578 .active_burn_policy(BurnPolicy::allow_all())
579 .build(),
580 )
581 .build()?;
582
583 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
584
585 Ok((faucet_account, secret_key))
586 }
587}
588
589#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
594#[serde(deny_unknown_fields)]
595pub struct WalletConfig {
596 #[serde(default)]
597 account_type: AccountTypeConfig,
598 assets: Vec<AssetEntry>,
599}
600
601#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
602struct AssetEntry {
603 symbol: TokenSymbolStr,
604 amount: u64,
606}
607
608#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
614pub enum AccountTypeConfig {
615 #[serde(alias = "public")]
617 Public,
618 #[serde(alias = "private")]
620 #[default]
621 Private,
622}
623
624impl From<AccountTypeConfig> for AccountType {
625 fn from(value: AccountTypeConfig) -> AccountType {
626 match value {
627 AccountTypeConfig::Public => AccountType::Public,
628 AccountTypeConfig::Private => AccountType::Private,
629 }
630 }
631}
632
633#[derive(Debug, Clone)]
637pub struct AccountFileWithName {
638 pub name: String,
639 pub account_file: AccountFile,
640}
641
642#[derive(Debug, Clone)]
644pub struct AccountSecrets {
645 pub secrets: Vec<(String, AccountId, Option<RpoSecretKey>)>,
647}
648
649impl AccountSecrets {
650 pub fn as_account_files(
655 &self,
656 genesis_state: &GenesisState,
657 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
658 let account_lut = genesis_state
659 .accounts
660 .iter()
661 .map(|account| (account.id(), account.clone()))
662 .collect::<IndexMap<AccountId, Account>>();
663 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
664 let account = account_lut
665 .get(&account_id)
666 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
667 let auth_secret_keys =
668 secret_key.map(AuthSecretKey::Falcon512Poseidon2).into_iter().collect();
669 let account_file = AccountFile::new(account.clone(), auth_secret_keys);
670 Ok(AccountFileWithName { name, account_file })
671 })
672 }
673}
674
675fn prepare_fungible_asset_update(
681 assets: impl IntoIterator<Item = AssetEntry>,
682 faucets: &IndexMap<TokenSymbolStr, Account>,
683 faucet_issuance: &mut IndexMap<AccountId, u64>,
684) -> Result<Vec<Asset>, GenesisConfigError> {
685 assets
686 .into_iter()
687 .map(|AssetEntry { amount, symbol }| {
688 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
689 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
690 })?;
691 let faucet_id = faucet_account.id();
692
693 let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
694 debug!(
695 target: LOG_TARGET,
696 "Updating faucet issuance",
697 account.id = faucet_id,
698 asset.symbol = symbol.to_string(),
699 asset.amount = amount
700 );
701 issuance
702 .checked_add_assign(&amount)
703 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
704
705 Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount)?))
706 })
707 .collect()
708}
709
710#[derive(Debug, Clone, PartialEq)]
715pub struct TokenSymbolStr {
716 raw: String,
718 encoded: TokenSymbol,
720}
721
722impl AsRef<TokenSymbol> for TokenSymbolStr {
723 fn as_ref(&self) -> &TokenSymbol {
724 &self.encoded
725 }
726}
727
728impl std::fmt::Display for TokenSymbolStr {
729 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
730 f.write_str(&self.raw)
731 }
732}
733
734impl FromStr for TokenSymbolStr {
735 type Err = TokenSymbolError;
737 fn from_str(s: &str) -> Result<Self, Self::Err> {
738 Ok(Self {
739 encoded: TokenSymbol::new(s)?,
740 raw: s.to_string(),
741 })
742 }
743}
744
745impl Eq for TokenSymbolStr {}
746
747impl From<TokenSymbolStr> for TokenSymbol {
748 fn from(value: TokenSymbolStr) -> Self {
749 value.encoded
750 }
751}
752
753impl From<TokenSymbol> for TokenSymbolStr {
754 fn from(symbol: TokenSymbol) -> Self {
755 let raw = symbol.to_string();
756 Self { raw, encoded: symbol }
757 }
758}
759
760impl Ord for TokenSymbolStr {
761 fn cmp(&self, other: &Self) -> Ordering {
762 self.raw.cmp(&other.raw)
763 }
764}
765
766impl PartialOrd for TokenSymbolStr {
767 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
768 Some(self.cmp(other))
769 }
770}
771
772impl std::hash::Hash for TokenSymbolStr {
773 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
774 self.raw.hash::<H>(state);
775 }
776}
777
778impl Serialize for TokenSymbolStr {
779 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
780 where
781 S: serde::Serializer,
782 {
783 serializer.serialize_str(&self.raw)
784 }
785}
786
787impl<'de> Deserialize<'de> for TokenSymbolStr {
788 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
789 where
790 D: serde::Deserializer<'de>,
791 {
792 deserializer.deserialize_str(TokenSymbolVisitor)
793 }
794}
795
796use serde::de::Visitor;
797
798struct TokenSymbolVisitor;
799
800impl Visitor<'_> for TokenSymbolVisitor {
801 type Value = TokenSymbolStr;
802
803 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
804 formatter.write_str("1 to 6 uppercase ascii letters")
805 }
806
807 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
808 where
809 E: serde::de::Error,
810 {
811 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
812 let raw = v.to_string();
813 Ok(TokenSymbolStr { raw, encoded })
814 }
815}