use crate::{
Block,
Ledger,
Transaction,
Transmission,
TransmissionID,
narwhal::{BatchCertificate, BatchHeader, Subdag},
puzzle::Solution,
store::{ConsensusStore, helpers::memory::ConsensusMemory},
};
use snarkvm_console::{
account::{Address, PrivateKey},
network::MainnetV0,
prelude::*,
};
use snarkvm_synthesizer::vm::VM;
use aleo_std::StorageMode;
use anyhow::{Context, Result};
use indexmap::{IndexMap, IndexSet};
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use std::collections::{BTreeMap, HashMap};
use time::OffsetDateTime;
pub type CurrentNetwork = MainnetV0;
#[cfg(not(feature = "rocks"))]
pub type LedgerType<N> = snarkvm_ledger_store::helpers::memory::ConsensusMemory<N>;
#[cfg(feature = "rocks")]
pub type LedgerType<N> = snarkvm_ledger_store::helpers::rocksdb::ConsensusDB<N>;
pub struct TestChainBuilder<N: Network> {
private_keys: Vec<PrivateKey<N>>,
ledger: Ledger<N, ConsensusMemory<N>>,
last_block_round: u64,
round_to_certificates: HashMap<u64, IndexMap<usize, BatchCertificate<N>>>,
previous_leader_certificate: Option<BatchCertificate<N>>,
last_batch_round: HashMap<usize, u64>,
last_committed_batch_round: HashMap<usize, u64>,
genesis_block: Block<N>,
}
#[derive(Clone)]
pub struct GenerateBlocksOptions<N: Network> {
pub skip_votes: bool,
pub skip_nodes: Vec<usize>,
pub skip_to_current_version: bool,
pub num_validators: usize,
pub transactions: Vec<Transaction<N>>,
}
impl<N: Network> Default for GenerateBlocksOptions<N> {
fn default() -> Self {
Self {
skip_votes: false,
skip_nodes: Default::default(),
skip_to_current_version: false,
num_validators: 0,
transactions: Default::default(),
}
}
}
#[derive(Clone)]
pub struct GenerateBlockOptions<N: Network> {
pub skip_votes: bool,
pub skip_nodes: Vec<usize>,
pub timestamp: i64,
pub solutions: Vec<Solution<N>>,
pub transactions: Vec<Transaction<N>>,
}
impl<N: Network> Default for GenerateBlockOptions<N> {
fn default() -> Self {
Self {
skip_votes: false,
skip_nodes: Default::default(),
transactions: Default::default(),
solutions: Default::default(),
timestamp: OffsetDateTime::now_utc().unix_timestamp(),
}
}
}
impl<N: Network> TestChainBuilder<N> {
pub fn initialize_components(committee_size: usize, rng: &mut TestRng) -> Result<(Vec<PrivateKey<N>>, Block<N>)> {
let private_key = PrivateKey::<N>::new(rng)?;
let store = ConsensusStore::<_, ConsensusMemory<_>>::open(StorageMode::new_test(None))
.with_context(|| "Failed to initialize consensus store")?;
let seed: u64 = rng.r#gen();
trace!("Using seed {seed} and key {} for genesis RNG", private_key);
let genesis_rng = &mut TestRng::from_seed(seed);
let genesis_block = VM::from(store).unwrap().genesis_beacon(&private_key, genesis_rng)?;
let genesis_rng = &mut TestRng::from_seed(seed);
let private_keys = (0..committee_size).map(|_| PrivateKey::new(genesis_rng).unwrap()).collect();
trace!(
"Generated genesis block ({}) and private keys for all {committee_size} committee members",
genesis_block.hash()
);
Ok((private_keys, genesis_block))
}
pub fn new(rng: &mut TestRng) -> Result<Self> {
Self::new_with_quorum_size(4, rng)
}
pub fn new_with_quorum_size(num_validators: usize, rng: &mut TestRng) -> Result<Self> {
let (private_keys, genesis) = Self::initialize_components(num_validators, rng)?;
Self::from_components(private_keys, genesis)
}
pub fn new_with_quorum_size_and_genesis_block(num_validators: usize, genesis_path: String) -> Result<Self> {
let buffer = std::fs::read(genesis_path)?;
let genesis = Block::from_bytes_le(&buffer)?;
pub const DEVELOPMENT_MODE_RNG_SEED: u64 = 1234567890u64;
let mut rng = ChaChaRng::seed_from_u64(DEVELOPMENT_MODE_RNG_SEED);
let private_keys = (0..num_validators).map(|_| PrivateKey::new(&mut rng).unwrap()).collect();
Self::from_components(private_keys, genesis)
}
pub fn from_components(private_keys: Vec<PrivateKey<N>>, genesis_block: Block<N>) -> Result<Self> {
let ledger = Ledger::<N, ConsensusMemory<N>>::load(genesis_block.clone(), StorageMode::new_test(None))
.with_context(|| "Failed to set up ledger for test chain")?;
ensure!(ledger.genesis_block == genesis_block);
Self::from_genesis(private_keys, genesis_block)
}
pub fn from_genesis(private_keys: Vec<PrivateKey<N>>, genesis_block: Block<N>) -> Result<Self> {
let ledger = Ledger::<N, ConsensusMemory<N>>::load(genesis_block.clone(), StorageMode::new_test(None))
.with_context(|| "Failed to set up ledger for test chain")?;
Ok(Self {
private_keys,
ledger,
genesis_block,
last_batch_round: Default::default(),
last_committed_batch_round: Default::default(),
last_block_round: 0,
round_to_certificates: Default::default(),
previous_leader_certificate: Default::default(),
})
}
pub fn generate_blocks(&mut self, num_blocks: usize, rng: &mut TestRng) -> Result<Vec<Block<N>>> {
let num_validators = self.private_keys.len();
self.generate_blocks_with_opts(num_blocks, GenerateBlocksOptions { num_validators, ..Default::default() }, rng)
}
pub fn generate_blocks_with_opts(
&mut self,
num_blocks: usize,
mut options: GenerateBlocksOptions<N>,
rng: &mut TestRng,
) -> Result<Vec<Block<N>>> {
assert!(num_blocks > 0, "Need to build at least one block");
let mut result = vec![];
if options.skip_to_current_version {
let (version, target_height) = TEST_CONSENSUS_VERSION_HEIGHTS.last().unwrap();
let mut current_height = self.ledger.latest_height();
let diff = target_height.saturating_sub(current_height);
if diff > 0 {
println!("Skipping {diff} blocks to reach {version}");
while current_height < *target_height && result.len() < num_blocks {
let options = GenerateBlockOptions {
skip_votes: options.skip_votes,
skip_nodes: options.skip_nodes.clone(),
..Default::default()
};
let block = self.generate_block_with_opts(options, rng)?;
current_height = block.height();
result.push(block);
}
println!("Advanced to the current consensus version at height {target_height}");
} else {
debug!("Already at the current consensus version. No blocks to skip.");
}
}
while result.len() < num_blocks {
let num_txs = (BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH * options.num_validators)
.min(options.transactions.len());
let options = GenerateBlockOptions {
skip_votes: options.skip_votes,
skip_nodes: options.skip_nodes.clone(),
transactions: options.transactions.drain(..num_txs).collect(),
..Default::default()
};
let block = self.generate_block_with_opts(options, rng)?;
result.push(block);
}
Ok(result)
}
pub fn generate_block(&mut self, rng: &mut TestRng) -> Result<Block<N>> {
self.generate_block_with_opts(GenerateBlockOptions::default(), rng)
}
pub fn generate_block_with_opts(
&mut self,
options: GenerateBlockOptions<N>,
rng: &mut TestRng,
) -> Result<Block<N>> {
assert!(
options.skip_nodes.len() * 3 < self.private_keys.len(),
"Cannot mark more than f nodes as unavailable/skipped"
);
let next_block_round = self.last_block_round + 2;
let mut cert_count = 0;
let mut round = next_block_round.checked_sub(BatchHeader::<N>::MAX_GC_ROUNDS as u64).unwrap_or(1).max(1);
let mut transmissions = IndexMap::default();
for txn in options.transactions {
let txn_id = txn.id();
let transmission = Transmission::from(txn);
let transmission_id = TransmissionID::Transaction(txn_id, transmission.to_checksum().unwrap().unwrap());
transmissions.insert(transmission_id, transmission);
}
for solution in options.solutions {
let transmission = Transmission::from(solution);
let transmission_id = TransmissionID::Solution(solution.id(), transmission.to_checksum().unwrap().unwrap());
transmissions.insert(transmission_id, transmission);
}
loop {
let mut created_anchor = false;
let previous_certificate_ids = if round == 1 {
IndexSet::default()
} else {
self.round_to_certificates
.get(&(round - 1))
.unwrap()
.iter()
.filter_map(|(_, cert)| {
let skip = if let Some(leader) = &self.previous_leader_certificate {
options.skip_votes && leader.id() == cert.id()
} else {
false
};
if skip { None } else { Some(cert.id()) }
})
.collect()
};
let committee = self.ledger.get_committee_lookback_for_round(round).unwrap().unwrap_or_else(|| {
panic!("No committee for round {round}");
});
for (key1_idx, private_key_1) in self.private_keys.iter().enumerate() {
if options.skip_nodes.contains(&key1_idx) {
continue;
}
if self.last_batch_round.get(&key1_idx).unwrap_or(&0) >= &round {
continue;
}
let transmission_ids: IndexSet<_> = transmissions
.keys()
.skip(key1_idx * BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH)
.take(BatchHeader::<N>::MAX_TRANSMISSIONS_PER_BATCH)
.copied()
.collect();
let batch_header = BatchHeader::new(
private_key_1,
round,
options.timestamp,
committee.id(),
transmission_ids.clone(),
previous_certificate_ids.clone(),
rng,
)
.unwrap();
let signatures = self
.private_keys
.iter()
.enumerate()
.filter(|&(key2_idx, _)| key1_idx != key2_idx)
.map(|(_, private_key_2)| private_key_2.sign(&[batch_header.batch_id()], rng).unwrap())
.collect();
self.last_batch_round.insert(key1_idx, round);
self.round_to_certificates
.entry(round)
.or_default()
.insert(key1_idx, BatchCertificate::from(batch_header, signatures).unwrap());
cert_count += 1;
if round % 2 == 0 {
let leader = committee.get_leader(round).unwrap();
if leader == Address::try_from(private_key_1).unwrap() {
created_anchor = true;
}
}
}
if created_anchor && round % 2 == 0 && self.last_block_round < round {
self.last_block_round = round;
break;
}
round += 1;
}
let commit_round = round;
let leader_committee = self.ledger.get_committee_lookback_for_round(round).unwrap().unwrap();
let leader = leader_committee.get_leader(commit_round).unwrap();
let (leader_idx, leader_certificate) =
self.round_to_certificates.get(&commit_round).unwrap().iter().find(|(_, c)| c.author() == leader).unwrap();
let leader_idx = *leader_idx;
let leader_certificate = leader_certificate.clone();
let mut subdag_map = BTreeMap::new();
let start_round = if commit_round < BatchHeader::<CurrentNetwork>::MAX_GC_ROUNDS as u64 {
1
} else {
commit_round - BatchHeader::<CurrentNetwork>::MAX_GC_ROUNDS as u64 + 2
};
for round in start_round..commit_round {
let mut to_insert = IndexSet::new();
for idx in 0..self.private_keys.len() {
let cround = self.last_committed_batch_round.entry(idx).or_default();
if *cround >= round {
continue;
}
if let Some(cert) = self.round_to_certificates.entry(round).or_default().get(&idx) {
to_insert.insert(cert.clone());
*cround = round;
}
}
if !to_insert.is_empty() {
subdag_map.insert(round, to_insert);
}
}
subdag_map.insert(commit_round, [leader_certificate.clone()].into());
self.last_committed_batch_round.insert(leader_idx, commit_round);
trace!("Generated {cert_count} certificates for the next block");
let subdag = Subdag::from(subdag_map).unwrap();
let block = self.ledger.prepare_advance_to_next_quorum_block(subdag, transmissions, rng)?;
trace!("Generated new block {} at height {}", block.hash(), block.height());
self.ledger
.advance_to_next_block(&block)
.with_context(|| "Failed to (internally) advance to generated block")?;
self.previous_leader_certificate = Some(leader_certificate.clone());
trace!("Updated internal ledger to height {}", block.height());
Ok(block)
}
pub fn genesis_block(&self) -> &Block<N> {
&self.genesis_block
}
pub fn private_keys(&self) -> &[PrivateKey<N>] {
&self.private_keys
}
pub fn validator_key(&self, index: usize) -> &PrivateKey<N> {
&self.private_keys[index]
}
pub fn validator_address(&self, index: usize) -> Address<N> {
Address::try_from(*self.validator_key(index)).unwrap()
}
pub fn instantiate_ledger(&self) -> Ledger<N, LedgerType<N>> {
Ledger::load(self.genesis_block().clone(), StorageMode::new_test(None)).unwrap()
}
}