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;
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!(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 "Reducing faucet account {faucet} for {symbol} by {amount}",
296 faucet = faucet_id.to_hex(),
297 symbol = symbol,
298 amount = total_issuance
299 );
300 } else {
301 tracing::debug!(
302 "No wallet is referencing {faucet} for {symbol}",
303 faucet = faucet_id.to_hex(),
304 symbol = symbol,
305 );
306 }
307
308 faucet_account.apply_delta(&AccountDelta::new(
309 faucet_id,
310 storage_delta,
311 AccountVaultDelta::default(),
312 ONE,
313 )?)?;
314
315 debug_assert_eq!(faucet_account.nonce(), ONE);
316
317 let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
319 let max_supply = faucet.max_supply().as_u64();
320 if max_supply < total_issuance {
321 return Err(GenesisConfigError::MaxIssuanceExceeded {
322 max_supply,
323 symbol,
324 total_issuance,
325 });
326 }
327
328 all_accounts.push(faucet_account);
329 }
330 all_accounts.extend(wallet_accounts);
332
333 all_accounts.extend(file_loaded_accounts);
335
336 Ok((
337 GenesisState {
338 fee_parameters,
339 accounts: all_accounts,
340 version,
341 timestamp,
342 validator_key,
343 },
344 AccountSecrets { secrets },
345 ))
346 }
347}
348
349#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct FeeParameterConfig {
358 verification_base_fee: u32,
360}
361
362struct NativeFaucetConfig(Option<PathBuf>);
369
370impl NativeFaucetConfig {
371 fn build_account(
376 self,
377 config_dir: &Path,
378 ) -> Result<(Account, TokenSymbolStr, Option<RpoSecretKey>), GenesisConfigError> {
379 match self.0 {
380 None => {
381 let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL).unwrap();
382 let faucet_config = FungibleFaucetConfig {
383 symbol: symbol.clone(),
384 decimals: DEFAULT_NATIVE_FAUCET_DECIMALS,
385 max_supply: DEFAULT_NATIVE_FAUCET_MAX_SUPPLY,
386 account_type: AccountTypeConfig::Public,
387 };
388 let (account, secret_key) = faucet_config.build_account()?;
389 Ok((account, symbol, Some(secret_key)))
390 },
391 Some(path) => {
392 let full_path = config_dir.join(&path);
393 let account_file = AccountFile::read(&full_path)
394 .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
395 let account = account_file.account;
396
397 let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
398 GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
399 })?;
400 let symbol = TokenSymbolStr::from(faucet.symbol().clone());
401 Ok((account, symbol, None))
402 },
403 }
404 }
405}
406
407#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
412#[serde(deny_unknown_fields)]
413pub struct FungibleFaucetConfig {
414 symbol: TokenSymbolStr,
415 decimals: u8,
416 max_supply: u64,
421 #[serde(default)]
422 account_type: AccountTypeConfig,
423}
424
425impl FungibleFaucetConfig {
426 fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
428 let FungibleFaucetConfig {
429 symbol,
430 decimals,
431 max_supply,
432 account_type,
433 } = self;
434 let mut rng = ChaCha20Rng::from_seed(rand::random());
435 let secret_key = RpoSecretKey::with_rng(&mut get_random_coin(&mut rng));
436 let auth =
437 AuthSingleSig::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
438 let init_seed: [u8; 32] = rng.random();
439
440 let faucet = FungibleFaucet::builder()
441 .name(
442 TokenName::new(&symbol.to_string())
443 .expect("token symbol fits within token name byte limit"),
444 )
445 .symbol(symbol.as_ref().clone())
446 .decimals(decimals)
447 .max_supply(AssetAmount::new(max_supply)?)
448 .build()?;
449
450 let faucet_account = AccountBuilder::new(init_seed)
452 .account_type(account_type.into())
453 .with_auth_component(auth)
454 .with_component(faucet)
455 .with_components(
456 TokenPolicyManager::new()
457 .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active)?
458 .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active)?,
459 )
460 .build()?;
461
462 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
463
464 Ok((faucet_account, secret_key))
465 }
466}
467
468#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
473#[serde(deny_unknown_fields)]
474pub struct WalletConfig {
475 #[serde(default)]
476 account_type: AccountTypeConfig,
477 assets: Vec<AssetEntry>,
478}
479
480#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
481struct AssetEntry {
482 symbol: TokenSymbolStr,
483 amount: u64,
485}
486
487#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
493pub enum AccountTypeConfig {
494 #[serde(alias = "public")]
496 Public,
497 #[serde(alias = "private")]
499 #[default]
500 Private,
501}
502
503impl From<AccountTypeConfig> for AccountType {
504 fn from(value: AccountTypeConfig) -> AccountType {
505 match value {
506 AccountTypeConfig::Public => AccountType::Public,
507 AccountTypeConfig::Private => AccountType::Private,
508 }
509 }
510}
511
512#[derive(Debug, Clone)]
516pub struct AccountFileWithName {
517 pub name: String,
518 pub account_file: AccountFile,
519}
520
521#[derive(Debug, Clone)]
523pub struct AccountSecrets {
524 pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
526}
527
528impl AccountSecrets {
529 pub fn as_account_files(
534 &self,
535 genesis_state: &GenesisState,
536 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
537 let account_lut = IndexMap::<AccountId, Account>::from_iter(
538 genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
539 );
540 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
541 let account = account_lut
542 .get(&account_id)
543 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
544 let account_file = AccountFile::new(
545 account.clone(),
546 vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
547 );
548 Ok(AccountFileWithName { name, account_file })
549 })
550 }
551}
552
553fn prepare_fungible_asset_update(
559 assets: impl IntoIterator<Item = AssetEntry>,
560 faucets: &IndexMap<TokenSymbolStr, Account>,
561 faucet_issuance: &mut IndexMap<AccountId, u64>,
562) -> Result<FungibleAssetDelta, GenesisConfigError> {
563 let assets =
564 Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
565 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
566 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
567 })?;
568
569 Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
570 }))?;
571
572 let mut wallet_asset_delta = FungibleAssetDelta::default();
573 assets
574 .into_iter()
575 .try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
576
577 wallet_asset_delta.iter().try_for_each(|(vault_key, amount)| {
578 let faucet_id = vault_key.faucet_id();
579 let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
580 tracing::debug!(
581 "Updating faucet issuance {faucet} with {issuance} += {amount}",
582 faucet = faucet_id.to_hex()
583 );
584
585 issuance
587 .checked_add_assign(
588 &u64::try_from(*amount)
589 .expect("Issuance must always be positive in the scope of genesis config"),
590 )
591 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
592
593 Ok::<_, GenesisConfigError>(())
594 })?;
595
596 Ok(wallet_asset_delta)
597}
598
599#[derive(Debug, Clone, PartialEq)]
604pub struct TokenSymbolStr {
605 raw: String,
607 encoded: TokenSymbol,
609}
610
611impl AsRef<TokenSymbol> for TokenSymbolStr {
612 fn as_ref(&self) -> &TokenSymbol {
613 &self.encoded
614 }
615}
616
617impl std::fmt::Display for TokenSymbolStr {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 f.write_str(&self.raw)
620 }
621}
622
623impl FromStr for TokenSymbolStr {
624 type Err = TokenSymbolError;
626 fn from_str(s: &str) -> Result<Self, Self::Err> {
627 Ok(Self {
628 encoded: TokenSymbol::new(s)?,
629 raw: s.to_string(),
630 })
631 }
632}
633
634impl Eq for TokenSymbolStr {}
635
636impl From<TokenSymbolStr> for TokenSymbol {
637 fn from(value: TokenSymbolStr) -> Self {
638 value.encoded
639 }
640}
641
642impl From<TokenSymbol> for TokenSymbolStr {
643 fn from(symbol: TokenSymbol) -> Self {
644 let raw = symbol.to_string();
645 Self { raw, encoded: symbol }
646 }
647}
648
649impl Ord for TokenSymbolStr {
650 fn cmp(&self, other: &Self) -> Ordering {
651 self.raw.cmp(&other.raw)
652 }
653}
654
655impl PartialOrd for TokenSymbolStr {
656 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
657 Some(self.cmp(other))
658 }
659}
660
661impl std::hash::Hash for TokenSymbolStr {
662 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
663 self.raw.hash::<H>(state);
664 }
665}
666
667impl Serialize for TokenSymbolStr {
668 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
669 where
670 S: serde::Serializer,
671 {
672 serializer.serialize_str(&self.raw)
673 }
674}
675
676impl<'de> Deserialize<'de> for TokenSymbolStr {
677 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
678 where
679 D: serde::Deserializer<'de>,
680 {
681 deserializer.deserialize_str(TokenSymbolVisitor)
682 }
683}
684
685use serde::de::Visitor;
686
687struct TokenSymbolVisitor;
688
689impl Visitor<'_> for TokenSymbolVisitor {
690 type Value = TokenSymbolStr;
691
692 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
693 formatter.write_str("1 to 6 uppercase ascii letters")
694 }
695
696 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
697 where
698 E: serde::de::Error,
699 {
700 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
701 let raw = v.to_string();
702 Ok(TokenSymbolStr { raw, encoded })
703 }
704}