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