Skip to main content

miden_node_store/genesis/config/
mod.rs

1//! Describe a subset of the genesis manifest in easily human readable format
2
3use 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// GENESIS CONFIG
46// ================================================================================================
47
48/// Specify a set of faucets and wallets with assets for easier test deployments.
49///
50/// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account.
51#[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    /// Read the genesis accounts from a toml formatted string
89    ///
90    /// Notice: It will generate the specified case during [`fn into_state`].
91    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    /// Convert the in memory representation into the new genesis state
97    ///
98    /// Also returns the set of secrets for the generated accounts.
99    #[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        // Every asset sitting in a wallet, has to reference a faucet for that asset
118        let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
119
120        // Collect the generated secret keys for the test, so one can interact with those
121        // accounts/sign transactions
122        let mut secrets = Vec::new();
123
124        // First setup all the faucets
125        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            // Do _not_ collect the account, only after we know all wallet assets
141            // we know the remaining supply in the faucets.
142        }
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        // Track all adjustments, one per faucet account id
153        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        // Setup all wallet accounts, which reference the faucet's for their provided assets.
158        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            // Add fungible assets and track the faucet adjustments per faucet/asset.
178            let wallet_fungible_asset_update =
179                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
180
181            // Force the account nonce to 1.
182            //
183            // By convention, a nonce of zero indicates a freshly generated local account that has
184            // yet to be deployed. An account is deployed onchain along with its first
185            // transaction which results in a non-zero nonce onchain.
186            //
187            // The genesis block is special in that accounts are "deployed" without transactions and
188            // therefore we need bump the nonce manually to uphold this invariant.
189            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        // Apply all fungible faucet adjustments to the respective faucet
214        for (symbol, mut faucet_account) in faucet_accounts {
215            let faucet_id = faucet_account.id();
216            // If there is no account using the asset, we use an empty delta to set the
217            // nonce to `ONE`.
218            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                // slot 0
224                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            // sanity check the total issuance against
252            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        // Ensure the faucets always precede the wallets referencing them
265        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// NATIVE FAUCET
281// ================================================================================================
282
283/// Declare the native fungible asset
284#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
285#[serde(deny_unknown_fields)]
286pub struct NativeFaucet {
287    /// Token symbol to use for fees.
288    symbol: TokenSymbolStr,
289
290    decimals: u8,
291    /// Max supply in full token units
292    ///
293    /// It will be converted internally to the smallest representable unit,
294    /// using based `10.powi(decimals)` as a multiplier.
295    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// FEE PARAMETER CONFIG
311// ================================================================================================
312
313/// Represents a the fee parameters using the given asset
314///
315/// A faucet providing the `symbol` token moste exist.
316#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
317#[serde(deny_unknown_fields)]
318pub struct FeeParameterConfig {
319    /// Verification base fee, in units of smallest denomination.
320    verification_base_fee: u32,
321}
322
323// FUNGIBLE FAUCET CONFIG
324// ================================================================================================
325
326/// Represents a faucet with asset specific properties
327#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct FungibleFaucetConfig {
330    symbol: TokenSymbolStr,
331    decimals: u8,
332    /// Max supply in full token units
333    ///
334    /// It will be converted internally to the smallest representable unit,
335    /// using based `10.powi(decimals)` as a multiplier.
336    max_supply: u64,
337    #[serde(default)]
338    storage_mode: StorageMode,
339}
340
341impl FungibleFaucetConfig {
342    /// Create a fungible faucet from a config entry
343    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        // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
361        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// WALLET CONFIG
375// ================================================================================================
376
377/// Represents a wallet, containing a set of assets
378#[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    /// The amount of full token units the given asset is populated with
392    amount: u64,
393}
394
395// STORAGE MODE
396// ================================================================================================
397
398/// See the [full description](https://0xmiden.github.io/miden-base/account.html?highlight=Accoun#account-storage-mode)
399/// for details
400#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
401pub enum StorageMode {
402    /// Monitor for `Notes` related to the account, in addition to being `Public`.
403    #[serde(alias = "network")]
404    #[default]
405    Network,
406    /// A publicly stored account, lives on-chain.
407    #[serde(alias = "public")]
408    Public,
409    /// A private account, which must be known by interactors.
410    #[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// ACCOUNTS
425// ================================================================================================
426
427#[derive(Debug, Clone)]
428pub struct AccountFileWithName {
429    pub name: String,
430    pub account_file: AccountFile,
431}
432
433/// Secrets generated during the state generation
434#[derive(Debug, Clone)]
435pub struct AccountSecrets {
436    // name, account, private key, account seed
437    pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
438}
439
440impl AccountSecrets {
441    /// Convert the internal tuple into an `AccountFile`
442    ///
443    /// If no name is present, a new one is generated based on the current time
444    /// and the index in
445    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
463// HELPERS
464// ================================================================================================
465
466/// Process wallet assets and return them as a fungible asset delta.
467/// Track the negative adjustments for the respective faucets.
468fn 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        // check against total supply is deferred
495        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/// Wrapper type used for configuration representation.
509///
510/// Required since `Felt` does not implement `Hash` or `Eq`, but both are useful and necessary for a
511/// coherent model construction.
512#[derive(Debug, Clone, PartialEq)]
513pub struct TokenSymbolStr {
514    /// The raw representation, used for `Hash` and `Eq`.
515    raw: String,
516    /// Maintain the duality with the actual implementation.
517    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    // note: we re-use the error type
534    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}