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::path::{Path, PathBuf};
5use std::str::FromStr;
6
7use indexmap::IndexMap;
8use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
9use miden_protocol::account::{Account, AccountBuilder, AccountFile, AccountId, AccountType};
10use miden_protocol::asset::{Asset, AssetAmount, FungibleAsset, TokenSymbol};
11use miden_protocol::block::FeeParameters;
12use miden_protocol::crypto::dsa::ecdsa_k256_keccak::PublicKey;
13use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
14use miden_protocol::errors::TokenSymbolError;
15use miden_protocol::{Felt, ONE};
16use miden_standards::account::auth::{Approver, AuthSingleSig};
17use miden_standards::account::faucets::{FungibleFaucet, TokenName};
18use miden_standards::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager};
19use miden_standards::account::wallets::create_basic_wallet;
20use rand::distr::weighted::Weight;
21use rand::{RngExt, SeedableRng};
22use rand_chacha::ChaCha20Rng;
23use serde::{Deserialize, Serialize};
24
25use crate::{GenesisState, LOG_TARGET};
26
27mod errors;
28use self::errors::GenesisConfigError;
29
30#[cfg(test)]
31mod tests;
32
33const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
34const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
35const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
36
37// GENESIS CONFIG
38// ================================================================================================
39
40/// An account loaded from a `.mac` file (path relative to genesis config directory).
41///
42/// Notice: Generic accounts are not validated (e.g. that their vault assets reference known
43/// faucets), leaving the responsibility of ensuring valid genesis state to the operator.
44#[derive(Debug, Clone, serde::Deserialize)]
45#[serde(deny_unknown_fields)]
46struct GenericAccountConfig {
47    path: PathBuf,
48}
49
50/// Specify a set of faucets and wallets with assets for easier test deployments.
51///
52/// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account.
53#[derive(Debug, Clone, serde::Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct GenesisConfig {
56    version: u32,
57    timestamp: u32,
58    /// Override the native faucet with a custom faucet account.
59    ///
60    /// If unspecified, a default native faucet will be used with:
61    ///
62    /// ```toml
63    /// symbol     = "MIDEN"
64    /// decimals   = 6
65    /// max_supply = 100_000_000_000_000_000
66    /// ```
67    #[serde(default)]
68    native_faucet: Option<PathBuf>,
69    fee_parameters: FeeParameterConfig,
70    #[serde(default)]
71    wallet: Vec<WalletConfig>,
72    #[serde(default)]
73    fungible_faucet: Vec<FungibleFaucetConfig>,
74    #[serde(default)]
75    account: Vec<GenericAccountConfig>,
76    #[serde(skip)]
77    config_dir: PathBuf,
78}
79
80impl Default for GenesisConfig {
81    fn default() -> Self {
82        Self {
83            version: 1_u32,
84            timestamp: u32::try_from(
85                std::time::SystemTime::now()
86                    .duration_since(std::time::UNIX_EPOCH)
87                    .expect("Time does not go backwards")
88                    .as_secs(),
89            )
90            .expect("Timestamp should fit into u32"),
91            wallet: vec![],
92            native_faucet: None,
93            fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
94            fungible_faucet: vec![],
95            account: vec![],
96            config_dir: PathBuf::from("."),
97        }
98    }
99}
100
101impl GenesisConfig {
102    /// Read the genesis config from a TOML file.
103    ///
104    /// The parent directory of `path` is used to resolve relative paths for account files
105    /// referenced in the configuration (e.g., `[[account]]` entries with `path` fields).
106    ///
107    /// Notice: It will generate the specified case during [`fn into_state`].
108    pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
109        let toml_str = fs_err::read_to_string(path)
110            .map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
111        let config_dir = path.parent().expect("config file path must have a parent directory");
112        Self::read_toml(&toml_str, config_dir)
113    }
114
115    /// Parse a genesis config from a TOML formatted string.
116    ///
117    /// The `config_dir` parameter is stored so that relative paths for account files
118    /// (e.g., `[[account]]` entries with `path` fields, or native faucet file references)
119    /// can be resolved later during [`Self::into_state`].
120    fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
121        let mut config: Self = toml::from_str(toml_str)?;
122        config.config_dir = config_dir.to_path_buf();
123        Ok(config)
124    }
125
126    /// Convert the in memory representation into the new genesis state
127    ///
128    /// Also returns the set of secrets for the generated accounts.
129    #[expect(clippy::too_many_lines)]
130    pub fn into_state(
131        self,
132        validator_key: PublicKey,
133    ) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
134        let GenesisConfig {
135            version,
136            timestamp,
137            native_faucet,
138            fee_parameters,
139            fungible_faucet: fungible_faucet_configs,
140            wallet: wallet_configs,
141            account: account_entries,
142            config_dir,
143        } = self;
144
145        // Load account files from disk
146        let file_loaded_accounts = account_entries
147            .into_iter()
148            .map(|acc| {
149                let full_path = config_dir.join(&acc.path);
150                let account_file = AccountFile::read(&full_path)
151                    .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
152                Ok(account_file.account)
153            })
154            .collect::<Result<Vec<_>, GenesisConfigError>>()?;
155
156        let mut wallet_accounts = Vec::<Account>::new();
157        // Every asset sitting in a wallet, has to reference a faucet for that asset
158        let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
159
160        // Collect the generated secret keys for the test, so one can interact with those
161        // accounts/sign transactions
162        let mut secrets = Vec::new();
163
164        // Handle native faucet: build from defaults or load from file
165        let (native_faucet_account, symbol, native_secret) =
166            NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
167        if let Some(secret_key) = native_secret {
168            secrets.push((
169                format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
170                native_faucet_account.id(),
171                secret_key,
172            ));
173        }
174        let native_faucet_account_id = native_faucet_account.id();
175        faucet_accounts.insert(symbol.clone(), native_faucet_account);
176
177        // Setup additional fungible faucets from parameters
178        for fungible_faucet_config in fungible_faucet_configs {
179            let symbol = fungible_faucet_config.symbol.clone();
180            let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
181
182            if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
183                return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
184            }
185
186            secrets.push((
187                format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
188                faucet_account.id(),
189                secret_key,
190            ));
191            // Do _not_ collect the account, only after we know all wallet assets we know the
192            // remaining supply in the faucets.
193        }
194
195        let fee_parameters =
196            FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee);
197
198        // Track all adjustments, one per faucet account id
199        let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
200
201        let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
202
203        // Setup all wallet accounts, which reference the faucet's for their provided assets.
204        for (index, WalletConfig { account_type, assets }) in wallet_configs.into_iter().enumerate()
205        {
206            tracing::debug!(target: LOG_TARGET, index, assets = ?assets, "Adding wallet account");
207
208            let mut rng = ChaCha20Rng::from_seed(rand::random());
209            let secret_key = RpoSecretKey::with_rng(&mut rng);
210            let auth =
211                Approver::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
212            let init_seed: [u8; 32] = rng.random();
213
214            let mut wallet_account = create_basic_wallet(init_seed, auth, account_type.into())?;
215
216            // Add fungible assets and track the faucet adjustments per faucet/asset.
217            let wallet_assets =
218                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
219            for asset in wallet_assets {
220                wallet_account.vault_mut().add_asset(asset)?;
221            }
222
223            // Force the account nonce to 1.
224            //
225            // By convention, a nonce of zero indicates a freshly generated local account that has
226            // yet to be deployed. An account is deployed onchain along with its first
227            // transaction which results in a non-zero nonce onchain.
228            //
229            // The genesis block is special in that accounts are "deployed" without transactions and
230            // therefore we need bump the nonce manually to uphold this invariant.
231            wallet_account.set_nonce(ONE)?;
232
233            debug_assert_eq!(wallet_account.nonce(), ONE);
234
235            secrets.push((
236                format!("wallet_{index:0zero_padding_width$}.mac"),
237                wallet_account.id(),
238                secret_key,
239            ));
240
241            wallet_accounts.push(wallet_account);
242        }
243
244        let mut all_accounts = Vec::<Account>::new();
245        // Apply all fungible faucet adjustments to the respective faucet
246        for (symbol, mut faucet_account) in faucet_accounts {
247            let faucet_id = faucet_account.id();
248            // If there is no account using the asset, we only bump the nonce to `ONE`.
249            let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
250
251            if total_issuance != 0 {
252                let current_faucet = FungibleFaucet::try_from(faucet_account.storage())?;
253                let new_token_supply = AssetAmount::new(total_issuance)?;
254                let max_supply = current_faucet.max_supply().as_u64();
255                if max_supply < total_issuance {
256                    return Err(GenesisConfigError::MaxIssuanceExceeded {
257                        max_supply,
258                        symbol: symbol.clone(),
259                        total_issuance,
260                    });
261                }
262                let updated_faucet = current_faucet.with_token_supply(new_token_supply)?;
263                let slot = updated_faucet.token_config_slot_value();
264                faucet_account.storage_mut().set_item(slot.name(), slot.value())?;
265                tracing::debug!(
266                    target: LOG_TARGET,
267                    "Reducing faucet account {faucet} for {symbol} by {amount}",
268                    faucet = faucet_id.to_hex(),
269                    symbol = symbol,
270                    amount = total_issuance
271                );
272            } else {
273                tracing::debug!(
274                    target: LOG_TARGET,
275                    "No wallet is referencing {faucet} for {symbol}",
276                    faucet = faucet_id.to_hex(),
277                    symbol = symbol,
278                );
279            }
280
281            // Force the account nonce to 1, marking the faucet as deployed at genesis.
282            faucet_account.set_nonce(ONE)?;
283
284            debug_assert_eq!(faucet_account.nonce(), ONE);
285
286            // sanity check the total issuance against
287            let faucet = FungibleFaucet::try_from(faucet_account.storage())?;
288            let max_supply = faucet.max_supply().as_u64();
289            if max_supply < total_issuance {
290                return Err(GenesisConfigError::MaxIssuanceExceeded {
291                    max_supply,
292                    symbol,
293                    total_issuance,
294                });
295            }
296
297            all_accounts.push(faucet_account);
298        }
299        // Ensure the faucets always precede the wallets referencing them
300        all_accounts.extend(wallet_accounts);
301
302        // Append file-loaded accounts as-is
303        all_accounts.extend(file_loaded_accounts);
304
305        Ok((
306            GenesisState {
307                fee_parameters,
308                accounts: all_accounts,
309                version,
310                timestamp,
311                validator_key,
312            },
313            AccountSecrets { secrets },
314        ))
315    }
316}
317
318// FEE PARAMETER CONFIG
319// ================================================================================================
320
321/// Represents a the fee parameters using the given asset
322///
323/// A faucet providing the `symbol` token moste exist.
324#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
325#[serde(deny_unknown_fields)]
326pub struct FeeParameterConfig {
327    /// Verification base fee, in units of smallest denomination.
328    verification_base_fee: u32,
329}
330
331// NATIVE FAUCET CONFIG
332// ================================================================================================
333
334/// Wraps an optional path to a pre-built faucet account file.
335///
336/// When no path is provided, a default native faucet is built using hardcoded MIDEN defaults.
337struct NativeFaucetConfig(Option<PathBuf>);
338
339impl NativeFaucetConfig {
340    /// Build or load the native faucet account.
341    ///
342    /// For `None`, builds a new faucet from defaults and returns the generated secret key.
343    /// For `Some(path)`, loads the account from disk and validates it is a fungible faucet.
344    fn build_account(
345        self,
346        config_dir: &Path,
347    ) -> Result<(Account, TokenSymbolStr, Option<RpoSecretKey>), GenesisConfigError> {
348        match self.0 {
349            None => {
350                let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL).unwrap();
351                let faucet_config = FungibleFaucetConfig {
352                    symbol: symbol.clone(),
353                    decimals: DEFAULT_NATIVE_FAUCET_DECIMALS,
354                    max_supply: DEFAULT_NATIVE_FAUCET_MAX_SUPPLY,
355                    account_type: AccountTypeConfig::Public,
356                };
357                let (account, secret_key) = faucet_config.build_account()?;
358                Ok((account, symbol, Some(secret_key)))
359            },
360            Some(path) => {
361                let full_path = config_dir.join(&path);
362                let account_file = AccountFile::read(&full_path)
363                    .map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
364                let account = account_file.account;
365
366                let faucet = FungibleFaucet::try_from(&account).map_err(|_| {
367                    GenesisConfigError::NativeFaucetNotFungible { path: full_path.clone() }
368                })?;
369                let symbol = TokenSymbolStr::from(faucet.symbol().clone());
370                Ok((account, symbol, None))
371            },
372        }
373    }
374}
375
376// FUNGIBLE FAUCET CONFIG
377// ================================================================================================
378
379/// Represents a faucet with asset specific properties
380#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
381#[serde(deny_unknown_fields)]
382pub struct FungibleFaucetConfig {
383    symbol: TokenSymbolStr,
384    decimals: u8,
385    /// Max supply in full token units
386    ///
387    /// It will be converted internally to the smallest representable unit,
388    /// using based `10.powi(decimals)` as a multiplier.
389    max_supply: u64,
390    #[serde(default)]
391    account_type: AccountTypeConfig,
392}
393
394impl FungibleFaucetConfig {
395    /// Create a fungible faucet from a config entry
396    fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
397        let FungibleFaucetConfig {
398            symbol,
399            decimals,
400            max_supply,
401            account_type,
402        } = self;
403        let mut rng = ChaCha20Rng::from_seed(rand::random());
404        let secret_key = RpoSecretKey::with_rng(&mut rng);
405        let auth = AuthSingleSig::new(Approver::new(
406            secret_key.public_key().into(),
407            AuthScheme::Falcon512Poseidon2,
408        ));
409        let init_seed: [u8; 32] = rng.random();
410
411        let faucet = FungibleFaucet::builder()
412            .name(
413                TokenName::new(&symbol.to_string())
414                    .expect("token symbol fits within token name byte limit"),
415            )
416            .symbol(symbol.as_ref().clone())
417            .decimals(decimals)
418            .max_supply(AssetAmount::new(max_supply)?)
419            .build()?;
420
421        // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
422        let faucet_account = AccountBuilder::new(init_seed)
423            .account_type(account_type.into())
424            .with_auth_component(auth)
425            .with_component(faucet)
426            .with_components(
427                TokenPolicyManager::builder()
428                    .active_mint_policy(MintPolicy::allow_all())
429                    .active_burn_policy(BurnPolicy::allow_all())
430                    .build(),
431            )
432            .build()?;
433
434        debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
435
436        Ok((faucet_account, secret_key))
437    }
438}
439
440// WALLET CONFIG
441// ================================================================================================
442
443/// Represents a wallet, containing a set of assets
444#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
445#[serde(deny_unknown_fields)]
446pub struct WalletConfig {
447    #[serde(default)]
448    account_type: AccountTypeConfig,
449    assets: Vec<AssetEntry>,
450}
451
452#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
453struct AssetEntry {
454    symbol: TokenSymbolStr,
455    /// The amount of full token units the given asset is populated with
456    amount: u64,
457}
458
459// ACCOUNT TYPE CONFIG
460// ================================================================================================
461
462/// See the [full description](https://0xmiden.github.io/miden-protocol/account.html?highlight=Accoun#account-storage-mode)
463/// for details
464#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
465pub enum AccountTypeConfig {
466    /// A publicly stored account, lives on-chain.
467    #[serde(alias = "public")]
468    Public,
469    /// A private account, which must be known by interactors.
470    #[serde(alias = "private")]
471    #[default]
472    Private,
473}
474
475impl From<AccountTypeConfig> for AccountType {
476    fn from(value: AccountTypeConfig) -> AccountType {
477        match value {
478            AccountTypeConfig::Public => AccountType::Public,
479            AccountTypeConfig::Private => AccountType::Private,
480        }
481    }
482}
483
484// ACCOUNTS
485// ================================================================================================
486
487#[derive(Debug, Clone)]
488pub struct AccountFileWithName {
489    pub name: String,
490    pub account_file: AccountFile,
491}
492
493/// Secrets generated during the state generation
494#[derive(Debug, Clone)]
495pub struct AccountSecrets {
496    // name, account, private key, account seed
497    pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
498}
499
500impl AccountSecrets {
501    /// Convert the internal tuple into an `AccountFile`
502    ///
503    /// If no name is present, a new one is generated based on the current time
504    /// and the index in
505    pub fn as_account_files(
506        &self,
507        genesis_state: &GenesisState,
508    ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
509        let account_lut = IndexMap::<AccountId, Account>::from_iter(
510            genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
511        );
512        self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
513            let account = account_lut
514                .get(&account_id)
515                .ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
516            let account_file = AccountFile::new(
517                account.clone(),
518                vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
519            );
520            Ok(AccountFileWithName { name, account_file })
521        })
522    }
523}
524
525// HELPERS
526// ================================================================================================
527
528/// Process wallet assets and return them as a fungible asset delta. Track the negative adjustments
529/// for the respective faucets.
530fn prepare_fungible_asset_update(
531    assets: impl IntoIterator<Item = AssetEntry>,
532    faucets: &IndexMap<TokenSymbolStr, Account>,
533    faucet_issuance: &mut IndexMap<AccountId, u64>,
534) -> Result<Vec<Asset>, GenesisConfigError> {
535    assets
536        .into_iter()
537        .map(|AssetEntry { amount, symbol }| {
538            let faucet_account = faucets.get(&symbol).ok_or_else(|| {
539                GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
540            })?;
541            let faucet_id = faucet_account.id();
542
543            let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
544            tracing::debug!(
545                target: LOG_TARGET,
546                "Updating faucet issuance {faucet} with {issuance} += {amount}",
547                faucet = faucet_id.to_hex()
548            );
549            issuance
550                .checked_add_assign(&amount)
551                .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
552
553            Ok(Asset::Fungible(FungibleAsset::new(faucet_id, amount)?))
554        })
555        .collect()
556}
557
558/// Wrapper type used for configuration representation.
559///
560/// Required since `Felt` does not implement `Hash` or `Eq`, but both are useful and necessary for a
561/// coherent model construction.
562#[derive(Debug, Clone, PartialEq)]
563pub struct TokenSymbolStr {
564    /// The raw representation, used for `Hash` and `Eq`.
565    raw: String,
566    /// Maintain the duality with the actual implementation.
567    encoded: TokenSymbol,
568}
569
570impl AsRef<TokenSymbol> for TokenSymbolStr {
571    fn as_ref(&self) -> &TokenSymbol {
572        &self.encoded
573    }
574}
575
576impl std::fmt::Display for TokenSymbolStr {
577    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578        f.write_str(&self.raw)
579    }
580}
581
582impl FromStr for TokenSymbolStr {
583    // note: we re-use the error type
584    type Err = TokenSymbolError;
585    fn from_str(s: &str) -> Result<Self, Self::Err> {
586        Ok(Self {
587            encoded: TokenSymbol::new(s)?,
588            raw: s.to_string(),
589        })
590    }
591}
592
593impl Eq for TokenSymbolStr {}
594
595impl From<TokenSymbolStr> for TokenSymbol {
596    fn from(value: TokenSymbolStr) -> Self {
597        value.encoded
598    }
599}
600
601impl From<TokenSymbol> for TokenSymbolStr {
602    fn from(symbol: TokenSymbol) -> Self {
603        let raw = symbol.to_string();
604        Self { raw, encoded: symbol }
605    }
606}
607
608impl Ord for TokenSymbolStr {
609    fn cmp(&self, other: &Self) -> Ordering {
610        self.raw.cmp(&other.raw)
611    }
612}
613
614impl PartialOrd for TokenSymbolStr {
615    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
616        Some(self.cmp(other))
617    }
618}
619
620impl std::hash::Hash for TokenSymbolStr {
621    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
622        self.raw.hash::<H>(state);
623    }
624}
625
626impl Serialize for TokenSymbolStr {
627    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
628    where
629        S: serde::Serializer,
630    {
631        serializer.serialize_str(&self.raw)
632    }
633}
634
635impl<'de> Deserialize<'de> for TokenSymbolStr {
636    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
637    where
638        D: serde::Deserializer<'de>,
639    {
640        deserializer.deserialize_str(TokenSymbolVisitor)
641    }
642}
643
644use serde::de::Visitor;
645
646struct TokenSymbolVisitor;
647
648impl Visitor<'_> for TokenSymbolVisitor {
649    type Value = TokenSymbolStr;
650
651    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
652        formatter.write_str("1 to 6 uppercase ascii letters")
653    }
654
655    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
656    where
657        E: serde::de::Error,
658    {
659        let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
660        let raw = v.to_string();
661        Ok(TokenSymbolStr { raw, encoded })
662    }
663}