1use std::cmp::Ordering;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
9use miden_protocol::account::{Account, AccountBuilder, AccountFile, AccountId, AccountType};
10use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol};
11use miden_protocol::block::FeeParameters;
12use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
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};
17use miden_standards::account::faucets::{FungibleFaucet, TokenName};
18use miden_standards::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager};
19use miden_standards::account::wallets::create_basic_wallet;
20use rand::distr::weighted::Weight;
21use rand::{RngExt, SeedableRng};
22use rand_chacha::ChaCha20Rng;
23use serde::{Deserialize, Serialize};
24
25use crate::{GenesisState, LOG_TARGET};
26
27mod errors;
28use self::errors::GenesisConfigError;
29
30#[cfg(test)]
31mod tests;
32
33const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
34const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
35const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
36
37#[derive(Debug, Clone, serde::Deserialize)]
45#[serde(deny_unknown_fields)]
46struct GenericAccountConfig {
47 path: PathBuf,
48}
49
50#[derive(Debug, Clone, serde::Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct GenesisConfig {
56 version: u32,
57 timestamp: u32,
58 #[serde(default)]
68 native_faucet: Option<PathBuf>,
69 fee_parameters: FeeParameterConfig,
70 #[serde(default)]
71 wallet: Vec<WalletConfig>,
72 #[serde(default)]
73 fungible_faucet: Vec<FungibleFaucetConfig>,
74 #[serde(default)]
75 account: Vec<GenericAccountConfig>,
76 #[serde(skip)]
77 config_dir: PathBuf,
78}
79
80impl Default for GenesisConfig {
81 fn default() -> Self {
82 Self {
83 version: 1_u32,
84 timestamp: u32::try_from(
85 std::time::SystemTime::now()
86 .duration_since(std::time::UNIX_EPOCH)
87 .expect("Time does not go backwards")
88 .as_secs(),
89 )
90 .expect("Timestamp should fit into u32"),
91 wallet: vec![],
92 native_faucet: None,
93 fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
94 fungible_faucet: vec![],
95 account: vec![],
96 config_dir: PathBuf::from("."),
97 }
98 }
99}
100
101impl GenesisConfig {
102 pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
109 let toml_str = fs_err::read_to_string(path)
110 .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
111 let config_dir = path.parent().expect("config file path must have a parent directory");
112 Self::read_toml(&toml_str, config_dir)
113 }
114
115 fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
121 let mut config: Self = toml::from_str(toml_str)?;
122 config.config_dir = config_dir.to_path_buf();
123 Ok(config)
124 }
125
126 #[expect(clippy::too_many_lines)]
130 pub fn into_state(
131 self,
132 validator_key: PublicKey,
133 ) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
134 let GenesisConfig {
135 version,
136 timestamp,
137 native_faucet,
138 fee_parameters,
139 fungible_faucet: fungible_faucet_configs,
140 wallet: wallet_configs,
141 account: account_entries,
142 config_dir,
143 } = self;
144
145 let file_loaded_accounts = account_entries
147 .into_iter()
148 .map(|acc| {
149 let full_path = config_dir.join(&acc.path);
150 let account_file = AccountFile::read(&full_path)
151 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
152 Ok(account_file.account)
153 })
154 .collect::<Result<Vec<_>, GenesisConfigError>>()?;
155
156 let mut wallet_accounts = Vec::<Account>::new();
157 let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
159
160 let mut secrets = Vec::new();
163
164 let (native_faucet_account, symbol, native_secret) =
166 NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
167 if let Some(secret_key) = native_secret {
168 secrets.push((
169 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
170 native_faucet_account.id(),
171 secret_key,
172 ));
173 }
174 let native_faucet_account_id = native_faucet_account.id();
175 faucet_accounts.insert(symbol.clone(), native_faucet_account);
176
177 for fungible_faucet_config in fungible_faucet_configs {
179 let symbol = fungible_faucet_config.symbol.clone();
180 let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
181
182 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
183 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
184 }
185
186 secrets.push((
187 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
188 faucet_account.id(),
189 secret_key,
190 ));
191 }
194
195 let fee_parameters =
196 FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee);
197
198 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
200
201 let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
202
203 for (index, WalletConfig { account_type, assets }) in wallet_configs.into_iter().enumerate()
205 {
206 tracing::debug!(target: LOG_TARGET, index, assets = ?assets, "Adding wallet account");
207
208 let mut rng = ChaCha20Rng::from_seed(rand::random());
209 let secret_key = RpoSecretKey::with_rng(&mut rng);
210 let auth =
211 Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
212 let init_seed: [u8; 32] = rng.random();
213
214 let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
215
216 let wallet_assets =
218 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
219 for asset in wallet_assets {
220 wallet_account.vault_mut().add_asset(asset)?;
221 }
222
223 wallet_account.set_nonce(ONE)?;
232
233 debug_assert_eq!(wallet_account.nonce(), ONE);
234
235 secrets.push((
236 format!("wallet_{index:0zero_padding_width$}.mac"),
237 wallet_account.id(),
238 secret_key,
239 ));
240
241 wallet_accounts.push(wallet_account);
242 }
243
244 let mut all_accounts = Vec::<Account>::new();
245 for (symbol, mut faucet_account) in faucet_accounts {
247 let faucet_id = faucet_account.id();
248 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
250
251 if total_issuance != 0 {
252 let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
253 let new_token_supply = AssetAmount::new(total_issuance)?;
254 let max_supply = current_faucet.max_supply().as_u64();
255 if max_supply < total_issuance {
256 return Err(GenesisConfigError::MaxIssuanceExceeded {
257 max_supply,
258 symbol: symbol.clone(),
259 total_issuance,
260 });
261 }
262 let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
263 let slot = updated_faucet.token_config_slot_value();
264 faucet_account.storage_mut().set_item(slot.name(), slot.value())?;
265 tracing::debug!(
266 target: LOG_TARGET,
267 "Reducing faucet account {faucet} for {symbol} by {amount}",
268 faucet = faucet_id.to_hex(),
269 symbol = symbol,
270 amount = total_issuance
271 );
272 } else {
273 tracing::debug!(
274 target: LOG_TARGET,
275 "No wallet is referencing {faucet} for {symbol}",
276 faucet = faucet_id.to_hex(),
277 symbol = symbol,
278 );
279 }
280
281 faucet_account.set_nonce(ONE)?;
283
284 debug_assert_eq!(faucet_account.nonce(), ONE);
285
286 let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
288 let max_supply = faucet.max_supply().as_u64();
289 if max_supply < total_issuance {
290 return Err(GenesisConfigError::MaxIssuanceExceeded {
291 max_supply,
292 symbol,
293 total_issuance,
294 });
295 }
296
297 all_accounts.push(faucet_account);
298 }
299 all_accounts.extend(wallet_accounts);
301
302 all_accounts.extend(file_loaded_accounts);
304
305 Ok((
306 GenesisState {
307 fee_parameters,
308 accounts: all_accounts,
309 version,
310 timestamp,
311 validator_key,
312 },
313 AccountSecrets { secrets },
314 ))
315 }
316}
317
318#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
325#[serde(deny_unknown_fields)]
326pub struct FeeParameterConfig {
327 verification_base_fee: u32,
329}
330
331struct NativeFaucetConfig(Option<PathBuf>);
338
339impl NativeFaucetConfig {
340 fn build_account(
345 self,
346 config_dir: &Path,
347 ) -> Result<(Account, TokenSymbolStr, Option<RpoSecretKey>), GenesisConfigError> {
348 match self.0 {
349 None => {
350 let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL).unwrap();
351 let faucet_config = FungibleFaucetConfig {
352 symbol: symbol.clone(),
353 decimals: DEFAULT_NATIVE_FAUCET_DECIMALS,
354 max_supply: DEFAULT_NATIVE_FAUCET_MAX_SUPPLY,
355 account_type: AccountTypeConfig::Public,
356 };
357 let (account, secret_key) = faucet_config.build_account()?;
358 Ok((account, symbol, Some(secret_key)))
359 },
360 Some(path) => {
361 let full_path = config_dir.join(&path);
362 let account_file = AccountFile::read(&full_path)
363 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
364 let account = account_file.account;
365
366 let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
367 GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
368 })?;
369 let symbol = TokenSymbolStr::from(faucet.symbol().clone());
370 Ok((account, symbol, None))
371 },
372 }
373 }
374}
375
376#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
381#[serde(deny_unknown_fields)]
382pub struct FungibleFaucetConfig {
383 symbol: TokenSymbolStr,
384 decimals: u8,
385 max_supply: u64,
390 #[serde(default)]
391 account_type: AccountTypeConfig,
392}
393
394impl FungibleFaucetConfig {
395 fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
397 let FungibleFaucetConfig {
398 symbol,
399 decimals,
400 max_supply,
401 account_type,
402 } = self;
403 let mut rng = ChaCha20Rng::from_seed(rand::random());
404 let secret_key = RpoSecretKey::with_rng(&mut rng);
405 let auth = AuthSingleSig::new(Approver::new(
406 secret_key.public_key().into(),
407 AuthScheme::Falcon512Poseidon2,
408 ));
409 let init_seed: [u8; 32] = rng.random();
410
411 let faucet = FungibleFaucet::builder()
412 .name(
413 TokenName::new(&symbol.to_string())
414 .expect("token symbol fits within token name byte limit"),
415 )
416 .symbol(symbol.as_ref().clone())
417 .decimals(decimals)
418 .max_supply(AssetAmount::new(max_supply)?)
419 .build()?;
420
421 let faucet_account = AccountBuilder::new(init_seed)
423 .account_type(account_type.into())
424 .with_auth_component(auth)
425 .with_component(faucet)
426 .with_components(
427 TokenPolicyManager::builder()
428 .active_mint_policy(MintPolicy::allow_all())
429 .active_burn_policy(BurnPolicy::allow_all())
430 .build(),
431 )
432 .build()?;
433
434 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
435
436 Ok((faucet_account, secret_key))
437 }
438}
439
440#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
445#[serde(deny_unknown_fields)]
446pub struct WalletConfig {
447 #[serde(default)]
448 account_type: AccountTypeConfig,
449 assets: Vec<AssetEntry>,
450}
451
452#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
453struct AssetEntry {
454 symbol: TokenSymbolStr,
455 amount: u64,
457}
458
459#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
465pub enum AccountTypeConfig {
466 #[serde(alias = "public")]
468 Public,
469 #[serde(alias = "private")]
471 #[default]
472 Private,
473}
474
475impl From<AccountTypeConfig> for AccountType {
476 fn from(value: AccountTypeConfig) -> AccountType {
477 match value {
478 AccountTypeConfig::Public => AccountType::Public,
479 AccountTypeConfig::Private => AccountType::Private,
480 }
481 }
482}
483
484#[derive(Debug, Clone)]
488pub struct AccountFileWithName {
489 pub name: String,
490 pub account_file: AccountFile,
491}
492
493#[derive(Debug, Clone)]
495pub struct AccountSecrets {
496 pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
498}
499
500impl AccountSecrets {
501 pub fn as_account_files(
506 &self,
507 genesis_state: &GenesisState,
508 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
509 let account_lut = IndexMap::<AccountId, Account>::from_iter(
510 genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
511 );
512 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
513 let account = account_lut
514 .get(&account_id)
515 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
516 let account_file = AccountFile::new(
517 account.clone(),
518 vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
519 );
520 Ok(AccountFileWithName { name, account_file })
521 })
522 }
523}
524
525fn prepare_fungible_asset_update(
531 assets: impl IntoIterator<Item = AssetEntry>,
532 faucets: &IndexMap<TokenSymbolStr, Account>,
533 faucet_issuance: &mut IndexMap<AccountId, u64>,
534) -> Result<Vec<Asset>, GenesisConfigError> {
535 assets
536 .into_iter()
537 .map(|AssetEntry { amount, symbol }| {
538 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
539 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
540 })?;
541 let faucet_id = faucet_account.id();
542
543 let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
544 tracing::debug!(
545 target: LOG_TARGET,
546 "Updating faucet issuance {faucet} with {issuance} += {amount}",
547 faucet = faucet_id.to_hex()
548 );
549 issuance
550 .checked_add_assign(&amount)
551 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
552
553 Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount)?))
554 })
555 .collect()
556}
557
558#[derive(Debug, Clone, PartialEq)]
563pub struct TokenSymbolStr {
564 raw: String,
566 encoded: TokenSymbol,
568}
569
570impl AsRef<TokenSymbol> for TokenSymbolStr {
571 fn as_ref(&self) -> &TokenSymbol {
572 &self.encoded
573 }
574}
575
576impl std::fmt::Display for TokenSymbolStr {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 f.write_str(&self.raw)
579 }
580}
581
582impl FromStr for TokenSymbolStr {
583 type Err = TokenSymbolError;
585 fn from_str(s: &str) -> Result<Self, Self::Err> {
586 Ok(Self {
587 encoded: TokenSymbol::new(s)?,
588 raw: s.to_string(),
589 })
590 }
591}
592
593impl Eq for TokenSymbolStr {}
594
595impl From<TokenSymbolStr> for TokenSymbol {
596 fn from(value: TokenSymbolStr) -> Self {
597 value.encoded
598 }
599}
600
601impl From<TokenSymbol> for TokenSymbolStr {
602 fn from(symbol: TokenSymbol) -> Self {
603 let raw = symbol.to_string();
604 Self { raw, encoded: symbol }
605 }
606}
607
608impl Ord for TokenSymbolStr {
609 fn cmp(&self, other: &Self) -> Ordering {
610 self.raw.cmp(&other.raw)
611 }
612}
613
614impl PartialOrd for TokenSymbolStr {
615 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
616 Some(self.cmp(other))
617 }
618}
619
620impl std::hash::Hash for TokenSymbolStr {
621 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
622 self.raw.hash::<H>(state);
623 }
624}
625
626impl Serialize for TokenSymbolStr {
627 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
628 where
629 S: serde::Serializer,
630 {
631 serializer.serialize_str(&self.raw)
632 }
633}
634
635impl<'de> Deserialize<'de> for TokenSymbolStr {
636 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
637 where
638 D: serde::Deserializer<'de>,
639 {
640 deserializer.deserialize_str(TokenSymbolVisitor)
641 }
642}
643
644use serde::de::Visitor;
645
646struct TokenSymbolVisitor;
647
648impl Visitor<'_> for TokenSymbolVisitor {
649 type Value = TokenSymbolStr;
650
651 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
652 formatter.write_str("1 to 6 uppercase ascii letters")
653 }
654
655 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
656 where
657 E: serde::de::Error,
658 {
659 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
660 let raw = v.to_string();
661 Ok(TokenSymbolStr { raw, encoded })
662 }
663}