use std::{fs, path::PathBuf, str::FromStr};
use aleo_std::StorageMode;
use anyhow::{anyhow, ensure, Result};
use clap::Parser;
use colored::Colorize;
use indexmap::IndexMap;
use rand::{CryptoRng, Rng, SeedableRng};
use rand_chacha::ChaChaRng;
use serde::{de::DeserializeOwned, Serialize};
use snarkvm::{
ledger::{
committee::MIN_VALIDATOR_STAKE,
store::{helpers::memory::ConsensusMemory, ConsensusStore},
Header, Ratify, Solutions,
},
synthesizer::program::FinalizeGlobalState,
utilities::ToBytes,
};
use crate::{
ledger::util::public_transaction, Address, Block, CTRecord, Committee, DbLedger, MemVM,
Network, NetworkId, PTRecord, PrivateKey, Transaction, ViewKey,
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AddressMap<N: Network, T: DeserializeOwned>(IndexMap<Address<N>, T>);
impl<N: Network, T: DeserializeOwned> FromStr for AddressMap<N, T> {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(AddressMap(serde_json::from_str(s)?))
}
}
#[derive(Debug, Clone, Parser)]
pub struct Genesis<N: Network> {
#[clap(short, long)]
pub genesis_key: Option<PrivateKey<N>>,
#[clap(short, long, default_value = "genesis.block")]
pub output: PathBuf,
#[clap(long, default_value_t = 4)]
pub committee_size: u16,
#[clap(long)]
pub committee_output: Option<PathBuf>,
#[clap(long, default_value_t = 0)]
pub additional_accounts: u16,
#[clap(
name = "additional-accounts-balance",
long,
default_value_t = 100_000_000
)]
pub additional_accounts_balance: u64,
#[clap(long)]
pub additional_accounts_record_balance: Option<u64>,
#[clap(long)]
pub additional_accounts_output: Option<PathBuf>,
#[clap(long)]
pub seed: Option<u64>,
#[clap(long, default_value_t = 10_000_000_000_000)]
pub bonded_balance: u64,
#[clap(long)]
pub bonded_balances: Option<AddressMap<N, u64>>,
#[clap(long)]
pub bonded_withdrawal: Option<AddressMap<N, Address<N>>>,
#[clap(long, default_value_t = 0)]
pub bonded_commission: u8,
#[clap(long)]
pub bonded_commissions: Option<AddressMap<N, u8>>,
#[clap(long)]
pub ledger: Option<PathBuf>,
}
pub fn genesis_quorum<R: Rng + CryptoRng, N: Network>(
vm: &MemVM<N>,
private_key: &PrivateKey<N>,
committee: Committee<N>,
public_balances: IndexMap<Address<N>, u64>,
bonded_balances: IndexMap<Address<N>, (Address<N>, Address<N>, u64)>,
transactions: Vec<Transaction<N>>,
rng: &mut R,
) -> Result<Block<N>> {
let total_stake = committee.total_stake();
let account_supply = public_balances.values().try_fold(0u64, |acc, x| {
acc.checked_add(*x).ok_or(anyhow!("Invalid account supply"))
})?;
let total_supply = total_stake
.checked_add(account_supply)
.ok_or_else(|| anyhow!("Invalid total supply"))?;
ensure!(
total_supply == N::STARTING_SUPPLY,
"Invalid total supply. Found {total_supply}, expected {}",
N::STARTING_SUPPLY
);
let ratifications = vec![Ratify::Genesis(
Box::new(committee),
Box::new(public_balances),
Box::new(bonded_balances),
)];
let solutions = Solutions::from(None);
let aborted_solution_ids = vec![];
let state = FinalizeGlobalState::new_genesis::<N>()?;
let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm
.speculate(
state,
None,
ratifications,
&solutions,
transactions.iter(),
rng,
)?;
ensure!(
aborted_transaction_ids.is_empty(),
"Failed to initialize a genesis block - found aborted transactionIDs"
);
let header = Header::genesis(&ratifications, &transactions, ratified_finalize_operations)?; let previous_hash = N::BlockHash::default();
let block = Block::new_beacon(
private_key,
previous_hash,
header,
ratifications,
solutions,
aborted_solution_ids,
transactions,
aborted_transaction_ids,
rng,
)?;
Ok(block)
}
impl<N: Network> Genesis<N> {
pub fn parse(self) -> Result<()> {
let mut rng = ChaChaRng::seed_from_u64(self.seed.unwrap_or(1234567890u64));
tracing::trace!(
"Generating genesis block for network {}",
NetworkId::from_network::<N>()
);
let genesis_key = match self.genesis_key {
Some(genesis_key) => genesis_key,
None => PrivateKey::new(&mut rng)?,
};
let genesis_addr = Address::try_from(&genesis_key)?;
let get_commission = |addr| {
self.bonded_commissions
.as_ref()
.and_then(|commissions| commissions.0.get(&addr))
.copied()
.unwrap_or(self.bonded_commission)
.clamp(0, 100)
};
let get_withdrawal = |addr| {
self.bonded_withdrawal
.as_ref()
.and_then(|withdrawals| withdrawals.0.get(&addr))
.copied()
.unwrap_or(addr)
};
let (mut committee_members, bonded_balances, members, mut public_balances) = match self
.bonded_balances
{
Some(balances) => {
ensure!(
balances.0.contains_key(&genesis_addr),
"The genesis address should be present in the passed-in bonded balances."
);
let mut bonded_balances = IndexMap::with_capacity(self.committee_size as usize);
let mut members = IndexMap::with_capacity(self.committee_size as usize);
for (addr, balance) in &balances.0 {
ensure!(
balance >= &MIN_VALIDATOR_STAKE,
"Validator stake is too low: {balance} < {MIN_VALIDATOR_STAKE}",
);
bonded_balances.insert(*addr, (*addr, get_withdrawal(*addr), *balance));
members.insert(*addr, (*balance, true, get_commission(*addr)));
}
(None, bonded_balances, members, balances.0)
}
None => {
ensure!(
self.bonded_balance >= MIN_VALIDATOR_STAKE,
"Validator stake is too low: {} < {MIN_VALIDATOR_STAKE}",
self.bonded_balance
);
let mut committee_members = IndexMap::with_capacity(self.committee_size as usize);
let mut bonded_balances = IndexMap::with_capacity(self.committee_size as usize);
let mut members = IndexMap::with_capacity(self.committee_size as usize);
let mut public_balances = IndexMap::with_capacity(self.committee_size as usize);
for i in 0..self.committee_size {
let (key, addr) = match i {
0 => (genesis_key, genesis_addr),
_ => {
let key = PrivateKey::new(&mut rng)?;
let addr = Address::try_from(&key)?;
(key, addr)
}
};
committee_members.insert(addr, (key, self.bonded_balance));
bonded_balances.insert(addr, (addr, get_withdrawal(addr), self.bonded_balance));
members.insert(addr, (self.bonded_balance, true, get_commission(addr)));
public_balances.insert(addr, self.bonded_balance);
}
(
Some(committee_members),
bonded_balances,
members,
public_balances,
)
}
};
let committee = Committee::new(0u64, members)?;
type Accounts<N> = IndexMap<Address<N>, (PrivateKey<N>, u64, Option<PTRecord<N>>)>;
let mut accounts: Accounts<N> = (0..self.additional_accounts)
.map(|_| {
let (key, addr) = loop {
let key = PrivateKey::new(&mut rng)?;
let addr = Address::try_from(&key)?;
if !bonded_balances.contains_key(&addr) {
break (key, addr);
}
};
let balance = self.additional_accounts_balance
+ self.additional_accounts_record_balance.unwrap_or(0);
public_balances.insert(addr, balance);
Ok((addr, (key, balance, None)))
})
.collect::<Result<IndexMap<_, _>>>()?;
let remaining_balance = N::STARTING_SUPPLY
.saturating_sub(committee.total_stake())
.saturating_sub(public_balances.values().sum());
if remaining_balance > 0 {
let balance = public_balances.get_mut(&genesis_addr).unwrap();
*balance += remaining_balance;
if let Some(ref mut committee_members) = committee_members {
let (_, balance) = committee_members.get_mut(&genesis_addr).unwrap();
*balance += remaining_balance;
}
}
let public_balances_sum: u64 = public_balances.values().sum();
if committee.total_stake() + public_balances_sum != N::STARTING_SUPPLY {
println!(
"Sum of committee stakes and public balances does not equal total starting supply:
{} + {public_balances_sum} != {}",
committee.total_stake(),
N::STARTING_SUPPLY
);
}
let compute_span = tracing::span!(tracing::Level::ERROR, "compute span").entered();
let vm = snarkvm::synthesizer::VM::from(ConsensusStore::<N, ConsensusMemory<_>>::open(
Some(0),
)?)?;
let mut txs = Vec::with_capacity(accounts.len());
if let Some(record_balance) = self.additional_accounts_record_balance {
accounts = accounts
.into_iter()
.map(|(addr, (key, balance, _))| {
let record_tx: Transaction<N> =
public_transaction::<N, ConsensusMemory<_>, N::Circuit>(
"transfer_public_to_private",
&vm,
addr,
record_balance,
key,
None,
)?;
let record_enc: CTRecord<N> = record_tx.records().next().unwrap().1.clone();
let record = record_enc.decrypt(&ViewKey::try_from(key)?)?;
txs.push(record_tx);
Ok((addr, (key, balance, Some(record))))
})
.collect::<Result<_>>()?;
}
let block = genesis_quorum(
&vm,
&genesis_key,
committee,
public_balances,
bonded_balances,
txs,
&mut rng,
)?;
compute_span.exit();
println!();
block.write_le(
fs::File::options()
.append(false)
.truncate(true)
.create(true)
.write(true)
.open(&self.output)?,
)?;
if self.genesis_key.is_none() {
println!(
"The genesis block private key is: {}",
genesis_key.to_string().cyan()
);
}
println!(
"Genesis block written to {}.",
self.output.display().to_string().yellow()
);
let output_json = self.output.with_extension("json");
serde_json::to_writer_pretty(
fs::File::options()
.append(false)
.truncate(true)
.create(true)
.write(true)
.open(&output_json)?,
&block,
)?;
println!(
"Genesis block JSON written to {}.",
output_json.display().to_string().yellow()
);
match (self.additional_accounts, self.additional_accounts_output) {
(0, _) => (),
(_, Some(accounts_file)) => {
let file = fs::File::options()
.append(false)
.create(true)
.truncate(true)
.write(true)
.open(&accounts_file)?;
serde_json::to_writer_pretty(file, &accounts)?;
println!(
"Additional accounts written to {}.",
accounts_file.display().to_string().yellow()
);
}
(_, None) => {
println!(
"Additional accounts (each given {} balance):",
self.additional_accounts_balance
);
for (addr, (key, _, _)) in accounts {
println!(
"\t{}: {}",
addr.to_string().yellow(),
key.to_string().cyan()
);
}
}
}
match (committee_members, self.committee_output) {
(Some(committee_members), Some(committee_file)) => {
let file = fs::File::options()
.append(false)
.create(true)
.truncate(true)
.write(true)
.open(&committee_file)?;
serde_json::to_writer_pretty(file, &committee_members)?;
println!(
"Generated committee written to {}.",
committee_file.display().to_string().yellow()
);
}
(Some(committee_members), None) => {
println!("Generated committee:");
for (addr, (key, _)) in committee_members {
println!(
"\t{}: {}",
addr.to_string().yellow(),
key.to_string().cyan()
);
}
}
_ => (),
}
if let Some(ledger) = self.ledger {
DbLedger::load(block.to_owned(), StorageMode::Custom(ledger.to_owned()))?;
println!(
"Initialized a ledger at {}.",
ledger.display().to_string().yellow()
);
}
println!();
println!("Genesis block hash: {}", block.hash().to_string().yellow());
Ok(())
}
}