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