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::{
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// 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, 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            // Do _not_ collect the account, only after we know all wallet assets
136            // we know the remaining supply in the faucets.
137        }
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        // Track all adjustments, one per faucet account id
148        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        // Setup all wallet accounts, which reference the faucet's for their provided assets.
153        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            // Add fungible assets and track the faucet adjustments per faucet/asset.
173            let wallet_fungible_asset_update =
174                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
175
176            // Force the account nonce to 1.
177            //
178            // By convention, a nonce of zero indicates a freshly generated local account that has
179            // yet to be deployed. An account is deployed onchain along with its first
180            // transaction which results in a non-zero nonce onchain.
181            //
182            // The genesis block is special in that accounts are "deployed" without transactions and
183            // therefore we need bump the nonce manually to uphold this invariant.
184            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        // Apply all fungible faucet adjustments to the respective faucet
210        for (symbol, mut faucet_account) in faucet_accounts {
211            let faucet_id = faucet_account.id();
212            // If there is no account using the asset, we use an empty delta to set the
213            // nonce to `ONE`.
214            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                // slot 0
220                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            // sanity check the total issuance against
248            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        // Ensure the faucets always precede the wallets referencing them
261        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// NATIVE FAUCET
276// ================================================================================================
277
278/// Declare the native fungible asset
279#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct NativeFaucet {
282    /// Token symbol to use for fees.
283    symbol: TokenSymbolStr,
284
285    decimals: u8,
286    /// Max supply in full token units
287    ///
288    /// It will be converted internally to the smallest representable unit,
289    /// using based `10.powi(decimals)` as a multiplier.
290    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// FEE PARAMETER CONFIG
306// ================================================================================================
307
308/// Represents a the fee parameters using the given asset
309///
310/// A faucet providing the `symbol` token moste exist.
311#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
312#[serde(deny_unknown_fields)]
313pub struct FeeParameterConfig {
314    /// Verification base fee, in units of smallest denomination.
315    verification_base_fee: u32,
316}
317
318// FUNGIBLE FAUCET CONFIG
319// ================================================================================================
320
321/// Represents a faucet with asset specific properties
322#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
323#[serde(deny_unknown_fields)]
324pub struct FungibleFaucetConfig {
325    symbol: TokenSymbolStr,
326    decimals: u8,
327    /// Max supply in full token units
328    ///
329    /// It will be converted internally to the smallest representable unit,
330    /// using based `10.powi(decimals)` as a multiplier.
331    max_supply: u64,
332    #[serde(default)]
333    storage_mode: StorageMode,
334}
335
336impl FungibleFaucetConfig {
337    /// Create a fungible faucet from a config entry
338    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        // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
356        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// WALLET CONFIG
370// ================================================================================================
371
372/// Represents a wallet, containing a set of assets
373#[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    /// The amount of full token units the given asset is populated with
387    amount: u64,
388}
389
390// STORAGE MODE
391// ================================================================================================
392
393/// See the [full description](https://0xmiden.github.io/miden-base/account.html?highlight=Accoun#account-storage-mode)
394/// for details
395#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
396pub enum StorageMode {
397    /// Monitor for `Notes` related to the account, in addition to being `Public`.
398    #[serde(alias = "network")]
399    #[default]
400    Network,
401    /// A publicly stored account, lives on-chain.
402    #[serde(alias = "public")]
403    Public,
404    /// A private account, which must be known by interactors.
405    #[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// ACCOUNTS
420// ================================================================================================
421
422#[derive(Debug, Clone)]
423pub struct AccountFileWithName {
424    pub name: String,
425    pub account_file: AccountFile,
426}
427
428/// Secrets generated during the state generation
429#[derive(Debug, Clone)]
430pub struct AccountSecrets {
431    // name, account, private key, account seed
432    pub secrets: Vec<(String, AccountId, SecretKey, Word)>,
433}
434
435impl AccountSecrets {
436    /// Convert the internal tuple into an `AccountFile`
437    ///
438    /// If no name is present, a new one is generated based on the current time
439    /// and the index in
440    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
462// HELPERS
463// ================================================================================================
464
465/// Process wallet assets and return them as a fungible asset delta.
466/// Track the negative adjustments for the respective faucets.
467fn 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        // check against total supply is deferred
494        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/// Wrapper type used for configuration representation.
508///
509/// Required since `Felt` does not implement `Hash` or `Eq`, but both are useful and necessary for a
510/// coherent model construction.
511#[derive(Debug, Clone, PartialEq)]
512pub struct TokenSymbolStr {
513    /// The raw representation, used for `Hash` and `Eq`.
514    raw: String,
515    /// Maintain the duality with the actual implementation.
516    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    // note: we re-use the error type
533    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}