1use std::cmp::Ordering;
4use std::str::FromStr;
5
6use indexmap::IndexMap;
7use miden_lib::AuthScheme;
8use miden_lib::account::auth::AuthRpoFalcon512;
9use miden_lib::account::faucets::BasicFungibleFaucet;
10use miden_lib::account::wallets::create_basic_wallet;
11use miden_lib::transaction::memory;
12use miden_node_utils::crypto::get_rpo_random_coin;
13use miden_objects::account::auth::AuthSecretKey;
14use miden_objects::account::{
15 Account,
16 AccountBuilder,
17 AccountDelta,
18 AccountFile,
19 AccountId,
20 AccountStorageDelta,
21 AccountStorageMode,
22 AccountType,
23 AccountVaultDelta,
24 FungibleAssetDelta,
25 NonFungibleAssetDelta,
26};
27use miden_objects::asset::{FungibleAsset, TokenSymbol};
28use miden_objects::block::FeeParameters;
29use miden_objects::crypto::dsa::rpo_falcon512::SecretKey;
30use miden_objects::{Felt, FieldElement, ONE, TokenSymbolError, ZERO};
31use rand::distr::weighted::Weight;
32use rand::{Rng, SeedableRng};
33use rand_chacha::ChaCha20Rng;
34use serde::{Deserialize, Serialize};
35
36use crate::GenesisState;
37
38mod errors;
39use self::errors::GenesisConfigError;
40
41#[cfg(test)]
42mod tests;
43
44#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
51pub struct GenesisConfig {
52 version: u32,
53 timestamp: u32,
54 native_faucet: NativeFaucet,
55 fee_parameters: FeeParameterConfig,
56 wallet: Vec<WalletConfig>,
57 fungible_faucet: Vec<FungibleFaucetConfig>,
58}
59
60impl Default for GenesisConfig {
61 fn default() -> Self {
62 let miden = TokenSymbolStr::from_str("MIDEN").unwrap();
63 Self {
64 version: 1_u32,
65 timestamp: u32::try_from(
66 std::time::SystemTime::now()
67 .duration_since(std::time::UNIX_EPOCH)
68 .expect("Time does not go backwards")
69 .as_secs(),
70 )
71 .expect("Timestamp should fit into u32"),
72 wallet: vec![],
73 native_faucet: NativeFaucet {
74 max_supply: 100_000_000_000_000_000u64,
75 decimals: 6u8,
76 symbol: miden.clone(),
77 },
78 fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
79 fungible_faucet: vec![],
80 }
81 }
82}
83
84impl GenesisConfig {
85 pub fn read_toml(toml_str: &str) -> Result<Self, GenesisConfigError> {
89 let me = toml::from_str::<Self>(toml_str)?;
90 Ok(me)
91 }
92
93 #[allow(clippy::too_many_lines)]
97 pub fn into_state(self) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
98 let GenesisConfig {
99 version,
100 timestamp,
101 native_faucet,
102 fee_parameters,
103 fungible_faucet: fungible_faucet_configs,
104 wallet: wallet_configs,
105 } = self;
106
107 let symbol = native_faucet.symbol.clone();
108
109 let mut wallet_accounts = Vec::<Account>::new();
110 let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
112
113 let mut secrets = Vec::new();
116
117 for fungible_faucet_config in std::iter::once(native_faucet.to_faucet_config())
119 .chain(fungible_faucet_configs.into_iter())
120 {
121 let symbol = fungible_faucet_config.symbol.clone();
122 let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
123
124 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
125 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
126 }
127
128 secrets.push((
129 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
130 faucet_account.id(),
131 secret_key,
132 ));
133 }
136
137 let native_faucet_account_id = faucet_accounts
138 .get(&symbol)
139 .expect("Parsing guarantees the existence of a native faucet.")
140 .id();
141
142 let fee_parameters =
143 FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee)?;
144
145 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
147
148 let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
149
150 for (index, WalletConfig { has_updatable_code, storage_mode, assets }) in
152 wallet_configs.into_iter().enumerate()
153 {
154 tracing::debug!("Adding wallet account {index} with {assets:?}");
155
156 let mut rng = ChaCha20Rng::from_seed(rand::random());
157 let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
158 let auth = AuthScheme::RpoFalcon512 { pub_key: secret_key.public_key().into() };
159 let init_seed: [u8; 32] = rng.random();
160
161 let account_type = if has_updatable_code {
162 AccountType::RegularAccountUpdatableCode
163 } else {
164 AccountType::RegularAccountImmutableCode
165 };
166 let account_storage_mode = storage_mode.into();
167 let mut wallet_account =
168 create_basic_wallet(init_seed, auth, account_type, account_storage_mode)?;
169
170 let wallet_fungible_asset_update =
172 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
173
174 let wallet_delta = AccountDelta::new(
183 wallet_account.id(),
184 AccountStorageDelta::default(),
185 AccountVaultDelta::new(
186 wallet_fungible_asset_update,
187 NonFungibleAssetDelta::default(),
188 ),
189 ONE,
190 )?;
191
192 wallet_account.apply_delta(&wallet_delta)?;
193
194 debug_assert_eq!(wallet_account.nonce(), ONE);
195
196 secrets.push((
197 format!("wallet_{index:0zero_padding_width$}.mac"),
198 wallet_account.id(),
199 secret_key,
200 ));
201
202 wallet_accounts.push(wallet_account);
203 }
204
205 let mut all_accounts = Vec::<Account>::new();
206 for (symbol, mut faucet_account) in faucet_accounts {
208 let faucet_id = faucet_account.id();
209 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
212
213 let mut storage_delta = AccountStorageDelta::default();
214
215 if total_issuance != 0 {
216 storage_delta.set_item(
218 memory::FAUCET_STORAGE_DATA_SLOT,
219 [ZERO, ZERO, ZERO, Felt::new(total_issuance)].into(),
220 );
221 tracing::debug!(
222 "Reducing faucet account {faucet} for {symbol} by {amount}",
223 faucet = faucet_id.to_hex(),
224 symbol = symbol,
225 amount = total_issuance
226 );
227 } else {
228 tracing::debug!(
229 "No wallet is referencing {faucet} for {symbol}",
230 faucet = faucet_id.to_hex(),
231 symbol = symbol,
232 );
233 }
234
235 faucet_account.apply_delta(&AccountDelta::new(
236 faucet_id,
237 storage_delta,
238 AccountVaultDelta::default(),
239 ONE,
240 )?)?;
241
242 debug_assert_eq!(faucet_account.nonce(), ONE);
243
244 let basic = BasicFungibleFaucet::try_from(&faucet_account)?;
246 let max_supply = basic.max_supply().inner();
247 if max_supply < total_issuance {
248 return Err(GenesisConfigError::MaxIssuanceExceeded {
249 max_supply,
250 symbol,
251 total_issuance,
252 });
253 }
254
255 all_accounts.push(faucet_account);
256 }
257 all_accounts.extend(wallet_accounts);
259
260 Ok((
261 GenesisState {
262 fee_parameters,
263 accounts: all_accounts,
264 version,
265 timestamp,
266 },
267 AccountSecrets { secrets },
268 ))
269 }
270}
271
272#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
277#[serde(deny_unknown_fields)]
278pub struct NativeFaucet {
279 symbol: TokenSymbolStr,
281
282 decimals: u8,
283 max_supply: u64,
288}
289
290impl NativeFaucet {
291 fn to_faucet_config(&self) -> FungibleFaucetConfig {
292 let NativeFaucet { symbol, decimals, max_supply, .. } = self;
293 FungibleFaucetConfig {
294 symbol: symbol.clone(),
295 decimals: *decimals,
296 max_supply: *max_supply,
297 storage_mode: StorageMode::Public,
298 }
299 }
300}
301
302#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct FeeParameterConfig {
311 verification_base_fee: u32,
313}
314
315#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
320#[serde(deny_unknown_fields)]
321pub struct FungibleFaucetConfig {
322 symbol: TokenSymbolStr,
323 decimals: u8,
324 max_supply: u64,
329 #[serde(default)]
330 storage_mode: StorageMode,
331}
332
333impl FungibleFaucetConfig {
334 fn build_account(self) -> Result<(Account, SecretKey), GenesisConfigError> {
336 let FungibleFaucetConfig {
337 symbol,
338 decimals,
339 max_supply,
340 storage_mode,
341 } = self;
342 let mut rng = ChaCha20Rng::from_seed(rand::random());
343 let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
344 let auth = AuthRpoFalcon512::new(secret_key.public_key().into());
345 let init_seed: [u8; 32] = rng.random();
346
347 let max_supply = Felt::try_from(max_supply)
348 .expect("The `Felt::MODULUS` is _always_ larger than the `max_supply`");
349
350 let component = BasicFungibleFaucet::new(*symbol.as_ref(), decimals, max_supply)?;
351
352 let faucet_account = AccountBuilder::new(init_seed)
354 .account_type(AccountType::FungibleFaucet)
355 .storage_mode(storage_mode.into())
356 .with_auth_component(auth)
357 .with_component(component)
358 .build()?;
359
360 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
361
362 Ok((faucet_account, secret_key))
363 }
364}
365
366#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
371#[serde(deny_unknown_fields)]
372pub struct WalletConfig {
373 #[serde(default)]
374 has_updatable_code: bool,
375 #[serde(default)]
376 storage_mode: StorageMode,
377 assets: Vec<AssetEntry>,
378}
379
380#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
381struct AssetEntry {
382 symbol: TokenSymbolStr,
383 amount: u64,
385}
386
387#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
393pub enum StorageMode {
394 #[serde(alias = "network")]
396 #[default]
397 Network,
398 #[serde(alias = "public")]
400 Public,
401 #[serde(alias = "private")]
403 Private,
404}
405
406impl From<StorageMode> for AccountStorageMode {
407 fn from(mode: StorageMode) -> AccountStorageMode {
408 match mode {
409 StorageMode::Network => AccountStorageMode::Network,
410 StorageMode::Private => AccountStorageMode::Private,
411 StorageMode::Public => AccountStorageMode::Public,
412 }
413 }
414}
415
416#[derive(Debug, Clone)]
420pub struct AccountFileWithName {
421 pub name: String,
422 pub account_file: AccountFile,
423}
424
425#[derive(Debug, Clone)]
427pub struct AccountSecrets {
428 pub secrets: Vec<(String, AccountId, SecretKey)>,
430}
431
432impl AccountSecrets {
433 pub fn as_account_files(
438 &self,
439 genesis_state: &GenesisState,
440 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + use<'_> {
441 let account_lut = IndexMap::<AccountId, Account>::from_iter(
442 genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
443 );
444 self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
445 let account = account_lut
446 .get(&account_id)
447 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
448 let account_file =
449 AccountFile::new(account.clone(), vec![AuthSecretKey::RpoFalcon512(secret_key)]);
450 Ok(AccountFileWithName { name, account_file })
451 })
452 }
453}
454
455fn prepare_fungible_asset_update(
461 assets: impl IntoIterator<Item = AssetEntry>,
462 faucets: &IndexMap<TokenSymbolStr, Account>,
463 faucet_issuance: &mut IndexMap<AccountId, u64>,
464) -> Result<FungibleAssetDelta, GenesisConfigError> {
465 let assets =
466 Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
467 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
468 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
469 })?;
470
471 Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
472 }))?;
473
474 let mut wallet_asset_delta = FungibleAssetDelta::default();
475 assets
476 .into_iter()
477 .try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
478
479 wallet_asset_delta.iter().try_for_each(|(faucet_id, amount)| {
480 let issuance: &mut u64 = faucet_issuance.entry(*faucet_id).or_default();
481 tracing::debug!(
482 "Updating faucet issuance {faucet} with {issuance} += {amount}",
483 faucet = faucet_id.to_hex()
484 );
485
486 issuance
488 .checked_add_assign(
489 &u64::try_from(*amount)
490 .expect("Issuance must always be positive in the scope of genesis config"),
491 )
492 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
493
494 Ok::<_, GenesisConfigError>(())
495 })?;
496
497 Ok(wallet_asset_delta)
498}
499
500#[derive(Debug, Clone, PartialEq)]
505pub struct TokenSymbolStr {
506 raw: String,
508 encoded: TokenSymbol,
510}
511
512impl AsRef<TokenSymbol> for TokenSymbolStr {
513 fn as_ref(&self) -> &TokenSymbol {
514 &self.encoded
515 }
516}
517
518impl std::fmt::Display for TokenSymbolStr {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 f.write_str(&self.raw)
521 }
522}
523
524impl FromStr for TokenSymbolStr {
525 type Err = TokenSymbolError;
527 fn from_str(s: &str) -> Result<Self, Self::Err> {
528 Ok(Self {
529 encoded: TokenSymbol::new(s)?,
530 raw: s.to_string(),
531 })
532 }
533}
534
535impl Eq for TokenSymbolStr {}
536
537impl From<TokenSymbolStr> for TokenSymbol {
538 fn from(value: TokenSymbolStr) -> Self {
539 value.encoded
540 }
541}
542
543impl Ord for TokenSymbolStr {
544 fn cmp(&self, other: &Self) -> Ordering {
545 self.raw.cmp(&other.raw)
546 }
547}
548
549impl PartialOrd for TokenSymbolStr {
550 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
551 Some(self.cmp(other))
552 }
553}
554
555impl std::hash::Hash for TokenSymbolStr {
556 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
557 self.raw.hash::<H>(state);
558 }
559}
560
561impl Serialize for TokenSymbolStr {
562 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
563 where
564 S: serde::Serializer,
565 {
566 serializer.serialize_str(&self.raw)
567 }
568}
569
570impl<'de> Deserialize<'de> for TokenSymbolStr {
571 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
572 where
573 D: serde::Deserializer<'de>,
574 {
575 deserializer.deserialize_str(TokenSymbolVisitor)
576 }
577}
578
579use serde::de::Visitor;
580
581struct TokenSymbolVisitor;
582
583impl Visitor<'_> for TokenSymbolVisitor {
584 type Value = TokenSymbolStr;
585
586 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
587 formatter.write_str("1 to 6 uppercase ascii letters")
588 }
589
590 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
591 where
592 E: serde::de::Error,
593 {
594 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
595 let raw = v.to_string();
596 Ok(TokenSymbolStr { raw, encoded })
597 }
598}