miden_node_store/genesis/config/
mod.rs

1//! Describe a subset of the genesis manifest in easily human readable format
2
3use std::collections::HashMap;
4
5use miden_lib::{
6    AuthScheme,
7    account::{auth::RpoFalcon512, faucets::BasicFungibleFaucet, wallets::create_basic_wallet},
8    transaction::memory,
9};
10use miden_node_utils::crypto::get_rpo_random_coin;
11use miden_objects::{
12    Felt, FieldElement, ONE, Word, ZERO,
13    account::{
14        Account, AccountBuilder, AccountDelta, AccountFile, AccountId, AccountStorageDelta,
15        AccountStorageMode, AccountType, AccountVaultDelta, AuthSecretKey, FungibleAssetDelta,
16        NonFungibleAssetDelta,
17    },
18    asset::{FungibleAsset, TokenSymbol},
19    crypto::dsa::rpo_falcon512::SecretKey,
20};
21use rand::{Rng, SeedableRng, distr::weighted::Weight};
22use rand_chacha::ChaCha20Rng;
23
24use crate::GenesisState;
25
26mod errors;
27use self::errors::GenesisConfigError;
28
29#[cfg(test)]
30mod tests;
31
32// GENESIS CONFIG
33// ================================================================================================
34
35/// Specify a set of faucets and wallets with assets for easier test deployments.
36///
37/// Notice: Any faucet must be declared _before_ it's use in a wallet/regular account.
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39pub struct GenesisConfig {
40    version: u32,
41    timestamp: u32,
42    wallet: Vec<WalletConfig>,
43    fungible_faucet: Vec<FungibleFaucetConfig>,
44}
45
46impl Default for GenesisConfig {
47    fn default() -> Self {
48        Self {
49            version: 1_u32,
50            timestamp: u32::try_from(
51                std::time::SystemTime::now()
52                    .duration_since(std::time::UNIX_EPOCH)
53                    .expect("Time does not go backwards")
54                    .as_secs(),
55            )
56            .expect("Timestamp should fit into u32"),
57            wallet: vec![],
58            fungible_faucet: vec![FungibleFaucetConfig {
59                max_supply: 100_000_000_000u64,
60                decimals: 6u8,
61                storage_mode: StorageMode::Public,
62                symbol: "MIDEN".to_owned(),
63            }],
64        }
65    }
66}
67
68impl GenesisConfig {
69    /// Read the genesis accounts from a toml formatted string
70    ///
71    /// Notice: It will generate the specified case during [`fn into_state`].
72    pub fn read_toml(toml_str: &str) -> Result<Self, GenesisConfigError> {
73        let me = toml::from_str::<Self>(toml_str)?;
74        Ok(me)
75    }
76
77    /// Convert the in memory representation into the new genesis state
78    ///
79    /// Also returns the set of secrets for the generated accounts.
80    #[allow(clippy::too_many_lines)]
81    pub fn into_state(self) -> Result<(GenesisState, AccountSecrets), GenesisConfigError> {
82        let GenesisConfig {
83            version,
84            timestamp,
85            fungible_faucet: fungible_faucet_configs,
86            wallet: wallet_configs,
87        } = self;
88
89        let mut wallet_accounts = Vec::<Account>::new();
90        // Every asset sitting in a wallet, has to reference a faucet for that asset
91        let mut faucet_accounts = HashMap::<String, Account>::new();
92
93        // Collect the generated secret keys for the test, so one can interact with those
94        // accounts/sign transactions
95        let mut secrets = Vec::new();
96
97        // First setup all the faucets
98        for FungibleFaucetConfig {
99            symbol,
100            decimals,
101            max_supply,
102            storage_mode,
103        } in fungible_faucet_configs
104        {
105            let mut rng = ChaCha20Rng::from_seed(rand::random());
106            let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
107            let auth = RpoFalcon512::new(secret_key.public_key());
108            let init_seed: [u8; 32] = rng.random();
109
110            let token_symbol = TokenSymbol::new(&symbol)?;
111
112            let account_type = AccountType::FungibleFaucet;
113
114            let max_supply = Felt::try_from(max_supply)
115                .expect("The `Felt::MODULUS` is _always_ larger than the `max_supply`");
116
117            let component = BasicFungibleFaucet::new(token_symbol, decimals, max_supply)?;
118
119            let account_storage_mode = storage_mode.into();
120
121            // It's similar to `fn create_basic_fungible_faucet`, but we need to cover more cases.
122            let (faucet_account, faucet_account_seed) = AccountBuilder::new(init_seed)
123                .account_type(account_type)
124                .storage_mode(account_storage_mode)
125                .with_auth_component(auth)
126                .with_component(component)
127                .build()?;
128
129            debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
130
131            if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
132                return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol: token_symbol });
133            }
134
135            secrets.push((
136                format!("faucet_{symbol}.mac", symbol = symbol.to_lowercase()),
137                faucet_account.id(),
138                secret_key,
139                faucet_account_seed,
140            ));
141
142            // Do _not_ collect the account, only after we know all wallet assets
143            // we know the remaining supply in the faucets.
144        }
145
146        // Track all adjustments, one per faucet account id
147        let mut faucet_issuance = HashMap::<AccountId, u64>::new();
148
149        let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
150
151        // Setup all wallet accounts, which reference the faucet's for their provided assets.
152        for (index, WalletConfig { has_updatable_code, storage_mode, assets }) in
153            wallet_configs.into_iter().enumerate()
154        {
155            tracing::debug!("Adding wallet account {index} with {assets:?}");
156
157            let mut rng = ChaCha20Rng::from_seed(rand::random());
158            let secret_key = SecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
159            let auth = AuthScheme::RpoFalcon512 { pub_key: secret_key.public_key() };
160            let init_seed: [u8; 32] = rng.random();
161
162            let account_type = if has_updatable_code {
163                AccountType::RegularAccountUpdatableCode
164            } else {
165                AccountType::RegularAccountImmutableCode
166            };
167            let account_storage_mode = storage_mode.into();
168            let (mut wallet_account, wallet_account_seed) =
169                create_basic_wallet(init_seed, auth, account_type, account_storage_mode)?;
170
171            // Add fungible assets and track the faucet adjustments per faucet/asset.
172            let wallet_fungible_asset_update =
173                prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
174
175            // Force the account nonce to 1.
176            //
177            // By convention, a nonce of zero indicates a freshly generated local account that has
178            // yet to be deployed. An account is deployed onchain along with its first
179            // transaction which results in a non-zero nonce onchain.
180            //
181            // The genesis block is special in that accounts are "deployed" without transactions and
182            // therefore we need bump the nonce manually to uphold this invariant.
183            let wallet_delta = AccountDelta::new(
184                wallet_account.id(),
185                AccountStorageDelta::default(),
186                AccountVaultDelta::new(
187                    wallet_fungible_asset_update,
188                    NonFungibleAssetDelta::default(),
189                ),
190                ONE,
191            )?;
192
193            wallet_account.apply_delta(&wallet_delta)?;
194
195            debug_assert_eq!(wallet_account.nonce(), ONE);
196
197            secrets.push((
198                format!("wallet_{index:0zero_padding_width$}.mac"),
199                wallet_account.id(),
200                secret_key,
201                wallet_account_seed,
202            ));
203
204            wallet_accounts.push(wallet_account);
205        }
206
207        let mut all_accounts = Vec::<Account>::new();
208        // Apply all fungible faucet adjustments to the respective faucet
209        for (symbol, mut faucet_account) in faucet_accounts {
210            let faucet_id = faucet_account.id();
211            // If there is no account using the asset, we use an empty delta to set the
212            // nonce to `ONE`.
213            let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
214
215            let mut storage_delta = AccountStorageDelta::default();
216
217            if total_issuance != 0 {
218                // slot 0
219                storage_delta.set_item(
220                    memory::FAUCET_STORAGE_DATA_SLOT,
221                    [ZERO, ZERO, ZERO, Felt::new(total_issuance)],
222                );
223                tracing::debug!(
224                    "Reducing faucet account {faucet} for {symbol} by {amount}",
225                    faucet = faucet_id.to_hex(),
226                    symbol = symbol,
227                    amount = total_issuance
228                );
229            } else {
230                tracing::debug!(
231                    "No wallet is referencing {faucet} for {symbol}",
232                    faucet = faucet_id.to_hex(),
233                    symbol = symbol,
234                );
235            }
236
237            faucet_account.apply_delta(&AccountDelta::new(
238                faucet_id,
239                storage_delta,
240                AccountVaultDelta::default(),
241                ONE,
242            )?)?;
243
244            debug_assert_eq!(faucet_account.nonce(), ONE);
245
246            // sanity check the total issuance against
247            let basic = BasicFungibleFaucet::try_from(&faucet_account)?;
248            let max_supply = basic.max_supply().inner();
249            if max_supply < total_issuance {
250                return Err(GenesisConfigError::MaxIssuanceExceeded {
251                    max_supply,
252                    symbol: TokenSymbol::new(&symbol)?,
253                    total_issuance,
254                });
255            }
256
257            all_accounts.push(faucet_account);
258        }
259        // Ensure the faucets always precede the wallets referencing them
260        all_accounts.extend(wallet_accounts);
261
262        Ok((
263            GenesisState {
264                accounts: all_accounts,
265                version,
266                timestamp,
267            },
268            AccountSecrets { secrets },
269        ))
270    }
271}
272
273// FUNGIBLE FAUCET CONFIG
274// ================================================================================================
275
276/// Represents a faucet with asset specific properties
277#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
278#[serde(deny_unknown_fields)]
279pub struct FungibleFaucetConfig {
280    // TODO eventually directly parse to `TokenSymbol`
281    symbol: String,
282    decimals: u8,
283    /// Max supply in full token units
284    ///
285    /// It will be converted internally to the smallest representable unit,
286    /// using based `10.powi(decimals)` as a multiplier.
287    max_supply: u64,
288    #[serde(default)]
289    storage_mode: StorageMode,
290}
291
292// WALLET CONFIG
293// ================================================================================================
294
295/// Represents a wallet, containing a set of assets
296#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
297#[serde(deny_unknown_fields)]
298pub struct WalletConfig {
299    #[serde(default)]
300    has_updatable_code: bool,
301    #[serde(default)]
302    storage_mode: StorageMode,
303    assets: Vec<AssetEntry>,
304}
305
306#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
307struct AssetEntry {
308    symbol: String,
309    /// The amount of full token units the given asset is populated with
310    amount: u64,
311}
312
313// STORAGE MODE
314// ================================================================================================
315
316/// See the [full description](https://0xmiden.github.io/miden-base/account.html?highlight=Accoun#account-storage-mode)
317/// for details
318#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
319pub enum StorageMode {
320    /// Monitor for `Notes` related to the account, in addition to being `Public`.
321    #[serde(alias = "network")]
322    #[default]
323    Network,
324    /// A publicly stored account, lives on-chain.
325    #[serde(alias = "public")]
326    Public,
327    /// A private account, which must be known by interactors.
328    #[serde(alias = "private")]
329    Private,
330}
331
332impl From<StorageMode> for AccountStorageMode {
333    fn from(mode: StorageMode) -> AccountStorageMode {
334        match mode {
335            StorageMode::Network => AccountStorageMode::Network,
336            StorageMode::Private => AccountStorageMode::Private,
337            StorageMode::Public => AccountStorageMode::Public,
338        }
339    }
340}
341
342// ACCOUNTS
343// ================================================================================================
344
345#[derive(Debug, Clone)]
346pub struct AccountFileWithName {
347    pub name: String,
348    pub account_file: AccountFile,
349}
350
351/// Secrets generated during the state generation
352#[derive(Debug, Clone)]
353pub struct AccountSecrets {
354    // name, account, private key, account seed
355    pub secrets: Vec<(String, AccountId, SecretKey, Word)>,
356}
357
358impl AccountSecrets {
359    /// Convert the internal tuple into an `AccountFile`
360    ///
361    /// If no name is present, a new one is generated based on the current time
362    /// and the index in
363    pub fn as_account_files(
364        &self,
365        genesis_state: &GenesisState,
366    ) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + use<'_> {
367        let account_lut = HashMap::<AccountId, Account>::from_iter(
368            genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
369        );
370        self.secrets.iter().map(move |(name, account_id, secret_key, account_seed)| {
371            let account = account_lut
372                .get(account_id)
373                .ok_or(GenesisConfigError::MissingGenesisAccount { account_id: *account_id })?;
374            let account_file = AccountFile::new(
375                account.clone(),
376                Some(*account_seed),
377                vec![AuthSecretKey::RpoFalcon512(secret_key.clone())],
378            );
379            let name = name.to_string();
380            Ok(AccountFileWithName { name, account_file })
381        })
382    }
383}
384
385// HELPERS
386// ================================================================================================
387
388/// Process wallet assets and return them as a fungible asset delta.
389/// Track the negative adjustments for the respective faucets.
390fn prepare_fungible_asset_update(
391    assets: impl IntoIterator<Item = AssetEntry>,
392    faucets: &HashMap<String, Account>,
393    faucet_issuance: &mut HashMap<AccountId, u64>,
394) -> Result<FungibleAssetDelta, GenesisConfigError> {
395    let assets =
396        Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
397            let token_symbol = TokenSymbol::new(&symbol)?;
398            let faucet_account = faucets.get(&symbol).ok_or_else(|| {
399                GenesisConfigError::MissingFaucetDefinition { symbol: token_symbol }
400            })?;
401
402            Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
403        }))?;
404
405    let mut wallet_asset_delta = FungibleAssetDelta::default();
406    assets
407        .into_iter()
408        .try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
409
410    wallet_asset_delta.iter().try_for_each(|(faucet_id, amount)| {
411        let issuance: &mut u64 = faucet_issuance.entry(*faucet_id).or_default();
412        tracing::debug!(
413            "Updating faucet issuance {faucet} with {issuance} += {amount}",
414            faucet = faucet_id.to_hex()
415        );
416
417        // check against total supply is deferred
418        issuance
419            .checked_add_assign(
420                &u64::try_from(*amount)
421                    .expect("Issuance must always be positive in the scope of genesis config"),
422            )
423            .map_err(|_| GenesisConfigError::IssuanceOverflow)?;
424
425        Ok::<_, GenesisConfigError>(())
426    })?;
427
428    Ok(wallet_asset_delta)
429}