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::{
14 Account,
15 AccountBuilder,
16 AccountDelta,
17 AccountFile,
18 AccountId,
19 AccountStorageDelta,
20 AccountStorageMode,
21 AccountType,
22 AccountVaultDelta,
23 AuthSecretKey,
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, Word, 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, faucet_account_seed, secret_key) =
123 fungible_faucet_config.build_account()?;
124
125 if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
126 return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
127 }
128
129 secrets.push((
130 format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
131 faucet_account.id(),
132 secret_key,
133 faucet_account_seed,
134 ));
135 }
138
139 let native_faucet_account_id = faucet_accounts
140 .get(&symbol)
141 .expect("Parsing guarantees the existence of a native faucet.")
142 .id();
143
144 let fee_parameters =
145 FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee)?;
146
147 let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
149
150 let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
151
152 for (index, WalletConfig { has_updatable_code, storage_mode, assets }) in
154 wallet_configs.into_iter().enumerate()
155 {
156 tracing::debug!("Adding wallet account {index} with {assets:?}");
157
158 let mut rng = ChaCha20Rng::from_seed(rand::random());
159 let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
160 let auth = AuthScheme::RpoFalcon512 { pub_key: secret_key.public_key() };
161 let init_seed: [u8; 32] = rng.random();
162
163 let account_type = if has_updatable_code {
164 AccountType::RegularAccountUpdatableCode
165 } else {
166 AccountType::RegularAccountImmutableCode
167 };
168 let account_storage_mode = storage_mode.into();
169 let (mut wallet_account, wallet_account_seed) =
170 create_basic_wallet(init_seed, auth, account_type, account_storage_mode)?;
171
172 let wallet_fungible_asset_update =
174 prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
175
176 let wallet_delta = AccountDelta::new(
185 wallet_account.id(),
186 AccountStorageDelta::default(),
187 AccountVaultDelta::new(
188 wallet_fungible_asset_update,
189 NonFungibleAssetDelta::default(),
190 ),
191 ONE,
192 )?;
193
194 wallet_account.apply_delta(&wallet_delta)?;
195
196 debug_assert_eq!(wallet_account.nonce(), ONE);
197
198 secrets.push((
199 format!("wallet_{index:0zero_padding_width$}.mac"),
200 wallet_account.id(),
201 secret_key,
202 wallet_account_seed,
203 ));
204
205 wallet_accounts.push(wallet_account);
206 }
207
208 let mut all_accounts = Vec::<Account>::new();
209 for (symbol, mut faucet_account) in faucet_accounts {
211 let faucet_id = faucet_account.id();
212 let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
215
216 let mut storage_delta = AccountStorageDelta::default();
217
218 if total_issuance != 0 {
219 storage_delta.set_item(
221 memory::FAUCET_STORAGE_DATA_SLOT,
222 [ZERO, ZERO, ZERO, Felt::new(total_issuance)].into(),
223 );
224 tracing::debug!(
225 "Reducing faucet account {faucet} for {symbol} by {amount}",
226 faucet = faucet_id.to_hex(),
227 symbol = symbol,
228 amount = total_issuance
229 );
230 } else {
231 tracing::debug!(
232 "No wallet is referencing {faucet} for {symbol}",
233 faucet = faucet_id.to_hex(),
234 symbol = symbol,
235 );
236 }
237
238 faucet_account.apply_delta(&AccountDelta::new(
239 faucet_id,
240 storage_delta,
241 AccountVaultDelta::default(),
242 ONE,
243 )?)?;
244
245 debug_assert_eq!(faucet_account.nonce(), ONE);
246
247 let basic = BasicFungibleFaucet::try_from(&faucet_account)?;
249 let max_supply = basic.max_supply().inner();
250 if max_supply < total_issuance {
251 return Err(GenesisConfigError::MaxIssuanceExceeded {
252 max_supply,
253 symbol,
254 total_issuance,
255 });
256 }
257
258 all_accounts.push(faucet_account);
259 }
260 all_accounts.extend(wallet_accounts);
262
263 Ok((
264 GenesisState {
265 fee_parameters,
266 accounts: all_accounts,
267 version,
268 timestamp,
269 },
270 AccountSecrets { secrets },
271 ))
272 }
273}
274
275#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct NativeFaucet {
282 symbol: TokenSymbolStr,
284
285 decimals: u8,
286 max_supply: u64,
291}
292
293impl NativeFaucet {
294 fn to_faucet_config(&self) -> FungibleFaucetConfig {
295 let NativeFaucet { symbol, decimals, max_supply, .. } = self;
296 FungibleFaucetConfig {
297 symbol: symbol.clone(),
298 decimals: *decimals,
299 max_supply: *max_supply,
300 storage_mode: StorageMode::Public,
301 }
302 }
303}
304
305#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
312#[serde(deny_unknown_fields)]
313pub struct FeeParameterConfig {
314 verification_base_fee: u32,
316}
317
318#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct FungibleFaucetConfig {
325 symbol: TokenSymbolStr,
326 decimals: u8,
327 max_supply: u64,
332 #[serde(default)]
333 storage_mode: StorageMode,
334}
335
336impl FungibleFaucetConfig {
337 fn build_account(self) -> Result<(Account, Word, SecretKey), GenesisConfigError> {
339 let FungibleFaucetConfig {
340 symbol,
341 decimals,
342 max_supply,
343 storage_mode,
344 } = self;
345 let mut rng = ChaCha20Rng::from_seed(rand::random());
346 let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
347 let auth = AuthRpoFalcon512::new(secret_key.public_key());
348 let init_seed: [u8; 32] = rng.random();
349
350 let max_supply = Felt::try_from(max_supply)
351 .expect("The `Felt::MODULUS` is _always_ larger than the `max_supply`");
352
353 let component = BasicFungibleFaucet::new(*symbol.as_ref(), decimals, max_supply)?;
354
355 let (faucet_account, faucet_account_seed) = AccountBuilder::new(init_seed)
357 .account_type(AccountType::FungibleFaucet)
358 .storage_mode(storage_mode.into())
359 .with_auth_component(auth)
360 .with_component(component)
361 .build()?;
362
363 debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
364
365 Ok((faucet_account, faucet_account_seed, secret_key))
366 }
367}
368
369#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
374#[serde(deny_unknown_fields)]
375pub struct WalletConfig {
376 #[serde(default)]
377 has_updatable_code: bool,
378 #[serde(default)]
379 storage_mode: StorageMode,
380 assets: Vec<AssetEntry>,
381}
382
383#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
384struct AssetEntry {
385 symbol: TokenSymbolStr,
386 amount: u64,
388}
389
390#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
396pub enum StorageMode {
397 #[serde(alias = "network")]
399 #[default]
400 Network,
401 #[serde(alias = "public")]
403 Public,
404 #[serde(alias = "private")]
406 Private,
407}
408
409impl From<StorageMode> for AccountStorageMode {
410 fn from(mode: StorageMode) -> AccountStorageMode {
411 match mode {
412 StorageMode::Network => AccountStorageMode::Network,
413 StorageMode::Private => AccountStorageMode::Private,
414 StorageMode::Public => AccountStorageMode::Public,
415 }
416 }
417}
418
419#[derive(Debug, Clone)]
423pub struct AccountFileWithName {
424 pub name: String,
425 pub account_file: AccountFile,
426}
427
428#[derive(Debug, Clone)]
430pub struct AccountSecrets {
431 pub secrets: Vec<(String, AccountId, SecretKey, Word)>,
433}
434
435impl AccountSecrets {
436 pub fn as_account_files(
441 &self,
442 genesis_state: &GenesisState,
443 ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + use<'_> {
444 let account_lut = IndexMap::<AccountId, Account>::from_iter(
445 genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
446 );
447 self.secrets.iter().map(move |(name, account_id, secret_key, account_seed)| {
448 let account = account_lut
449 .get(account_id)
450 .ok_or(GenesisConfigError::MissingGenesisAccount { account_id: *account_id })?;
451 let account_file = AccountFile::new(
452 account.clone(),
453 Some(*account_seed),
454 vec![AuthSecretKey::RpoFalcon512(secret_key.clone())],
455 );
456 let name = name.to_string();
457 Ok(AccountFileWithName { name, account_file })
458 })
459 }
460}
461
462fn prepare_fungible_asset_update(
468 assets: impl IntoIterator<Item = AssetEntry>,
469 faucets: &IndexMap<TokenSymbolStr, Account>,
470 faucet_issuance: &mut IndexMap<AccountId, u64>,
471) -> Result<FungibleAssetDelta, GenesisConfigError> {
472 let assets =
473 Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
474 let faucet_account = faucets.get(&symbol).ok_or_else(|| {
475 GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
476 })?;
477
478 Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
479 }))?;
480
481 let mut wallet_asset_delta = FungibleAssetDelta::default();
482 assets
483 .into_iter()
484 .try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
485
486 wallet_asset_delta.iter().try_for_each(|(faucet_id, amount)| {
487 let issuance: &mut u64 = faucet_issuance.entry(*faucet_id).or_default();
488 tracing::debug!(
489 "Updating faucet issuance {faucet} with {issuance} += {amount}",
490 faucet = faucet_id.to_hex()
491 );
492
493 issuance
495 .checked_add_assign(
496 &u64::try_from(*amount)
497 .expect("Issuance must always be positive in the scope of genesis config"),
498 )
499 .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
500
501 Ok::<_, GenesisConfigError>(())
502 })?;
503
504 Ok(wallet_asset_delta)
505}
506
507#[derive(Debug, Clone, PartialEq)]
512pub struct TokenSymbolStr {
513 raw: String,
515 encoded: TokenSymbol,
517}
518
519impl AsRef<TokenSymbol> for TokenSymbolStr {
520 fn as_ref(&self) -> &TokenSymbol {
521 &self.encoded
522 }
523}
524
525impl std::fmt::Display for TokenSymbolStr {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527 f.write_str(&self.raw)
528 }
529}
530
531impl FromStr for TokenSymbolStr {
532 type Err = TokenSymbolError;
534 fn from_str(s: &str) -> Result<Self, Self::Err> {
535 Ok(Self {
536 encoded: TokenSymbol::new(s)?,
537 raw: s.to_string(),
538 })
539 }
540}
541
542impl Eq for TokenSymbolStr {}
543
544impl From<TokenSymbolStr> for TokenSymbol {
545 fn from(value: TokenSymbolStr) -> Self {
546 value.encoded
547 }
548}
549
550impl Ord for TokenSymbolStr {
551 fn cmp(&self, other: &Self) -> Ordering {
552 self.raw.cmp(&other.raw)
553 }
554}
555
556impl PartialOrd for TokenSymbolStr {
557 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
558 Some(self.cmp(other))
559 }
560}
561
562impl std::hash::Hash for TokenSymbolStr {
563 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
564 self.raw.hash::<H>(state);
565 }
566}
567
568impl Serialize for TokenSymbolStr {
569 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
570 where
571 S: serde::Serializer,
572 {
573 serializer.serialize_str(&self.raw)
574 }
575}
576
577impl<'de> Deserialize<'de> for TokenSymbolStr {
578 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
579 where
580 D: serde::Deserializer<'de>,
581 {
582 deserializer.deserialize_str(TokenSymbolVisitor)
583 }
584}
585
586use serde::de::Visitor;
587
588struct TokenSymbolVisitor;
589
590impl Visitor<'_> for TokenSymbolVisitor {
591 type Value = TokenSymbolStr;
592
593 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
594 formatter.write_str("1 to 6 uppercase ascii letters")
595 }
596
597 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
598 where
599 E: serde::de::Error,
600 {
601 let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
602 let raw = v.to_string();
603 Ok(TokenSymbolStr { raw, encoded })
604 }
605}