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