1use std::cmp::Ordering;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_node_utils::crypto::get_random_coin;
9use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
10use miden_protocol::account::{
11 Account,
12 AccountBuilder,
13 AccountDelta,
14 AccountFile,
15 AccountId,
16 AccountStorageDelta,
17 AccountType,
18 AccountVaultDelta,
19 FungibleAssetDelta,
20 NonFungibleAssetDelta,
21};
22use miden_protocol::asset::{AssetAmount, FungibleAsset, TokenSymbol};
23use miden_protocol::block::FeeParameters;
24use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
25use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
26use miden_protocol::errors::TokenSymbolError;
27use miden_protocol::{Felt, ONE};
28use miden_standards::AuthMethod;
29use miden_standards::account::auth::AuthSingleSig;
30use miden_standards::account::faucets::{FungibleFaucet, TokenName};
31use miden_standards::account::policies::{
32 BurnPolicyConfig,
33 MintPolicyConfig,
34 PolicyRegistration,
35 TokenPolicyManager,
36};
37use miden_standards::account::wallets::create_basic_wallet;
38use rand::distr::weighted::Weight;
39use rand::{Rng, SeedableRng};
40use rand_chacha::ChaCha20Rng;
41use serde::{Deserialize, Serialize};
42
43use crate::{GenesisState, LOG_TARGET};
44
45mod errors;
46use self::errors::GenesisConfigError;
47
48#[cfg(test)]
49mod tests;
50
51const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
52const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
53const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
54
55#[derive(Debug, Clone, serde::Deserialize)]
63#[serde(deny_unknown_fields)]
64struct GenericAccountConfig {
65 path: PathBuf,
66}
67
68#[derive(Debug, Clone, serde::Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct GenesisConfig {
74 version: u32,
75 timestamp: u32,
76 #[serde(default)]
86 native_faucet: Option<PathBuf>,
87 fee_parameters: FeeParameterConfig,
88 #[serde(default)]
89 wallet: Vec<WalletConfig>,
90 #[serde(default)]
91 fungible_faucet: Vec<FungibleFaucetConfig>,
92 #[serde(default)]
93 account: Vec<GenericAccountConfig>,
94 #[serde(skip)]
95 config_dir: PathBuf,
96}
97
98impl Default for GenesisConfig {
99 fn default() -> Self {
100 Self {
101 version: 1_u32,
102 timestamp: u32::try_from(
103 std::time::SystemTime::now()
104 .duration_since(std::time::UNIX_EPOCH)
105 .expect("Time does not go backwards")
106 .as_secs(),
107 )
108 .expect("Timestamp should fit into u32"),
109 wallet: vec![],
110 native_faucet: None,
111 fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
112 fungible_faucet: vec![],
113 account: vec![],
114 config_dir: PathBuf::from("."),
115 }
116 }
117}
118
119impl GenesisConfig {
120 pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
127 let toml_str = fs_err::read_to_string(path)
128 .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
129 let config_dir = path.parent().expect("config file path must have a parent directory");
130 Self::read_toml(&toml_str, config_dir)
131 }
132
133 fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
139 let mut config: Self = toml::from_str(toml_str)?;
140 config.config_dir = config_dir.to_path_buf();
141 Ok(config)
142 }
143
144 #[expect(clippy::too_many_lines)]
148 pub fn into_state(
149 self,
150 validator_key: PublicKey,
151 ) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
152 let GenesisConfig {
153 version,
154 timestamp,
155 native_faucet,
156 fee_parameters,
157 fungible_faucet: fungible_faucet_configs,
158 wallet: wallet_configs,
159 account: account_entries,
160 config_dir,
161 } = self;
162
163 let file_loaded_accounts = account_entries
165 .into_iter()
166 .map(|acc| {
167 let full_path = config_dir.join(&acc.path);
168 let account_file = AccountFile::read(&full_path)
169 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
170 Ok(account_file.account)
171 })
172 .collect::<Result<Vec<_>, GenesisConfigError>>()?;
173
174 let mut wallet_accounts = Vec::<Account>::new();
175 let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
177
178 let mut secrets = Vec::new();
181
182 let (native_faucet_account, symbol, native_secret) =
184 NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
185 if let Some(secret_key) = native_secret {
186 secrets.push((
187 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
188 native_faucet_account.id(),
189 secret_key,
190 ));
191 }
192 let native_faucet_account_id = native_faucet_account.id();
193 faucet_accounts.insert(symbol.clone(), native_faucet_account);
194
195 for fungible_faucet_config in fungible_faucet_configs {
197 let symbol = fungible_faucet_config.symbol.clone();
198 let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
199
200 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
201 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
202 }
203
204 secrets.push((
205 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
206 faucet_account.id(),
207 secret_key,
208 ));
209 }
212
213 let fee_parameters =
214 FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee);
215
216 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
218
219 let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
220
221 for (index, WalletConfig { account_type, assets }) in wallet_configs.into_iter().enumerate()
223 {
224 tracing::debug!(target: LOG_TARGET, index, assets = ?assets, "Adding wallet account");
225
226 let mut rng = ChaCha20Rng::from_seed(rand::random());
227 let secret_key = RpoSecretKey::with_rng(&mut get_random_coin(&mut rng));
228 let auth = AuthMethod::SingleSig {
229 approver: (secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2),
230 };
231 let init_seed: [u8; 32] = rng.random();
232
233 let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
234
235 let wallet_fungible_asset_update =
237 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
238
239 let wallet_delta = AccountDelta::new(
248 wallet_account.id(),
249 AccountStorageDelta::default(),
250 AccountVaultDelta::new(
251 wallet_fungible_asset_update,
252 NonFungibleAssetDelta::default(),
253 ),
254 ONE,
255 )?;
256
257 wallet_account.apply_delta(&wallet_delta)?;
258
259 debug_assert_eq!(wallet_account.nonce(), ONE);
260
261 secrets.push((
262 format!("wallet_{index:0zero_padding_width$}.mac"),
263 wallet_account.id(),
264 secret_key,
265 ));
266
267 wallet_accounts.push(wallet_account);
268 }
269
270 let mut all_accounts = Vec::<Account>::new();
271 for (symbol, mut faucet_account) in faucet_accounts {
273 let faucet_id = faucet_account.id();
274 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
277
278 let mut storage_delta = AccountStorageDelta::default();
279
280 if total_issuance != 0 {
281 let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
282 let new_token_supply = AssetAmount::new(total_issuance)?;
283 let max_supply = current_faucet.max_supply().as_u64();
284 if max_supply < total_issuance {
285 return Err(GenesisConfigError::MaxIssuanceExceeded {
286 max_supply,
287 symbol: symbol.clone(),
288 total_issuance,
289 });
290 }
291 let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
292 let slot = updated_faucet.token_config_slot_value();
293 storage_delta.set_item(slot.name().clone(), slot.value())?;
294 tracing::debug!(
295 target: LOG_TARGET,
296 "Reducing faucet account {faucet} for {symbol} by {amount}",
297 faucet = faucet_id.to_hex(),
298 symbol = symbol,
299 amount = total_issuance
300 );
301 } else {
302 tracing::debug!(
303 target: LOG_TARGET,
304 "No wallet is referencing {faucet} for {symbol}",
305 faucet = faucet_id.to_hex(),
306 symbol = symbol,
307 );
308 }
309
310 faucet_account.apply_delta(&AccountDelta::new(
311 faucet_id,
312 storage_delta,
313 AccountVaultDelta::default(),
314 ONE,
315 )?)?;
316
317 debug_assert_eq!(faucet_account.nonce(), ONE);
318
319 let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
321 let max_supply = faucet.max_supply().as_u64();
322 if max_supply < total_issuance {
323 return Err(GenesisConfigError::MaxIssuanceExceeded {
324 max_supply,
325 symbol,
326 total_issuance,
327 });
328 }
329
330 all_accounts.push(faucet_account);
331 }
332 all_accounts.extend(wallet_accounts);
334
335 all_accounts.extend(file_loaded_accounts);
337
338 Ok((
339 GenesisState {
340 fee_parameters,
341 accounts: all_accounts,
342 version,
343 timestamp,
344 validator_key,
345 },
346 AccountSecrets { secrets },
347 ))
348 }
349}
350
351#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
358#[serde(deny_unknown_fields)]
359pub struct FeeParameterConfig {
360 verification_base_fee: u32,
362}
363
364struct NativeFaucetConfig(Option<PathBuf>);
371
372impl NativeFaucetConfig {
373 fn build_account(
378 self,
379 config_dir: &Path,
380 ) -> Result<(Account, TokenSymbolStr, Option<RpoSecretKey>), GenesisConfigError> {
381 match self.0 {
382 None => {
383 let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL).unwrap();
384 let faucet_config = FungibleFaucetConfig {
385 symbol: symbol.clone(),
386 decimals: DEFAULT_NATIVE_FAUCET_DECIMALS,
387 max_supply: DEFAULT_NATIVE_FAUCET_MAX_SUPPLY,
388 account_type: AccountTypeConfig::Public,
389 };
390 let (account, secret_key) = faucet_config.build_account()?;
391 Ok((account, symbol, Some(secret_key)))
392 },
393 Some(path) => {
394 let full_path = config_dir.join(&path);
395 let account_file = AccountFile::read(&full_path)
396 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
397 let account = account_file.account;
398
399 let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
400 GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
401 })?;
402 let symbol = TokenSymbolStr::from(faucet.symbol().clone());
403 Ok((account, symbol, None))
404 },
405 }
406 }
407}
408
409#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
414#[serde(deny_unknown_fields)]
415pub struct FungibleFaucetConfig {
416 symbol: TokenSymbolStr,
417 decimals: u8,
418 max_supply: u64,
423 #[serde(default)]
424 account_type: AccountTypeConfig,
425}
426
427impl FungibleFaucetConfig {
428 fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
430 let FungibleFaucetConfig {
431 symbol,
432 decimals,
433 max_supply,
434 account_type,
435 } = self;
436 let mut rng = ChaCha20Rng::from_seed(rand::random());
437 let secret_key = RpoSecretKey::with_rng(&mut get_random_coin(&mut rng));
438 let auth =
439 AuthSingleSig::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
440 let init_seed: [u8; 32] = rng.random();
441
442 let faucet = FungibleFaucet::builder()
443 .name(
444 TokenName::new(&symbol.to_string())
445 .expect("token symbol fits within token name byte limit"),
446 )
447 .symbol(symbol.as_ref().clone())
448 .decimals(decimals)
449 .max_supply(AssetAmount::new(max_supply)?)
450 .build()?;
451
452 let faucet_account = AccountBuilder::new(init_seed)
454 .account_type(account_type.into())
455 .with_auth_component(auth)
456 .with_component(faucet)
457 .with_components(
458 TokenPolicyManager::new()
459 .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)?
460 .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)?,
461 )
462 .build()?;
463
464 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
465
466 Ok((faucet_account, secret_key))
467 }
468}
469
470#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
475#[serde(deny_unknown_fields)]
476pub struct WalletConfig {
477 #[serde(default)]
478 account_type: AccountTypeConfig,
479 assets: Vec<AssetEntry>,
480}
481
482#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
483struct AssetEntry {
484 symbol: TokenSymbolStr,
485 amount: u64,
487}
488
489#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
495pub enum AccountTypeConfig {
496 #[serde(alias = "public")]
498 Public,
499 #[serde(alias = "private")]
501 #[default]
502 Private,
503}
504
505impl From<AccountTypeConfig> for AccountType {
506 fn from(value: AccountTypeConfig) -> AccountType {
507 match value {
508 AccountTypeConfig::Public => AccountType::Public,
509 AccountTypeConfig::Private => AccountType::Private,
510 }
511 }
512}
513
514#[derive(Debug, Clone)]
518pub struct AccountFileWithName {
519 pub name: String,
520 pub account_file: AccountFile,
521}
522
523#[derive(Debug, Clone)]
525pub struct AccountSecrets {
526 pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
528}
529
530impl AccountSecrets {
531 pub fn as_account_files(
536 &self,
537 genesis_state: &GenesisState,
538 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
539 let account_lut = IndexMap::<AccountId, Account>::from_iter(
540 genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
541 );
542 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
543 let account = account_lut
544 .get(&account_id)
545 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
546 let account_file = AccountFile::new(
547 account.clone(),
548 vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
549 );
550 Ok(AccountFileWithName { name, account_file })
551 })
552 }
553}
554
555fn prepare_fungible_asset_update(
561 assets: impl IntoIterator<Item = AssetEntry>,
562 faucets: &IndexMap<TokenSymbolStr, Account>,
563 faucet_issuance: &mut IndexMap<AccountId, u64>,
564) -> Result<FungibleAssetDelta, GenesisConfigError> {
565 let assets =
566 Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
567 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
568 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
569 })?;
570
571 Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
572 }))?;
573
574 let mut wallet_asset_delta = FungibleAssetDelta::default();
575 assets
576 .into_iter()
577 .try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
578
579 wallet_asset_delta.iter().try_for_each(|(vault_key, amount)| {
580 let faucet_id = vault_key.faucet_id();
581 let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
582 tracing::debug!(
583 target: LOG_TARGET,
584 "Updating faucet issuance {faucet} with {issuance} += {amount}",
585 faucet = faucet_id.to_hex()
586 );
587
588 issuance
590 .checked_add_assign(
591 &u64::try_from(*amount)
592 .expect("Issuance must always be positive in the scope of genesis config"),
593 )
594 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
595
596 Ok::<_, GenesisConfigError>(())
597 })?;
598
599 Ok(wallet_asset_delta)
600}
601
602#[derive(Debug, Clone, PartialEq)]
607pub struct TokenSymbolStr {
608 raw: String,
610 encoded: TokenSymbol,
612}
613
614impl AsRef<TokenSymbol> for TokenSymbolStr {
615 fn as_ref(&self) -> &TokenSymbol {
616 &self.encoded
617 }
618}
619
620impl std::fmt::Display for TokenSymbolStr {
621 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
622 f.write_str(&self.raw)
623 }
624}
625
626impl FromStr for TokenSymbolStr {
627 type Err = TokenSymbolError;
629 fn from_str(s: &str) -> Result<Self, Self::Err> {
630 Ok(Self {
631 encoded: TokenSymbol::new(s)?,
632 raw: s.to_string(),
633 })
634 }
635}
636
637impl Eq for TokenSymbolStr {}
638
639impl From<TokenSymbolStr> for TokenSymbol {
640 fn from(value: TokenSymbolStr) -> Self {
641 value.encoded
642 }
643}
644
645impl From<TokenSymbol> for TokenSymbolStr {
646 fn from(symbol: TokenSymbol) -> Self {
647 let raw = symbol.to_string();
648 Self { raw, encoded: symbol }
649 }
650}
651
652impl Ord for TokenSymbolStr {
653 fn cmp(&self, other: &Self) -> Ordering {
654 self.raw.cmp(&other.raw)
655 }
656}
657
658impl PartialOrd for TokenSymbolStr {
659 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
660 Some(self.cmp(other))
661 }
662}
663
664impl std::hash::Hash for TokenSymbolStr {
665 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
666 self.raw.hash::<H>(state);
667 }
668}
669
670impl Serialize for TokenSymbolStr {
671 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
672 where
673 S: serde::Serializer,
674 {
675 serializer.serialize_str(&self.raw)
676 }
677}
678
679impl<'de> Deserialize<'de> for TokenSymbolStr {
680 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
681 where
682 D: serde::Deserializer<'de>,
683 {
684 deserializer.deserialize_str(TokenSymbolVisitor)
685 }
686}
687
688use serde::de::Visitor;
689
690struct TokenSymbolVisitor;
691
692impl Visitor<'_> for TokenSymbolVisitor {
693 type Value = TokenSymbolStr;
694
695 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
696 formatter.write_str("1 to 6 uppercase ascii letters")
697 }
698
699 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
700 where
701 E: serde::de::Error,
702 {
703 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
704 let raw = v.to_string();
705 Ok(TokenSymbolStr { raw, encoded })
706 }
707}