1use std::cmp::Ordering;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_node_tracing::debug;
9use miden_objects::account_file::AccountFile;
10use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
11use miden_protocol::account::{Account, AccountBuilder, AccountId, AccountType};
12use miden_protocol::asset::{Asset, AssetAmount, AssetId, FungibleAsset, TokenSymbol};
13use miden_protocol::block::{FeeParameters, ValidatorConfig};
14use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
15use miden_protocol::errors::TokenSymbolError;
16use miden_protocol::protocol_config::ProtocolConfig;
17use miden_protocol::{Felt, ONE};
18use miden_standards::account::auth::{Approver, AuthSingleSig};
19use miden_standards::account::faucets::{FungibleFaucet, TokenName};
20use miden_standards::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager};
21use miden_standards::account::wallets::create_basic_wallet;
22use rand::distr::weighted::Weight;
23use rand::{RngExt, SeedableRng};
24use rand_chacha::ChaCha20Rng;
25use serde::{Deserialize, Serialize};
26
27use crate::{GenesisState, LOG_TARGET};
28
29mod errors;
30use self::errors::GenesisConfigError;
31
32#[cfg(test)]
33mod tests;
34
35#[derive(Debug, Clone)]
37pub struct GenesisInputs {
38 pub native_faucet: Account,
39 pub funding_account: Account,
40 pub fee_parameters: FeeParameters,
41 pub timestamp: u32,
42 pub validator_config: ValidatorConfig,
43}
44
45#[derive(Debug, Clone, serde::Deserialize)]
53#[serde(deny_unknown_fields)]
54struct GenericAccountConfig {
55 name: String,
56 path: PathBuf,
57}
58
59#[derive(Debug, Clone, Default, serde::Deserialize)]
61#[serde(deny_unknown_fields)]
62pub struct GenesisConfig {
63 #[serde(default)]
64 wallet: Vec<WalletConfig>,
65 #[serde(default)]
66 fungible_faucet: Vec<FungibleFaucetConfig>,
67 #[serde(default)]
68 account: Vec<GenericAccountConfig>,
69 #[serde(skip)]
70 config_dir: PathBuf,
71}
72
73impl GenesisConfig {
74 pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
79 let toml_str = fs_err::read_to_string(path)
80 .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
81 let config_dir = path.parent().expect("config file path must have a parent directory");
82 Self::read_toml(&toml_str, config_dir)
83 }
84
85 fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
87 let mut config: Self = toml::from_str(toml_str)?;
88 if config.account.iter().any(|account| account.name.trim().is_empty()) {
89 return Err(GenesisConfigError::EmptyImportedAccountName);
90 }
91 config.config_dir = config_dir.to_path_buf();
92 Ok(config)
93 }
94
95 #[expect(clippy::too_many_lines)]
102 pub fn into_state(
103 self,
104 inputs: GenesisInputs,
105 ) -> Result<(GenesisState, GenesisAccountMetadata), GenesisConfigError> {
106 let GenesisInputs {
107 native_faucet,
108 funding_account,
109 fee_parameters,
110 timestamp,
111 validator_config,
112 } = inputs;
113 let GenesisConfig {
114 fungible_faucet: fungible_faucet_configs,
115 wallet: wallet_configs,
116 account: account_entries,
117 config_dir,
118 } = self;
119
120 let mut names = IndexMap::from([
121 (native_faucet.id(), "Native faucet".to_owned()),
122 (funding_account.id(), "Funding".to_owned()),
123 ]);
124
125 let file_loaded_accounts = account_entries
127 .into_iter()
128 .map(|acc| {
129 let full_path = config_dir.join(&acc.path);
130 let account_file = AccountFile::read(&full_path)
131 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
132 names.insert(account_file.account().id(), acc.name);
133 Ok(account_file.into_parts().0)
134 })
135 .collect::<Result<Vec<_>, GenesisConfigError>>()?;
136
137 let mut wallet_accounts = Vec::<Account>::new();
138 let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
140
141 let mut secrets = Vec::new();
143
144 let native_faucet_account_id = native_faucet.id();
145 let faucet = FungibleFaucet::try_from(&native_faucet).map_err(|_| {
146 GenesisConfigError::NativeFaucetNotFungible { account_id: native_faucet_account_id }
147 })?;
148 if funding_account.id().account_type() != AccountType::Public {
149 return Err(GenesisConfigError::FundingAccountNotPublic {
150 account_id: funding_account.id(),
151 });
152 }
153 for account in [&native_faucet, &funding_account] {
154 if account.nonce() == Felt::ZERO {
155 return Err(GenesisConfigError::UndeployedAccount { account_id: account.id() });
156 }
157 }
158
159 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
162 faucet_issuance.insert(native_faucet_account_id, faucet.token_supply().as_u64());
163 faucet_accounts.insert(TokenSymbolStr::from(faucet.symbol().clone()), native_faucet);
164
165 for fungible_faucet_config in fungible_faucet_configs {
167 let symbol = fungible_faucet_config.symbol.clone();
168 let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
169
170 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
171 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
172 }
173
174 secrets.push((
175 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
176 faucet_account.id(),
177 Some(AuthSecretKey::Falcon512Poseidon2(secret_key)),
178 ));
179 }
180
181 let protocol_config =
182 ProtocolConfig::current(AssetId::new_fungible(native_faucet_account_id))?;
183
184 for (index, WalletConfig { name, account_type, auth_scheme, assets }) in
186 wallet_configs.into_iter().enumerate()
187 {
188 debug!(
189 target: LOG_TARGET,
190 "Adding wallet account",
191 account.index = index,
192 account.assets.count = assets.len()
193 );
194
195 if Path::new(&name).file_name() != Some(name.as_ref()) {
197 return Err(GenesisConfigError::InvalidAccountFileName { name });
198 }
199
200 let auth_scheme = auth_scheme
201 .as_deref()
202 .map(AuthScheme::from_str)
203 .transpose()?
204 .unwrap_or(AuthScheme::Falcon512Poseidon2);
205
206 let mut rng = ChaCha20Rng::from_seed(rand::random());
207 let secret_key = AuthSecretKey::with_scheme_and_rng(auth_scheme, &mut rng)?;
208 let auth = Approver::from(&secret_key.public_key());
209 let init_seed: [u8; 32] = rng.random();
210
211 let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
212
213 let wallet_assets =
215 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
216 for asset in wallet_assets {
217 wallet_account.vault_mut().add_asset(asset)?;
218 }
219
220 wallet_account.set_nonce(ONE)?;
229
230 debug_assert_eq!(wallet_account.nonce(), ONE);
231
232 secrets.push((format!("{name}.mac"), wallet_account.id(), Some(secret_key)));
233
234 wallet_accounts.push(wallet_account);
235 }
236
237 let mut all_accounts = Vec::<Account>::new();
238 for (symbol, mut faucet_account) in faucet_accounts {
240 let faucet_id = faucet_account.id();
241 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
243
244 if total_issuance != 0 {
245 let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
246 let new_token_supply = AssetAmount::new(total_issuance)?;
247 let max_supply = current_faucet.max_supply().as_u64();
248 if max_supply < total_issuance {
249 return Err(GenesisConfigError::MaxIssuanceExceeded {
250 max_supply,
251 symbol: symbol.clone(),
252 total_issuance,
253 });
254 }
255 let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
256 let slot = updated_faucet.token_config_slot_value();
257 faucet_account.storage_mut().set_item(slot.name(), slot.value())?;
258 debug!(
259 target: LOG_TARGET,
260 "Setting faucet account issuance",
261 account.id = faucet_id,
262 asset.symbol = symbol.to_string(),
263 asset.amount = total_issuance
264 );
265 } else {
266 debug!(
267 target: LOG_TARGET,
268 "No wallet references faucet asset",
269 account.id = faucet_id,
270 asset.symbol = symbol.to_string()
271 );
272 }
273
274 if faucet_id != native_faucet_account_id {
275 faucet_account.set_nonce(ONE)?;
276 }
277
278 all_accounts.push(faucet_account);
279 }
280 all_accounts.push(funding_account);
281
282 all_accounts.extend(wallet_accounts);
284
285 all_accounts.extend(file_loaded_accounts);
287
288 let mut account_ids = std::collections::HashSet::new();
289 for account in &all_accounts {
290 if !account_ids.insert(account.id()) {
291 return Err(GenesisConfigError::DuplicateAccount { account_id: account.id() });
292 }
293 }
294
295 let mut file_names: Vec<&str> = secrets.iter().map(|(name, ..)| name.as_str()).collect();
297 file_names.sort_unstable();
298 if let Some(pair) = file_names.windows(2).find(|pair| pair[0] == pair[1]) {
299 return Err(GenesisConfigError::DuplicateAccountFileName { name: pair[0].to_string() });
300 }
301 names.extend(secrets.iter().map(|(name, id, _)| (*id, name.clone())));
302
303 Ok((
304 GenesisState {
305 fee_parameters,
306 accounts: all_accounts,
307 timestamp,
308 validator_config,
309 protocol_config,
310 },
311 GenesisAccountMetadata { names, secrets },
312 ))
313 }
314}
315
316#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
321#[serde(deny_unknown_fields)]
322pub struct FungibleFaucetConfig {
323 symbol: TokenSymbolStr,
324 decimals: u8,
325 max_supply: u64,
330 #[serde(default)]
331 account_type: AccountTypeConfig,
332}
333
334impl FungibleFaucetConfig {
335 fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
337 let FungibleFaucetConfig {
338 symbol,
339 decimals,
340 max_supply,
341 account_type,
342 } = self;
343 let mut rng = ChaCha20Rng::from_seed(rand::random());
344 let secret_key = RpoSecretKey::with_rng(&mut rng);
345 let auth = AuthSingleSig::new(Approver::new(
346 secret_key.public_key().into(),
347 AuthScheme::Falcon512Poseidon2,
348 ));
349 let init_seed: [u8; 32] = rng.random();
350
351 let faucet = FungibleFaucet::builder()
352 .name(
353 TokenName::new(&symbol.to_string())
354 .expect("token symbol fits within token name byte limit"),
355 )
356 .symbol(symbol.as_ref().clone())
357 .decimals(decimals)
358 .max_supply(AssetAmount::new(max_supply)?)
359 .build()?;
360
361 let faucet_account = AccountBuilder::new(init_seed)
363 .account_type(account_type.into())
364 .with_component(auth)
365 .with_component(faucet)
366 .with_components(
367 TokenPolicyManager::builder()
368 .active_mint_policy(MintPolicy::allow_all())
369 .active_burn_policy(BurnPolicy::allow_all())
370 .build(),
371 )
372 .build()?;
373
374 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
375
376 Ok((faucet_account, secret_key))
377 }
378}
379
380#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
385#[serde(deny_unknown_fields)]
386pub struct WalletConfig {
387 name: String,
389 #[serde(default)]
390 account_type: AccountTypeConfig,
391 #[serde(default)]
394 auth_scheme: Option<String>,
395 assets: Vec<AssetEntry>,
396}
397
398#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
399struct AssetEntry {
400 symbol: TokenSymbolStr,
401 amount: u64,
403}
404
405#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
411pub enum AccountTypeConfig {
412 #[serde(alias = "public")]
414 Public,
415 #[serde(alias = "private")]
417 #[default]
418 Private,
419}
420
421impl From<AccountTypeConfig> for AccountType {
422 fn from(value: AccountTypeConfig) -> AccountType {
423 match value {
424 AccountTypeConfig::Public => AccountType::Public,
425 AccountTypeConfig::Private => AccountType::Private,
426 }
427 }
428}
429
430#[derive(Debug, Clone)]
434pub struct AccountFileWithName {
435 pub name: String,
436 pub account_file: AccountFile,
437}
438
439#[derive(Debug, Clone)]
441pub struct GenesisAccountMetadata {
442 pub names: IndexMap<AccountId, String>,
443 pub secrets: Vec<(String, AccountId, Option<AuthSecretKey>)>,
445}
446
447impl GenesisAccountMetadata {
448 pub fn as_account_files(
450 &self,
451 genesis_state: &GenesisState,
452 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
453 let account_lut = genesis_state
454 .accounts
455 .iter()
456 .map(|account| (account.id(), account.clone()))
457 .collect::<IndexMap<AccountId, Account>>();
458 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
459 let account = account_lut
460 .get(&account_id)
461 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
462 let auth_secret_keys = secret_key.into_iter().collect();
463 let account_file = AccountFile::new(account.clone(), auth_secret_keys);
464 Ok(AccountFileWithName { name, account_file })
465 })
466 }
467}
468
469fn prepare_fungible_asset_update(
474 assets: impl IntoIterator<Item = AssetEntry>,
475 faucets: &IndexMap<TokenSymbolStr, Account>,
476 faucet_issuance: &mut IndexMap<AccountId, u64>,
477) -> Result<Vec<Asset>, GenesisConfigError> {
478 assets
479 .into_iter()
480 .map(|AssetEntry { amount, symbol }| {
481 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
482 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
483 })?;
484 let faucet_id = faucet_account.id();
485
486 let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
487 debug!(
488 target: LOG_TARGET,
489 "Updating faucet issuance",
490 account.id = faucet_id,
491 asset.symbol = symbol.to_string(),
492 asset.amount = amount
493 );
494 issuance
495 .checked_add_assign(&amount)
496 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
497
498 Ok(FungibleAsset::new(faucet_id, amount)?.into())
499 })
500 .collect()
501}
502
503#[derive(Debug, Clone, PartialEq)]
508pub struct TokenSymbolStr {
509 raw: String,
511 encoded: TokenSymbol,
513}
514
515impl AsRef<TokenSymbol> for TokenSymbolStr {
516 fn as_ref(&self) -> &TokenSymbol {
517 &self.encoded
518 }
519}
520
521impl std::fmt::Display for TokenSymbolStr {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 f.write_str(&self.raw)
524 }
525}
526
527impl FromStr for TokenSymbolStr {
528 type Err = TokenSymbolError;
530 fn from_str(s: &str) -> Result<Self, Self::Err> {
531 Ok(Self {
532 encoded: TokenSymbol::new(s)?,
533 raw: s.to_string(),
534 })
535 }
536}
537
538impl Eq for TokenSymbolStr {}
539
540impl From<TokenSymbolStr> for TokenSymbol {
541 fn from(value: TokenSymbolStr) -> Self {
542 value.encoded
543 }
544}
545
546impl From<TokenSymbol> for TokenSymbolStr {
547 fn from(symbol: TokenSymbol) -> Self {
548 let raw = symbol.to_string();
549 Self { raw, encoded: symbol }
550 }
551}
552
553impl Ord for TokenSymbolStr {
554 fn cmp(&self, other: &Self) -> Ordering {
555 self.raw.cmp(&other.raw)
556 }
557}
558
559impl PartialOrd for TokenSymbolStr {
560 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
561 Some(self.cmp(other))
562 }
563}
564
565impl std::hash::Hash for TokenSymbolStr {
566 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
567 self.raw.hash::<H>(state);
568 }
569}
570
571impl Serialize for TokenSymbolStr {
572 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
573 where
574 S: serde::Serializer,
575 {
576 serializer.serialize_str(&self.raw)
577 }
578}
579
580impl<'de> Deserialize<'de> for TokenSymbolStr {
581 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
582 where
583 D: serde::Deserializer<'de>,
584 {
585 deserializer.deserialize_str(TokenSymbolVisitor)
586 }
587}
588
589use serde::de::Visitor;
590
591struct TokenSymbolVisitor;
592
593impl Visitor<'_> for TokenSymbolVisitor {
594 type Value = TokenSymbolStr;
595
596 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
597 formatter.write_str("1 to 6 uppercase ascii letters")
598 }
599
600 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
601 where
602 E: serde::de::Error,
603 {
604 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
605 let raw = v.to_string();
606 Ok(TokenSymbolStr { raw, encoded })
607 }
608}