use std::collections::{BTreeMap, BTreeSet};
use std::marker::PhantomData;
use std::path::Path;
use namada_macros::BorshDeserializer;
#[cfg(feature = "migrations")]
use namada_migrations::*;
use namada_sdk::address::Address;
use namada_sdk::borsh::{BorshDeserialize, BorshSerialize};
use namada_sdk::dec::Dec;
use namada_sdk::eth_bridge::storage::parameters::{
Contracts, Erc20WhitelistEntry, MinimumConfirmations,
};
use namada_sdk::parameters::ProposalBytes;
use namada_sdk::token::{
Amount, DenominatedAmount, Denomination, NATIVE_MAX_DECIMAL_PLACES,
};
use namada_sdk::{ethereum_structs, token};
use serde::{Deserialize, Serialize};
use super::transactions::{self, Transactions};
use super::utils::{read_toml, write_toml};
use crate::config::genesis::chain::DeriveEstablishedAddress;
use crate::config::genesis::transactions::{BondTx, SignedBondTx};
use crate::wallet::Alias;
pub const BALANCES_FILE_NAME: &str = "balances.toml";
pub const PARAMETERS_FILE_NAME: &str = "parameters.toml";
pub const VPS_FILE_NAME: &str = "validity-predicates.toml";
pub const TOKENS_FILE_NAME: &str = "tokens.toml";
pub const TRANSACTIONS_FILE_NAME: &str = "transactions.toml";
const MAX_TOKEN_BALANCE_SUM: u64 = i64::MAX as u64;
pub fn read_balances(path: &Path) -> eyre::Result<UndenominatedBalances> {
read_toml(path, "Balances")
}
pub fn read_parameters(path: &Path) -> eyre::Result<Parameters<Unvalidated>> {
read_toml(path, "Parameters")
}
pub fn read_validity_predicates(
path: &Path,
) -> eyre::Result<ValidityPredicates> {
read_toml(path, "Validity predicates")
}
pub fn read_tokens(path: &Path) -> eyre::Result<Tokens> {
read_toml(path, "Tokens")
}
pub fn read_transactions(
path: &Path,
) -> eyre::Result<Transactions<Unvalidated>> {
read_toml(path, "Transactions")
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct UndenominatedBalances {
pub token: BTreeMap<Alias, RawTokenBalances>,
}
impl UndenominatedBalances {
pub fn denominate(
self,
tokens: &Tokens,
) -> eyre::Result<DenominatedBalances> {
let mut balances = DenominatedBalances {
token: BTreeMap::new(),
};
for (alias, bals) in self.token {
let denom = tokens
.token
.get(&alias)
.ok_or_else(|| {
eyre::eyre!(
"A balance of token {} was found, but this token was \
not found in the `tokens.toml` file",
alias
)
})?
.denom;
let mut denominated_bals = BTreeMap::new();
for (addr, bal) in bals.0.into_iter() {
let denominated = bal.increase_precision(denom)?;
denominated_bals.insert(addr, denominated);
}
balances
.token
.insert(alias, TokenBalances(denominated_bals));
}
Ok(balances)
}
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct DenominatedBalances {
pub token: BTreeMap<Alias, TokenBalances>,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct RawTokenBalances(pub BTreeMap<Address, token::DenominatedAmount>);
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct TokenBalances(pub BTreeMap<Address, token::DenominatedAmount>);
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct ValidityPredicates {
pub wasm: BTreeMap<String, WasmVpConfig>,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct WasmVpConfig {
pub filename: String,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct Tokens {
pub token: BTreeMap<Alias, TokenConfig>,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct TokenConfig {
pub denom: Denomination,
pub masp_params: Option<token::ShieldedParams>,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct Parameters<T: TemplateValidation> {
pub parameters: ChainParams<T>,
pub pos_params: PosParams,
pub gov_params: GovernanceParams,
pub pgf_params: PgfParams<T>,
pub eth_bridge_params: Option<EthBridgeParams>,
pub ibc_params: IbcParams,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct ChainParams<T: TemplateValidation> {
pub max_tx_bytes: u32,
pub native_token: Alias,
pub is_native_token_transferable: bool,
pub min_num_of_blocks: u64,
pub max_proposal_bytes: ProposalBytes,
#[serde(alias = "vp_whitelist")] pub vp_allowlist: Option<Vec<String>>,
#[serde(alias = "tx_whitelist")] pub tx_allowlist: Option<Vec<String>>,
pub implicit_vp: String,
pub epochs_per_year: u64,
pub masp_epoch_multiplier: u64,
pub max_block_gas: u64,
pub masp_fee_payment_gas_limit: u64,
pub gas_scale: u64,
pub minimum_gas_price: T::GasMinimums,
}
impl ChainParams<Unvalidated> {
pub fn denominate(
self,
tokens: &Tokens,
) -> eyre::Result<ChainParams<Validated>> {
let ChainParams {
max_tx_bytes,
native_token,
is_native_token_transferable,
min_num_of_blocks,
max_proposal_bytes,
vp_allowlist,
tx_allowlist,
implicit_vp,
epochs_per_year,
masp_epoch_multiplier,
max_block_gas,
masp_fee_payment_gas_limit,
gas_scale,
minimum_gas_price,
} = self;
let mut min_gas_prices = BTreeMap::default();
for (token, amount) in minimum_gas_price.into_iter() {
let denom = if let Some(TokenConfig { denom, .. }) =
tokens.token.get(&token)
{
*denom
} else {
eprintln!(
"Genesis files contained minimum gas amount of token {}, \
which is not in the `tokens.toml` file",
token
);
return Err(eyre::eyre!(
"Genesis files contained minimum gas amount of token {}, \
which is not in the `tokens.toml` file",
token
));
};
let amount = amount.increase_precision(denom).map_err(|e| {
eprintln!(
"A minimum gas amount in the parameters.toml file was \
incorrectly formatted:\n{}",
e
);
e
})?;
min_gas_prices.insert(token, amount);
}
Ok(ChainParams {
max_tx_bytes,
native_token,
is_native_token_transferable,
min_num_of_blocks,
max_proposal_bytes,
vp_allowlist,
tx_allowlist,
implicit_vp,
epochs_per_year,
masp_epoch_multiplier,
max_block_gas,
masp_fee_payment_gas_limit,
gas_scale,
minimum_gas_price: min_gas_prices,
})
}
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct PosParams {
pub max_validator_slots: u64,
pub pipeline_len: u64,
pub unbonding_len: u64,
pub tm_votes_per_token: Dec,
pub block_proposer_reward: Dec,
pub block_vote_reward: Dec,
pub max_inflation_rate: Dec,
pub target_staked_ratio: Dec,
pub duplicate_vote_min_slash_rate: Dec,
pub light_client_attack_min_slash_rate: Dec,
pub cubic_slashing_window_length: u64,
pub validator_stake_threshold: token::Amount,
pub liveness_window_check: u64,
pub liveness_threshold: Dec,
pub rewards_gain_p: Dec,
pub rewards_gain_d: Dec,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct GovernanceParams {
pub min_proposal_fund: u64,
pub max_proposal_code_size: u64,
pub min_proposal_voting_period: u64,
pub max_proposal_period: u64,
pub max_proposal_content_size: u64,
pub min_proposal_grace_epochs: u64,
pub max_proposal_latency: u64,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct PgfParams<T: TemplateValidation> {
pub stewards: BTreeSet<Address>,
pub pgf_inflation_rate: Dec,
pub stewards_inflation_rate: Dec,
pub maximum_number_of_stewards: u64,
#[serde(default)]
#[serde(skip_serializing)]
#[cfg(test)]
pub valid: PhantomData<T>,
#[serde(default)]
#[serde(skip_serializing)]
#[cfg(not(test))]
valid: PhantomData<T>,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct EthBridgeParams {
pub eth_start_height: ethereum_structs::BlockHeight,
pub min_confirmations: MinimumConfirmations,
pub erc20_whitelist: Vec<Erc20WhitelistEntry>,
pub contracts: Contracts,
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct IbcParams {
pub default_mint_limit: token::Amount,
pub default_per_epoch_throughput_limit: token::Amount,
}
impl TokenBalances {
pub fn get(&self, addr: &Address) -> Option<token::Amount> {
self.0.get(addr).map(|amt| amt.amount())
}
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
pub struct Unvalidated {}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshDeserializer,
BorshSerialize,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
pub struct Validated {}
pub trait TemplateValidation: Serialize {
type Amount: for<'a> Deserialize<'a>
+ Serialize
+ Into<token::DenominatedAmount>
+ Clone
+ std::fmt::Debug
+ BorshSerialize
+ BorshDeserialize
+ PartialEq
+ Eq
+ PartialOrd
+ Ord;
type Balances: for<'a> Deserialize<'a>
+ Serialize
+ Clone
+ std::fmt::Debug
+ BorshSerialize
+ BorshDeserialize
+ PartialEq
+ Eq;
type BondTx: for<'a> Deserialize<'a>
+ Serialize
+ Clone
+ std::fmt::Debug
+ BorshSerialize
+ BorshDeserialize
+ PartialEq
+ Eq
+ PartialOrd
+ Ord;
type GasMinimums: for<'a> Deserialize<'a>
+ Serialize
+ Clone
+ std::fmt::Debug
+ BorshSerialize
+ BorshDeserialize
+ PartialEq
+ Eq;
}
impl TemplateValidation for Unvalidated {
type Amount = token::DenominatedAmount;
type Balances = UndenominatedBalances;
type BondTx = SignedBondTx<Unvalidated>;
type GasMinimums = BTreeMap<Alias, DenominatedAmount>;
}
impl TemplateValidation for Validated {
type Amount = token::DenominatedAmount;
type Balances = DenominatedBalances;
type BondTx = BondTx<Validated>;
type GasMinimums = BTreeMap<Alias, DenominatedAmount>;
}
#[derive(
Clone,
Debug,
Deserialize,
Serialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
)]
pub struct All<T: TemplateValidation> {
pub vps: ValidityPredicates,
pub tokens: Tokens,
pub balances: T::Balances,
pub parameters: Parameters<T>,
pub transactions: Transactions<T>,
}
impl<T: TemplateValidation> All<T> {
pub fn write_toml_files(&self, output_dir: &Path) -> eyre::Result<()> {
let All {
vps,
tokens,
balances,
parameters,
transactions,
} = self;
let vps_file = output_dir.join(VPS_FILE_NAME);
let tokens_file = output_dir.join(TOKENS_FILE_NAME);
let balances_file = output_dir.join(BALANCES_FILE_NAME);
let parameters_file = output_dir.join(PARAMETERS_FILE_NAME);
let transactions_file = output_dir.join(TRANSACTIONS_FILE_NAME);
write_toml(vps, &vps_file, "Validity predicates")?;
write_toml(tokens, &tokens_file, "Tokens")?;
write_toml(balances, &balances_file, "Balances")?;
write_toml(parameters, ¶meters_file, "Parameters")?;
write_toml(transactions, &transactions_file, "Transactions")?;
Ok(())
}
}
impl All<Unvalidated> {
pub fn read_toml_files(input_dir: &Path) -> eyre::Result<Self> {
let vps_file = input_dir.join(VPS_FILE_NAME);
let tokens_file = input_dir.join(TOKENS_FILE_NAME);
let balances_file = input_dir.join(BALANCES_FILE_NAME);
let parameters_file = input_dir.join(PARAMETERS_FILE_NAME);
let transactions_file = input_dir.join(TRANSACTIONS_FILE_NAME);
let vps = read_toml(&vps_file, "Validity predicates")?;
let tokens = read_toml(&tokens_file, "Tokens")?;
let balances = read_toml(&balances_file, "Balances")?;
let parameters = read_toml(¶meters_file, "Parameters")?;
let transactions = read_toml(&transactions_file, "Transactions")?;
Ok(Self {
vps,
tokens,
balances,
parameters,
transactions,
})
}
}
pub fn load_and_validate(templates_dir: &Path) -> Option<All<Validated>> {
let mut is_valid = true;
let vps_file = templates_dir.join(VPS_FILE_NAME);
let tokens_file = templates_dir.join(TOKENS_FILE_NAME);
let balances_file = templates_dir.join(BALANCES_FILE_NAME);
let parameters_file = templates_dir.join(PARAMETERS_FILE_NAME);
let transactions_file = templates_dir.join(TRANSACTIONS_FILE_NAME);
let mut check_file_exists = |file: &Path, name: &str| {
if !file.exists() {
is_valid = false;
eprintln!("{name} file is missing at {}", file.to_string_lossy());
}
};
check_file_exists(&vps_file, "Validity predicates");
check_file_exists(&tokens_file, "Tokens");
check_file_exists(&balances_file, "Balances");
check_file_exists(¶meters_file, "Parameters");
check_file_exists(&transactions_file, "Transactions");
let vps = read_validity_predicates(&vps_file);
let tokens = read_tokens(&tokens_file);
let balances = read_balances(&balances_file);
let parameters = read_parameters(¶meters_file);
let transactions = read_transactions(&transactions_file);
let eprintln_invalid_file = |err: &eyre::Report, name: &str| {
eprintln!("{name} file is NOT valid. Failed to read with: {err:?}");
};
let vps = vps.map_or_else(
|err| {
eprintln_invalid_file(&err, "Validity predicates");
None
},
Some,
);
let tokens = tokens.map_or_else(
|err| {
eprintln_invalid_file(&err, "Tokens");
None
},
Some,
);
let balances = balances.map_or_else(
|err| {
eprintln_invalid_file(&err, "Balances");
None
},
Some,
);
let parameters = parameters.map_or_else(
|err| {
eprintln_invalid_file(&err, "Parameters");
None
},
Some,
);
let transactions = transactions.map_or_else(
|err| {
eprintln_invalid_file(&err, "Transactions");
None
},
Some,
);
if let Some(vps) = vps.as_ref() {
if validate_vps(vps) {
println!("Validity predicates file is valid.");
} else {
is_valid = false;
}
}
let (parameters, native_token) = if let Some(parameters) = parameters {
let params = validate_parameters(
parameters,
&tokens,
&transactions,
vps.as_ref(),
);
if let Some(validate_params) = params.clone() {
println!("Parameters file is valid.");
(params, Some(validate_params.parameters.native_token))
} else {
is_valid = false;
(params, None)
}
} else {
(None, None)
};
let balances = if let Some(tokens) = tokens.as_ref() {
if tokens.token.is_empty() {
is_valid = false;
eprintln!(
"Tokens file is invalid. There has to be at least one token."
);
}
println!("Tokens file is valid.");
balances
.and_then(|raw| raw.denominate(tokens).ok())
.and_then(|balances| {
validate_balances(&balances, Some(tokens), native_token).then(
|| {
println!("Balances file is valid.");
balances
},
)
})
} else {
None
};
if balances.is_none() {
is_valid = false;
}
let txs = if let Some(txs) = transactions.and_then(|txs| {
transactions::validate(
txs,
vps.as_ref(),
balances.as_ref(),
parameters.as_ref(),
)
}) {
println!("Transactions file is valid.");
Some(txs)
} else {
is_valid = false;
None
};
match vps {
Some(vps) if is_valid => Some(All {
vps,
tokens: tokens.unwrap(),
balances: balances.unwrap(),
parameters: parameters.unwrap(),
transactions: txs.unwrap(),
}),
_ => None,
}
}
pub fn validate_vps(vps: &ValidityPredicates) -> bool {
let mut is_valid = true;
vps.wasm.iter().for_each(|(name, config)| {
if !config.filename.ends_with(".wasm") {
eprintln!(
"Invalid validity predicate \"{name}\" configuration. Only \
\".wasm\" filenames are currently supported."
);
is_valid = false;
}
});
is_valid
}
pub fn validate_parameters(
parameters: Parameters<Unvalidated>,
tokens: &Option<Tokens>,
transactions: &Option<Transactions<Unvalidated>>,
vps: Option<&ValidityPredicates>,
) -> Option<Parameters<Validated>> {
let tokens = tokens.as_ref()?;
let txs = transactions.as_ref()?;
let mut is_valid = true;
let implicit_vp = ¶meters.parameters.implicit_vp;
if !vps
.map(|vps| vps.wasm.contains_key(implicit_vp))
.unwrap_or_default()
{
eprintln!(
"Implicit VP \"{implicit_vp}\" not found in the Validity \
predicates files."
);
is_valid = false;
}
for steward in ¶meters.pgf_params.stewards {
let mut found_steward = false;
if let Some(accs) = &txs.established_account {
if accs.iter().any(|acct| {
let addr = acct.derive_address();
&addr == steward
}) {
found_steward = true;
}
}
is_valid = found_steward && is_valid;
if !is_valid {
eprintln!(
"Could not find an established or validator account \
associated with the PGF steward {}",
steward
);
}
}
let Parameters {
parameters,
pos_params,
gov_params,
pgf_params,
eth_bridge_params,
ibc_params,
} = parameters;
match parameters.denominate(tokens) {
Err(e) => {
eprintln!("{}", e);
None
}
Ok(parameters) => is_valid.then(|| Parameters {
parameters,
pos_params,
gov_params,
pgf_params: PgfParams {
stewards: pgf_params.stewards,
pgf_inflation_rate: pgf_params.pgf_inflation_rate,
stewards_inflation_rate: pgf_params.stewards_inflation_rate,
maximum_number_of_stewards: pgf_params
.maximum_number_of_stewards,
valid: Default::default(),
},
eth_bridge_params,
ibc_params,
}),
}
}
pub fn validate_balances(
balances: &DenominatedBalances,
tokens: Option<&Tokens>,
native_alias: Option<Alias>,
) -> bool {
let mut is_valid = true;
balances.token.iter().for_each(|(token, next)| {
if !tokens
.as_ref()
.map(|tokens| tokens.token.contains_key(token))
.unwrap_or_default()
{
is_valid = false;
eprintln!(
"Token \"{token}\" from the Balances file is not present in \
the Tokens file."
)
}
let sum = next.0.values().try_fold(
token::Amount::default(),
|acc, amount| {
let res = acc.checked_add(amount.amount());
if res.as_ref().is_none() {
is_valid = false;
eprintln!(
"Balances for token {token} overflow `token::Amount`"
);
}
res
},
);
if Some(token) == native_alias.as_ref() {
match sum {
Some(sum) if sum.is_positive() => {
if sum
> Amount::from_uint(
MAX_TOKEN_BALANCE_SUM,
NATIVE_MAX_DECIMAL_PLACES,
)
.unwrap()
{
eprintln!(
"The sum of balances for token {token} is greater \
than {MAX_TOKEN_BALANCE_SUM}"
);
is_valid = false;
}
}
_ => {
eprintln!(
"Native token {token} balance is zero at genesis."
);
is_valid = false;
}
}
}
});
is_valid
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use namada_sdk::key;
use namada_sdk::key::RefTo;
use tempfile::tempdir;
use super::*;
#[test]
fn test_validate_localnet_genesis_templates() {
let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("genesis/localnet");
assert!(
load_and_validate(&templates_dir).is_some(),
"Localnet genesis templates must be valid"
);
}
#[test]
fn test_validate_starter_genesis_templates() {
let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("genesis/starter");
assert!(
load_and_validate(&templates_dir).is_some(),
"Starter genesis templates must be valid"
);
}
#[test]
fn test_validate_hardware_genesis_templates() {
let templates_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("genesis/hardware");
assert!(
load_and_validate(&templates_dir).is_some(),
"Hardware genesis templates must be valid"
);
}
#[test]
fn test_read_balances() {
let test_dir = tempdir().unwrap();
let path = test_dir.path().join(BALANCES_FILE_NAME);
let sk = key::testing::keypair_1();
let pk = sk.ref_to();
let address: Address = (&pk).into();
let balance = token::Amount::from(101_000_001);
let token_alias = Alias::from("Some_token".to_string());
let contents = format!(
r#"
[token.{token_alias}]
{address} = "{}"
"#,
balance.to_string_native()
);
fs::write(&path, contents).unwrap();
let balances = read_balances(&path).unwrap();
let example_balance = balances.token.get(&token_alias).unwrap();
assert_eq!(balance, example_balance.0.get(&address).unwrap().amount());
}
}