use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use indexmap::IndexMap;
use miden_node_utils::crypto::get_rpo_random_coin;
use miden_node_utils::signer::BlockSigner;
use miden_protocol::account::auth::{AuthScheme, AuthSecretKey};
use miden_protocol::account::{
Account,
AccountBuilder,
AccountDelta,
AccountFile,
AccountId,
AccountStorageDelta,
AccountStorageMode,
AccountType,
AccountVaultDelta,
FungibleAssetDelta,
NonFungibleAssetDelta,
};
use miden_protocol::asset::{FungibleAsset, TokenSymbol};
use miden_protocol::block::FeeParameters;
use miden_protocol::crypto::dsa::falcon512_poseidon2::SecretKey as RpoSecretKey;
use miden_protocol::errors::TokenSymbolError;
use miden_protocol::{Felt, ONE};
use miden_standards::AuthMethod;
use miden_standards::account::auth::AuthSingleSig;
use miden_standards::account::faucets::{BasicFungibleFaucet, TokenMetadata};
use miden_standards::account::mint_policies::AuthControlled;
use miden_standards::account::wallets::create_basic_wallet;
use rand::distr::weighted::Weight;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::{Deserialize, Serialize};
use crate::GenesisState;
mod errors;
use self::errors::GenesisConfigError;
#[cfg(test)]
mod tests;
const DEFAULT_NATIVE_FAUCET_SYMBOL: &str = "MIDEN";
const DEFAULT_NATIVE_FAUCET_DECIMALS: u8 = 6;
const DEFAULT_NATIVE_FAUCET_MAX_SUPPLY: u64 = 100_000_000_000_000_000;
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct GenericAccountConfig {
path: PathBuf,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct GenesisConfig {
version: u32,
timestamp: u32,
#[serde(default)]
native_faucet: Option<PathBuf>,
fee_parameters: FeeParameterConfig,
#[serde(default)]
wallet: Vec<WalletConfig>,
#[serde(default)]
fungible_faucet: Vec<FungibleFaucetConfig>,
#[serde(default)]
account: Vec<GenericAccountConfig>,
#[serde(skip)]
config_dir: PathBuf,
}
impl Default for GenesisConfig {
fn default() -> Self {
Self {
version: 1_u32,
timestamp: u32::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Time does not go backwards")
.as_secs(),
)
.expect("Timestamp should fit into u32"),
wallet: vec![],
native_faucet: None,
fee_parameters: FeeParameterConfig { verification_base_fee: 0 },
fungible_faucet: vec![],
account: vec![],
config_dir: PathBuf::from("."),
}
}
}
impl GenesisConfig {
pub fn read_toml_file(path: &Path) -> Result<Self, GenesisConfigError> {
let toml_str = fs_err::read_to_string(path)
.map_err(|e| GenesisConfigError::ConfigFileRead(e, path.to_path_buf()))?;
let config_dir = path.parent().expect("config file path must have a parent directory");
Self::read_toml(&toml_str, config_dir)
}
fn read_toml(toml_str: &str, config_dir: &Path) -> Result<Self, GenesisConfigError> {
let mut config: Self = toml::from_str(toml_str)?;
config.config_dir = config_dir.to_path_buf();
Ok(config)
}
#[expect(clippy::too_many_lines)]
pub fn into_state<S>(
self,
signer: S,
) -> Result<(GenesisState<S>, AccountSecrets), GenesisConfigError> {
let GenesisConfig {
version,
timestamp,
native_faucet,
fee_parameters,
fungible_faucet: fungible_faucet_configs,
wallet: wallet_configs,
account: account_entries,
config_dir,
} = self;
let file_loaded_accounts = account_entries
.into_iter()
.map(|acc| {
let full_path = config_dir.join(&acc.path);
let account_file = AccountFile::read(&full_path)
.map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
Ok(account_file.account)
})
.collect::<Result<Vec<_>, GenesisConfigError>>()?;
let mut wallet_accounts = Vec::<Account>::new();
let mut faucet_accounts = IndexMap::<TokenSymbolStr, Account>::new();
let mut secrets = Vec::new();
let (native_faucet_account, symbol, native_secret) =
NativeFaucetConfig(native_faucet).build_account(&config_dir)?;
if let Some(secret_key) = native_secret {
secrets.push((
format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
native_faucet_account.id(),
secret_key,
));
}
let native_faucet_account_id = native_faucet_account.id();
faucet_accounts.insert(symbol.clone(), native_faucet_account);
for fungible_faucet_config in fungible_faucet_configs {
let symbol = fungible_faucet_config.symbol.clone();
let (faucet_account, secret_key) = fungible_faucet_config.build_account()?;
if faucet_accounts.insert(symbol.clone(), faucet_account.clone()).is_some() {
return Err(GenesisConfigError::DuplicateFaucetDefinition { symbol });
}
secrets.push((
format!("faucet_{symbol}.mac", symbol = symbol.to_string().to_lowercase()),
faucet_account.id(),
secret_key,
));
}
let fee_parameters =
FeeParameters::new(native_faucet_account_id, fee_parameters.verification_base_fee)?;
let mut faucet_issuance = IndexMap::<AccountId, u64>::new();
let zero_padding_width = usize::ilog10(std::cmp::max(10, wallet_configs.len())) as usize;
for (index, WalletConfig { has_updatable_code, storage_mode, assets }) in
wallet_configs.into_iter().enumerate()
{
tracing::debug!(index, assets = ?assets, "Adding wallet account");
let mut rng = ChaCha20Rng::from_seed(rand::random());
let secret_key = RpoSecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
let auth = AuthMethod::SingleSig {
approver: (secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2),
};
let init_seed: [u8; 32] = rng.random();
let account_type = if has_updatable_code {
AccountType::RegularAccountUpdatableCode
} else {
AccountType::RegularAccountImmutableCode
};
let account_storage_mode = storage_mode.into();
let mut wallet_account =
create_basic_wallet(init_seed, auth, account_type, account_storage_mode)?;
let wallet_fungible_asset_update =
prepare_fungible_asset_update(assets, &faucet_accounts, &mut faucet_issuance)?;
let wallet_delta = AccountDelta::new(
wallet_account.id(),
AccountStorageDelta::default(),
AccountVaultDelta::new(
wallet_fungible_asset_update,
NonFungibleAssetDelta::default(),
),
ONE,
)?;
wallet_account.apply_delta(&wallet_delta)?;
debug_assert_eq!(wallet_account.nonce(), ONE);
secrets.push((
format!("wallet_{index:0zero_padding_width$}.mac"),
wallet_account.id(),
secret_key,
));
wallet_accounts.push(wallet_account);
}
let mut all_accounts = Vec::<Account>::new();
for (symbol, mut faucet_account) in faucet_accounts {
let faucet_id = faucet_account.id();
let total_issuance = faucet_issuance.get(&faucet_id).copied().unwrap_or_default();
let mut storage_delta = AccountStorageDelta::default();
if total_issuance != 0 {
let current_metadata = TokenMetadata::try_from(faucet_account.storage())?;
let updated_metadata =
current_metadata.with_token_supply(Felt::new(total_issuance))?;
storage_delta
.set_item(TokenMetadata::metadata_slot().clone(), updated_metadata.into())?;
tracing::debug!(
"Reducing faucet account {faucet} for {symbol} by {amount}",
faucet = faucet_id.to_hex(),
symbol = symbol,
amount = total_issuance
);
} else {
tracing::debug!(
"No wallet is referencing {faucet} for {symbol}",
faucet = faucet_id.to_hex(),
symbol = symbol,
);
}
faucet_account.apply_delta(&AccountDelta::new(
faucet_id,
storage_delta,
AccountVaultDelta::default(),
ONE,
)?)?;
debug_assert_eq!(faucet_account.nonce(), ONE);
let basic = BasicFungibleFaucet::try_from(&faucet_account)?;
let max_supply = basic.max_supply().as_canonical_u64();
if max_supply < total_issuance {
return Err(GenesisConfigError::MaxIssuanceExceeded {
max_supply,
symbol,
total_issuance,
});
}
all_accounts.push(faucet_account);
}
all_accounts.extend(wallet_accounts);
all_accounts.extend(file_loaded_accounts);
Ok((
GenesisState {
fee_parameters,
accounts: all_accounts,
version,
timestamp,
block_signer: signer,
},
AccountSecrets { secrets },
))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FeeParameterConfig {
verification_base_fee: u32,
}
struct NativeFaucetConfig(Option<PathBuf>);
impl NativeFaucetConfig {
fn build_account(
self,
config_dir: &Path,
) -> Result<(Account, TokenSymbolStr, Option<RpoSecretKey>), GenesisConfigError> {
match self.0 {
None => {
let symbol = TokenSymbolStr::from_str(DEFAULT_NATIVE_FAUCET_SYMBOL).unwrap();
let faucet_config = FungibleFaucetConfig {
symbol: symbol.clone(),
decimals: DEFAULT_NATIVE_FAUCET_DECIMALS,
max_supply: DEFAULT_NATIVE_FAUCET_MAX_SUPPLY,
storage_mode: StorageMode::Public,
};
let (account, secret_key) = faucet_config.build_account()?;
Ok((account, symbol, Some(secret_key)))
},
Some(path) => {
let full_path = config_dir.join(&path);
let account_file = AccountFile::read(&full_path)
.map_err(|e| GenesisConfigError::AccountFileRead(e, full_path.clone()))?;
let account = account_file.account;
if account.id().account_type() != AccountType::FungibleFaucet {
return Err(GenesisConfigError::NativeFaucetNotFungible { path: full_path });
}
let faucet = BasicFungibleFaucet::try_from(&account)
.expect("validated as fungible faucet above");
let symbol = TokenSymbolStr::from(faucet.symbol().clone());
Ok((account, symbol, None))
},
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FungibleFaucetConfig {
symbol: TokenSymbolStr,
decimals: u8,
max_supply: u64,
#[serde(default)]
storage_mode: StorageMode,
}
impl FungibleFaucetConfig {
fn build_account(self) -> Result<(Account, RpoSecretKey), GenesisConfigError> {
let FungibleFaucetConfig {
symbol,
decimals,
max_supply,
storage_mode,
} = self;
let mut rng = ChaCha20Rng::from_seed(rand::random());
let secret_key = RpoSecretKey::with_rng(&mut get_rpo_random_coin(&mut rng));
let auth =
AuthSingleSig::new(secret_key.public_key().into(), AuthScheme::Falcon512Poseidon2);
let init_seed: [u8; 32] = rng.random();
let max_supply = Felt::try_from(max_supply)
.expect("The `Felt::MODULUS` is _always_ larger than the `max_supply`");
let component = BasicFungibleFaucet::new(symbol.as_ref().clone(), decimals, max_supply)?;
let faucet_account = AccountBuilder::new(init_seed)
.account_type(AccountType::FungibleFaucet)
.storage_mode(storage_mode.into())
.with_auth_component(auth)
.with_component(component)
.with_component(AuthControlled::allow_all())
.build()?;
debug_assert_eq!(faucet_account.nonce(), Felt::ZERO);
Ok((faucet_account, secret_key))
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WalletConfig {
#[serde(default)]
has_updatable_code: bool,
#[serde(default)]
storage_mode: StorageMode,
assets: Vec<AssetEntry>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct AssetEntry {
symbol: TokenSymbolStr,
amount: u64,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
pub enum StorageMode {
#[serde(alias = "network")]
#[default]
Network,
#[serde(alias = "public")]
Public,
#[serde(alias = "private")]
Private,
}
impl From<StorageMode> for AccountStorageMode {
fn from(mode: StorageMode) -> AccountStorageMode {
match mode {
StorageMode::Network => AccountStorageMode::Network,
StorageMode::Private => AccountStorageMode::Private,
StorageMode::Public => AccountStorageMode::Public,
}
}
}
#[derive(Debug, Clone)]
pub struct AccountFileWithName {
pub name: String,
pub account_file: AccountFile,
}
#[derive(Debug, Clone)]
pub struct AccountSecrets {
pub secrets: Vec<(String, AccountId, RpoSecretKey)>,
}
impl AccountSecrets {
pub fn as_account_files(
&self,
genesis_state: &GenesisState<impl BlockSigner>,
) -> impl Iterator<Item = Result<AccountFileWithName, GenesisConfigError>> + '_ {
let account_lut = IndexMap::<AccountId, Account>::from_iter(
genesis_state.accounts.iter().map(|account| (account.id(), account.clone())),
);
self.secrets.iter().cloned().map(move |(name, account_id, secret_key)| {
let account = account_lut
.get(&account_id)
.ok_or(GenesisConfigError::MissingGenesisAccount { account_id })?;
let account_file = AccountFile::new(
account.clone(),
vec![AuthSecretKey::Falcon512Poseidon2(secret_key)],
);
Ok(AccountFileWithName { name, account_file })
})
}
}
fn prepare_fungible_asset_update(
assets: impl IntoIterator<Item = AssetEntry>,
faucets: &IndexMap<TokenSymbolStr, Account>,
faucet_issuance: &mut IndexMap<AccountId, u64>,
) -> Result<FungibleAssetDelta, GenesisConfigError> {
let assets =
Result::<Vec<_>, _>::from_iter(assets.into_iter().map(|AssetEntry { amount, symbol }| {
let faucet_account = faucets.get(&symbol).ok_or_else(|| {
GenesisConfigError::MissingFaucetDefinition { symbol: symbol.clone() }
})?;
Ok::<_, GenesisConfigError>(FungibleAsset::new(faucet_account.id(), amount)?)
}))?;
let mut wallet_asset_delta = FungibleAssetDelta::default();
assets
.into_iter()
.try_for_each(|fungible_asset| wallet_asset_delta.add(fungible_asset))?;
wallet_asset_delta.iter().try_for_each(|(vault_key, amount)| {
let faucet_id = vault_key.faucet_id();
let issuance: &mut u64 = faucet_issuance.entry(faucet_id).or_default();
tracing::debug!(
"Updating faucet issuance {faucet} with {issuance} += {amount}",
faucet = faucet_id.to_hex()
);
issuance
.checked_add_assign(
&u64::try_from(*amount)
.expect("Issuance must always be positive in the scope of genesis config"),
)
.map_err(|_| GenesisConfigError::IssuanceOverflow)?;
Ok::<_, GenesisConfigError>(())
})?;
Ok(wallet_asset_delta)
}
#[derive(Debug, Clone, PartialEq)]
pub struct TokenSymbolStr {
raw: String,
encoded: TokenSymbol,
}
impl AsRef<TokenSymbol> for TokenSymbolStr {
fn as_ref(&self) -> &TokenSymbol {
&self.encoded
}
}
impl std::fmt::Display for TokenSymbolStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.raw)
}
}
impl FromStr for TokenSymbolStr {
type Err = TokenSymbolError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
encoded: TokenSymbol::new(s)?,
raw: s.to_string(),
})
}
}
impl Eq for TokenSymbolStr {}
impl From<TokenSymbolStr> for TokenSymbol {
fn from(value: TokenSymbolStr) -> Self {
value.encoded
}
}
impl From<TokenSymbol> for TokenSymbolStr {
fn from(symbol: TokenSymbol) -> Self {
let raw = symbol.to_string();
Self { raw, encoded: symbol }
}
}
impl Ord for TokenSymbolStr {
fn cmp(&self, other: &Self) -> Ordering {
self.raw.cmp(&other.raw)
}
}
impl PartialOrd for TokenSymbolStr {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl std::hash::Hash for TokenSymbolStr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.raw.hash::<H>(state);
}
}
impl Serialize for TokenSymbolStr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.raw)
}
}
impl<'de> Deserialize<'de> for TokenSymbolStr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(TokenSymbolVisitor)
}
}
use serde::de::Visitor;
struct TokenSymbolVisitor;
impl Visitor<'_> for TokenSymbolVisitor {
type Value = TokenSymbolStr;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("1 to 6 uppercase ascii letters")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let encoded = TokenSymbol::new(v).map_err(|e| E::custom(format!("{e}")))?;
let raw = v.to_string();
Ok(TokenSymbolStr { raw, encoded })
}
}