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_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// GENESIS CONFIG
45// ================================================================================================
46
47/// Specify a set of faucets and wallets with assets for easier test deployments.
48///
49/// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account.
50#[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    /// Read the genesis accounts from a toml formatted string
86    ///
87    /// Notice: It will generate the specified case during [`fn into_state`].
88    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    /// Convert the in memory representation into the new genesis state
94    ///
95    /// Also returns the set of secrets for the generated accounts.
96    #[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        // Every asset sitting in a wallet, has to reference a faucet for that asset
111        let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
112
113        // Collect the generated secret keys for the test, so one can interact with those
114        // accounts/sign transactions
115        let mut secrets = Vec::new();
116
117        // First setup all the faucets
118        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            // Do _not_ collect the account, only after we know all wallet assets
134            // we know the remaining supply in the faucets.
135        }
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        // Track all adjustments, one per faucet account id
146        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        // Setup all wallet accounts, which reference the faucet's for their provided assets.
151        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            // Add fungible assets and track the faucet adjustments per faucet/asset.
171            let wallet_fungible_asset_update =
172                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
173
174            // Force the account nonce to 1.
175            //
176            // By convention, a nonce of zero indicates a freshly generated local account that has
177            // yet to be deployed. An account is deployed onchain along with its first
178            // transaction which results in a non-zero nonce onchain.
179            //
180            // The genesis block is special in that accounts are "deployed" without transactions and
181            // therefore we need bump the nonce manually to uphold this invariant.
182            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        // Apply all fungible faucet adjustments to the respective faucet
207        for (symbol, mut faucet_account) in faucet_accounts {
208            let faucet_id = faucet_account.id();
209            // If there is no account using the asset, we use an empty delta to set the
210            // nonce to `ONE`.
211            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                // slot 0
217                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            // sanity check the total issuance against
245            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        // Ensure the faucets always precede the wallets referencing them
258        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// NATIVE FAUCET
273// ================================================================================================
274
275/// Declare the native fungible asset
276#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
277#[serde(deny_unknown_fields)]
278pub struct NativeFaucet {
279    /// Token symbol to use for fees.
280    symbol: TokenSymbolStr,
281
282    decimals: u8,
283    /// Max supply in full token units
284    ///
285    /// It will be converted internally to the smallest representable unit,
286    /// using based `10.powi(decimals)` as a multiplier.
287    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// FEE PARAMETER CONFIG
303// ================================================================================================
304
305/// Represents a the fee parameters using the given asset
306///
307/// A faucet providing the `symbol` token moste exist.
308#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct FeeParameterConfig {
311    /// Verification base fee, in units of smallest denomination.
312    verification_base_fee: u32,
313}
314
315// FUNGIBLE FAUCET CONFIG
316// ================================================================================================
317
318/// Represents a faucet with asset specific properties
319#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
320#[serde(deny_unknown_fields)]
321pub struct FungibleFaucetConfig {
322    symbol: TokenSymbolStr,
323    decimals: u8,
324    /// Max supply in full token units
325    ///
326    /// It will be converted internally to the smallest representable unit,
327    /// using based `10.powi(decimals)` as a multiplier.
328    max_supply: u64,
329    #[serde(default)]
330    storage_mode: StorageMode,
331}
332
333impl FungibleFaucetConfig {
334    /// Create a fungible faucet from a config entry
335    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        // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
353        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// WALLET CONFIG
367// ================================================================================================
368
369/// Represents a wallet, containing a set of assets
370#[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    /// The amount of full token units the given asset is populated with
384    amount: u64,
385}
386
387// STORAGE MODE
388// ================================================================================================
389
390/// See the [full description](https://0xmiden.github.io/miden-base/account.html?highlight=Accoun#account-storage-mode)
391/// for details
392#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
393pub enum StorageMode {
394    /// Monitor for `Notes` related to the account, in addition to being `Public`.
395    #[serde(alias = "network")]
396    #[default]
397    Network,
398    /// A publicly stored account, lives on-chain.
399    #[serde(alias = "public")]
400    Public,
401    /// A private account, which must be known by interactors.
402    #[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// ACCOUNTS
417// ================================================================================================
418
419#[derive(Debug, Clone)]
420pub struct AccountFileWithName {
421    pub name: String,
422    pub account_file: AccountFile,
423}
424
425/// Secrets generated during the state generation
426#[derive(Debug, Clone)]
427pub struct AccountSecrets {
428    // name, account, private key, account seed
429    pub secrets: Vec<(String, AccountId, SecretKey)>,
430}
431
432impl AccountSecrets {
433    /// Convert the internal tuple into an `AccountFile`
434    ///
435    /// If no name is present, a new one is generated based on the current time
436    /// and the index in
437    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
455// HELPERS
456// ================================================================================================
457
458/// Process wallet assets and return them as a fungible asset delta.
459/// Track the negative adjustments for the respective faucets.
460fn 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        // check against total supply is deferred
487        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/// Wrapper type used for configuration representation.
501///
502/// Required since `Felt` does not implement `Hash` or `Eq`, but both are useful and necessary for a
503/// coherent model construction.
504#[derive(Debug, Clone, PartialEq)]
505pub struct TokenSymbolStr {
506    /// The raw representation, used for `Hash` and `Eq`.
507    raw: String,
508    /// Maintain the duality with the actual implementation.
509    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    // note: we re-use the error type
526    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}