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