Skip to main content

miden_node_store/genesis/config/
mod.rs

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