/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use darkfi_sdk::{ crypto::{constants::MERKLE_DEPTH, MerkleNode}, incrementalmerkletree::bridgetree::BridgeTree, pasta::{group::ff::PrimeField, pallas}, }; use darkfi_serial::{deserialize, serialize, SerialDecodable, SerialEncodable}; use log::info; use rand::{thread_rng, Rng}; use sqlx::Row; use super::{ constants, lead_coin::{LeadCoin, LeadCoinSecrets}, utils::fbig2base, Block, BlockProposal, Float10, }; use crate::{ blockchain::Blockchain, net, tx::Transaction, util::time::{TimeKeeper, Timestamp}, wallet::WalletPtr, Error, Result, }; use std::{ fs::File, io::{prelude::*, BufWriter}, }; /// This struct represents the information required by the consensus algorithm pub struct ConsensusState { /// Wallet interface pub wallet: WalletPtr, /// Canonical (finalized) blockchain pub blockchain: Blockchain, /// Network bootstrap timestamp pub bootstrap_ts: Timestamp, /// Helper structure to calculate time related operations pub time_keeper: TimeKeeper, /// Genesis block hash pub genesis_block: blake3::Hash, /// Total sum of initial staking coins pub initial_distribution: u64, /// Flag to enable single-node mode pub single_node: bool, /// Slot the network was bootstrapped pub bootstrap_slot: u64, /// Participating start slot pub participating: Option, /// Node is able to propose proposals pub proposing: bool, /// Last slot node check for finalization pub checked_finalization: u64, /// Fork chains containing block proposals pub forks: Vec, /// Current epoch pub epoch: u64, /// Hot/live slot checkpoints pub slot_checkpoints: Vec, /// Last slot leaders count pub previous_leaders: u64, /// Controller output history pub f_history: Vec, /// Controller proportional error history pub err_history: Vec, // TODO: Aren't these already in db after finalization? /// Canonical competing coins pub coins: Vec, /// Canonical coin commitments tree pub coins_tree: BridgeTree, /// Canonical seen nullifiers from proposals pub nullifiers: Vec, } impl ConsensusState { pub fn new( wallet: WalletPtr, blockchain: Blockchain, bootstrap_ts: Timestamp, genesis_ts: Timestamp, genesis_data: blake3::Hash, initial_distribution: u64, single_node: bool, ) -> Self { let genesis_block = Block::genesis_block(genesis_ts, genesis_data).blockhash(); let time_keeper = TimeKeeper::new(genesis_ts, constants::EPOCH_LENGTH as u64, constants::SLOT_TIME, 0); Self { wallet, blockchain, bootstrap_ts, time_keeper, genesis_block, initial_distribution, single_node, bootstrap_slot: 0, participating: None, proposing: false, checked_finalization: 0, forks: vec![], epoch: 0, slot_checkpoints: vec![], previous_leaders: 0, f_history: vec![constants::FLOAT10_ZERO.clone()], err_history: vec![constants::FLOAT10_ZERO.clone(), constants::FLOAT10_ZERO.clone()], coins: vec![], coins_tree: BridgeTree::::new(constants::EPOCH_LENGTH * 100), nullifiers: vec![], } } /// Finds the last slot a proposal or block was generated. pub fn last_slot(&self) -> Result { let mut slot = 0; for chain in &self.forks { for state_checkpoint in &chain.sequence { if state_checkpoint.proposal.block.header.slot > slot { slot = state_checkpoint.proposal.block.header.slot; } } } // We return here in case proposals exist, // so we don't query the sled database. if slot > 0 { return Ok(slot) } let (last_slot, _) = self.blockchain.last()?; Ok(last_slot) } /// Set participating slot to next. pub fn set_participating(&mut self) -> Result<()> { self.participating = Some(self.time_keeper.current_slot() + 1); Ok(()) } /// Generate current slot checkpoint fn generate_slot_checkpoint( &mut self, fork_hashes: Vec, sigma1: pallas::Base, sigma2: pallas::Base, ) { let slot = self.time_keeper.current_slot(); let previous_eta = self.get_previous_eta(); let checkpoint = SlotCheckpoint { slot, previous_eta, fork_hashes, sigma1, sigma2 }; info!(target: "consensus::state", "generate_slot_checkpoint: {:?}", checkpoint); self.slot_checkpoints.push(checkpoint); } // Initialize node lead coins and set current epoch and eta. pub async fn init_coins(&mut self) -> Result<()> { self.epoch = self.time_keeper.current_epoch(); self.coins = self.create_coins().await?; self.update_forks_checkpoints(); Ok(()) } /// Check if new epoch has started and generate slot checkpoint. /// Returns flag to signify if epoch has changed. pub async fn epoch_changed( &mut self, fork_hashes: Vec, sigma1: pallas::Base, sigma2: pallas::Base, ) -> Result { self.generate_slot_checkpoint(fork_hashes, sigma1, sigma2); let epoch = self.time_keeper.current_epoch(); if epoch <= self.epoch { return Ok(false) } self.epoch = epoch; Ok(true) } /// Return 2-term target approximation sigma coefficients. pub fn sigmas(&mut self) -> (pallas::Base, pallas::Base) { let f = self.win_inv_prob_with_full_stake(); let total_stake = self.total_stake(); let total_sigma = Float10::try_from(total_stake).unwrap(); self.calc_sigmas(f, total_sigma) } fn calc_sigmas(&self, f: Float10, total_sigma: Float10) -> (pallas::Base, pallas::Base) { info!(target: "consensus::state", "sigmas(): f: {}", f); info!(target: "consensus::state", "sigmas(): total network stake: {:}", total_sigma); let one = constants::FLOAT10_ONE.clone(); let neg_one = constants::FLOAT10_NEG_ONE.clone(); let two = constants::FLOAT10_TWO.clone(); let field_p = Float10::try_from(constants::P).unwrap(); let x = one - f; let c = x.ln(); let neg_c = neg_one * c; let sigma1_fbig = neg_c.clone() / (total_sigma.clone() + constants::FLOAT10_EPSILON.clone()) * field_p.clone(); info!(target: "consensus::state", "sigma1_fbig: {:}", sigma1_fbig); let sigma1 = fbig2base(sigma1_fbig); let sigma2_fbig = (neg_c / (total_sigma + constants::FLOAT10_EPSILON.clone())) .powf(two.clone()) * (field_p / two); info!(target: "consensus::state", "sigma2_fbig: {:}", sigma2_fbig); let sigma2 = fbig2base(sigma2_fbig); (sigma1, sigma2) } /// Generate coins for provided sigmas. /// NOTE: The strategy here is having a single competing coin per slot. // TODO: DRK coin need to be burned, and consensus coin to be minted. async fn create_coins(&mut self) -> Result> { // TODO: cleanup LeadCoinSecrets, no need to keep a vector let (seeds, epoch_secrets) = { let mut rng = thread_rng(); let mut seeds: Vec = Vec::with_capacity(constants::EPOCH_LENGTH); for _ in 0..constants::EPOCH_LENGTH { seeds.push(rng.gen()); } (seeds, LeadCoinSecrets::generate()) }; // LeadCoin matrix containing node competing coins. let mut coins: Vec = Vec::with_capacity(constants::EPOCH_LENGTH); // Retrieve coin from wallet // NOTE: In future this will be retrieved from the money contract. // Get a wallet connection let mut conn = self.wallet.conn.acquire().await?; // Execute the query and see if we find any rows let query_str = format!("SELECT * FROM {}", constants::CONSENSUS_COIN_TABLE); let mut query = sqlx::query(&query_str); let coin = match query.fetch_one(&mut conn).await { Ok(row) => { let bytes: Vec = row.try_get(constants::CONSENSUS_COIN_COL)?; deserialize(&bytes)? } Err(_) => { // If no records are found, we generate a new coin and save it to the database info!(target: "consensus::state", "create_coins(): No LeadCoin was found in DB, generating new one..."); // Temporarily, we compete with fixed stake. // This stake should be based on how many nodes we want to run, and they all // must sum to initial distribution total coins. //let stake = self.initial_distribution; let c = LeadCoin::new( 0, self.time_keeper.current_slot(), epoch_secrets.secret_keys[0].inner(), epoch_secrets.merkle_roots[0], 0, epoch_secrets.merkle_paths[0].clone(), pallas::Base::from(seeds[0]), &mut self.coins_tree, ); let query_str = format!( "INSERT INTO {} ({}) VALUES (?1);", constants::CONSENSUS_COIN_TABLE, constants::CONSENSUS_COIN_COL ); query = sqlx::query(&query_str); query = query.bind(serialize(&c)); query.execute(&mut conn).await?; c } }; info!(target: "consensus::state", "create_coins(): Will use LeadCoin with value: {}", coin.value); coins.push(coin); Ok(coins) } /// Leadership reward, assuming constant reward /// TODO (res) implement reward mechanism with accord to DRK,DARK token-economics fn reward(&self) -> u64 { constants::REWARD } /// Auxillary function to calculate total slot rewards. fn slot_rewards(&self) -> u64 { // Retrieve existing blocks excluding genesis let blocks = (self.blockchain.len() as u64) - 1; // Retrieve longest fork length, to include those proposals in the calculation let max_fork_length = self.longest_chain_length() as u64; // Calculate rewarded slots let rewarded_slots = blocks + max_fork_length; rewarded_slots * self.reward() } /// Network total stake, assuming constant reward. /// Only used for fine-tuning. At genesis epoch first slot, of absolute index 0, /// if no stake was distributed, the total stake would be 0. /// To avoid division by zero, we asume total stake at first division is GENESIS_TOTAL_STAKE(1). fn total_stake(&self) -> u64 { let total_stake = self.slot_rewards() + self.initial_distribution; if total_stake == 0 { return constants::GENESIS_TOTAL_STAKE } total_stake } fn f_err(&mut self) -> Float10 { info!(target: "consensus::state", "Previous leaders: {}", self.previous_leaders); // Write counter to file let mut count_str: String = self.previous_leaders.to_string(); count_str.push(','); let f = File::options().append(true).create(true).open(constants::LEADER_HISTORY_LOG).unwrap(); { let mut writer = BufWriter::new(f); let _ = writer.write(&count_str.into_bytes()).unwrap(); } // Calculate feedback let feedback = Float10::try_from(self.previous_leaders as i64).unwrap(); // Reset previous leaders counter self.previous_leaders = 0; let target = constants::FLOAT10_ONE.clone(); target - feedback } fn discrete_pid(&mut self) -> Float10 { let k1 = constants::KP.clone() + constants::KI.clone() + constants::KD.clone(); let k2 = constants::FLOAT10_NEG_ONE.clone() * constants::KP.clone() + constants::FLOAT10_NEG_TWO.clone() * constants::KD.clone(); let k3 = constants::KD.clone(); let f_len = self.f_history.len(); let err = self.f_err(); let err_len = self.err_history.len(); let ret = self.f_history[f_len - 1].clone() + k1.clone() * err.clone() + k2.clone() * self.err_history[err_len - 1].clone() + k3.clone() * self.err_history[err_len - 2].clone(); info!(target: "consensus::state", "pid::f-1: {:}", self.f_history[f_len - 1].clone()); info!(target: "consensus::state", "pid::err: {:}", err); info!(target: "consensus::state", "pid::err-1: {}", self.err_history[err_len - 1].clone()); info!(target: "consensus::state", "pid::err-2: {}", self.err_history[err_len - 2].clone()); info!(target: "consensus::state", "pid::k1: {}", k1); info!(target: "consensus::state", "pid::k2: {}", k2); info!(target: "consensus::state", "pid::k3: {}", k3); self.err_history.push(err); ret } /// the probability inverse of winnig lottery having all the stake /// returns f fn win_inv_prob_with_full_stake(&mut self) -> Float10 { let mut f = self.discrete_pid(); if f <= constants::FLOAT10_ZERO.clone() { f = constants::MIN_F.clone() } else if f >= constants::FLOAT10_ONE.clone() { f = constants::MAX_F.clone() } // log f history let file = File::options().append(true).create(true).open(constants::F_HISTORY_LOG).unwrap(); { let mut f_history = format!("{:}", f); f_history.push(','); let mut writer = BufWriter::new(file); let _ = writer.write(&f_history.into_bytes()).unwrap(); } self.f_history.push(f.clone()); f } /// Check that the participant/stakeholder coins win the slot lottery. /// If the stakeholder has multiple competing winning coins, only the highest value /// coin is selected, since the stakeholder can't give more than one proof per block/slot. /// * 'sigma1', 'sigma2': slot sigmas /// Returns: (check: bool, idx: usize) where idx is the winning coin's index pub fn is_slot_leader( &mut self, sigma1: pallas::Base, sigma2: pallas::Base, ) -> (bool, i64, usize) { // Check if node can produce proposals if !self.proposing { return (false, 0, 0) } let fork_index = self.longest_chain_index(); let competing_coins = if fork_index == -1 { self.coins.clone() } else { self.forks[fork_index as usize].sequence.last().unwrap().coins.clone() }; // If on single-node mode, node always proposes by extending the // single fork it holds. if self.single_node { return (true, fork_index, 0) } let mut won = false; let mut highest_stake = 0; let mut highest_stake_idx = 0; let total_stake = self.total_stake(); for (winning_idx, coin) in competing_coins.iter().enumerate() { info!(target: "consensus::state", "is_slot_leader: coin stake: {:?}", coin.value); info!(target: "consensus::state", "is_slot_leader: total stake: {}", total_stake); info!(target: "consensus::state", "is_slot_leader: relative stake: {}", (coin.value as f64) / total_stake as f64); let first_winning = coin.is_leader( sigma1, sigma2, self.get_previous_eta(), pallas::Base::from(self.time_keeper.current_slot()), ); if first_winning && !won { highest_stake_idx = winning_idx; } won |= first_winning; if won && coin.value > highest_stake { highest_stake = coin.value; highest_stake_idx = winning_idx; } } (won, fork_index, highest_stake_idx) } /// Finds the longest forkchain the node holds and /// returns its index. pub fn longest_chain_index(&self) -> i64 { let mut length = 0; let mut index = -1; if !self.forks.is_empty() { for (i, chain) in self.forks.iter().enumerate() { if chain.sequence.len() > length { length = chain.sequence.len(); index = i as i64; } } } index } /// Finds the length of longest fork chain the node holds. pub fn longest_chain_length(&self) -> usize { let mut max = 0; for fork in &self.forks { if fork.sequence.len() > max { max = fork.sequence.len(); } } max } /// Given a proposal, find the index of the fork chain it extends. pub fn find_extended_chain_index(&mut self, proposal: &BlockProposal) -> Result { // We iterate through all forks to find which fork to extend let mut chain_index = -1; let mut state_checkpoint_index = 0; for (c_index, chain) in self.forks.iter().enumerate() { // Traverse sequence in reverse for (sc_index, state_checkpoint) in chain.sequence.iter().enumerate().rev() { if proposal.block.header.previous == state_checkpoint.proposal.hash { chain_index = c_index as i64; state_checkpoint_index = sc_index; break } } if chain_index != -1 { break } } // If no fork was found, we check with canonical if chain_index == -1 { let (last_slot, last_block) = self.blockchain.last()?; if proposal.block.header.previous != last_block || proposal.block.header.slot <= last_slot { info!(target: "consensus::state", "find_extended_chain_index(): Proposal doesn't extend any known chain"); return Ok(-2) } // Proposal extends canonical chain return Ok(-1) } // Found fork chain let chain = &self.forks[chain_index as usize]; // Proposal extends fork at last proposal if state_checkpoint_index == (chain.sequence.len() - 1) { return Ok(chain_index) } info!(target: "consensus::state", "find_extended_chain_index(): Proposal to fork a forkchain was received."); let mut chain = self.forks[chain_index as usize].clone(); // We keep all proposals until the one it extends chain.sequence.drain((state_checkpoint_index + 1)..); self.forks.push(chain); Ok(self.forks.len() as i64 - 1) } /// Search the chains we're holding for the given proposal. pub fn proposal_exists(&self, input_proposal: &blake3::Hash) -> bool { for chain in self.forks.iter() { for state_checkpoint in chain.sequence.iter().rev() { if input_proposal == &state_checkpoint.proposal.hash { return true } } } false } /// Utility function to extract leader selection lottery randomness(eta), /// defined as the hash of the last block, converted to pallas base. pub fn get_previous_eta(&self) -> pallas::Base { let (_, hash) = self.blockchain.last().unwrap(); let mut bytes: [u8; 32] = *hash.as_bytes(); // Read first 254 bits bytes[30] = 0; bytes[31] = 0; pallas::Base::from_repr(bytes).unwrap() } /// Auxillary function to retrieve slot checkpoint of provided slot UID. pub fn get_slot_checkpoint(&self, slot: u64) -> Result { // Check hot/live slot checkpoints for slot_checkpoint in self.slot_checkpoints.iter().rev() { if slot_checkpoint.slot == slot { return Ok(slot_checkpoint.clone()) } } // Check if slot is finalized if let Ok(slot_checkpoints) = self.blockchain.get_slot_checkpoints_by_slot(&[slot]) { if !slot_checkpoints.is_empty() { if let Some(slot_checkpoint) = &slot_checkpoints[0] { return Ok(slot_checkpoint.clone()) } } } Err(Error::SlotCheckpointNotFound(slot)) } /// Auxillary function to check if node has seen current or previous slot checkpoints. /// This check ensures that either the slots exist in memory or node has seen the finalization of these slots. pub fn slot_checkpoints_is_empty(&self) -> bool { let current_slot = self.time_keeper.current_slot(); if self.get_slot_checkpoint(current_slot).is_ok() { return false } let previous_slot = current_slot - 1; self.get_slot_checkpoint(previous_slot).is_err() } /// Auxillary function to update all fork state checkpoints to nodes coins current canonical states. /// Note: This function should only be invoked once on nodes' coins creation. pub fn update_forks_checkpoints(&mut self) { for fork in &mut self.forks { for state_checkpoint in &mut fork.sequence { state_checkpoint.coins = self.coins.clone(); state_checkpoint.coins_tree = self.coins_tree.clone(); } } } /// Retrieve current forks last proposal hashes. /// If node holds no fork, retrieve last canonical hash. pub fn fork_hashes(&self) -> Vec { let mut hashes = vec![]; for fork in &self.forks { hashes.push(fork.sequence.last().unwrap().proposal.hash); } if hashes.is_empty() { hashes.push(self.genesis_block); } hashes } /// Auxiliary structure to reset consensus state for a resync pub fn reset(&mut self) { self.participating = None; self.proposing = false; self.forks = vec![]; self.slot_checkpoints = vec![]; self.previous_leaders = 0; self.f_history = vec![constants::FLOAT10_ZERO.clone()]; self.err_history = vec![constants::FLOAT10_ZERO.clone(), constants::FLOAT10_ZERO.clone()]; self.nullifiers = vec![]; } } /// Auxiliary structure used for consensus syncing. #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct ConsensusRequest {} impl net::Message for ConsensusRequest { fn name() -> &'static str { "consensusrequest" } } /// Auxiliary structure used for consensus syncing. #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct ConsensusResponse { /// Slot the network was bootstrapped pub bootstrap_slot: u64, /// Current slot pub current_slot: u64, /// Hot/live data used by the consensus algorithm pub forks: Vec, /// Pending transactions pub pending_txs: Vec, /// Hot/live slot checkpoints pub slot_checkpoints: Vec, // TODO: When Float10 supports encoding/decoding this should be // replaced by directly using Vec /// Controller output history pub f_history: Vec, /// Controller proportional error history pub err_history: Vec, /// Seen nullifiers from proposals pub nullifiers: Vec, } impl net::Message for ConsensusResponse { fn name() -> &'static str { "consensusresponse" } } /// Auxiliary structure used for consensus syncing. #[derive(Debug, SerialEncodable, SerialDecodable)] pub struct ConsensusSlotCheckpointsRequest {} impl net::Message for ConsensusSlotCheckpointsRequest { fn name() -> &'static str { "consensusslotcheckpointsrequest" } } /// Auxiliary structure used for consensus syncing. #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct ConsensusSlotCheckpointsResponse { /// Node known bootstrap slot pub bootstrap_slot: u64, /// Node is able to propose proposals pub proposing: bool, /// Node has hot/live slot checkpoints pub is_empty: bool, } impl net::Message for ConsensusSlotCheckpointsResponse { fn name() -> &'static str { "consensusslotcheckpointsresponse" } } /// Auxiliary structure used to keep track of slot validation parameters. #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct SlotCheckpoint { /// Slot UID pub slot: u64, /// Previous slot eta pub previous_eta: pallas::Base, /// Previous slot forks last proposal/block hashes, /// as observed by the validator pub fork_hashes: Vec, /// Slot sigma1 pub sigma1: pallas::Base, /// Slot sigma2 pub sigma2: pallas::Base, } impl SlotCheckpoint { pub fn new( slot: u64, previous_eta: pallas::Base, fork_hashes: Vec, sigma1: pallas::Base, sigma2: pallas::Base, ) -> Self { Self { slot, previous_eta, fork_hashes, sigma1, sigma2 } } /// Generate the genesis slot checkpoint. pub fn genesis_slot_checkpoint() -> Self { let previous_eta = pallas::Base::zero(); let fork_hashes = vec![]; let sigma1 = pallas::Base::zero(); let sigma2 = pallas::Base::zero(); Self::new(0, previous_eta, fork_hashes, sigma1, sigma2) } } impl net::Message for SlotCheckpoint { fn name() -> &'static str { "slotcheckpoint" } } /// Auxiliary structure used for slot checkpoints syncing #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct SlotCheckpointRequest { /// Slot UID pub slot: u64, } impl net::Message for SlotCheckpointRequest { fn name() -> &'static str { "slotcheckpointrequest" } } /// Auxiliary structure used for slot checkpoints syncing #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct SlotCheckpointResponse { /// Response blocks. pub slot_checkpoints: Vec, } impl net::Message for SlotCheckpointResponse { fn name() -> &'static str { "slotcheckpointresponse" } } /// Auxiliary structure used to keep track of consensus state checkpoints. #[derive(Debug, Clone)] pub struct StateCheckpoint { /// Block proposal pub proposal: BlockProposal, /// Node competing coins current state pub coins: Vec, /// Coin commitments tree current state pub coins_tree: BridgeTree, /// Seen nullifiers from proposals current state pub nullifiers: Vec, } impl StateCheckpoint { pub fn new( proposal: BlockProposal, coins: Vec, coins_tree: BridgeTree, nullifiers: Vec, ) -> Self { Self { proposal, coins, coins_tree, nullifiers } } } /// Auxiliary structure used for forked consensus state checkpoints syncing #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct StateCheckpointInfo { /// Block proposal pub proposal: BlockProposal, /// Seen nullifiers from proposals current state pub nullifiers: Vec, } impl From for StateCheckpointInfo { fn from(state_checkpoint: StateCheckpoint) -> Self { Self { proposal: state_checkpoint.proposal, nullifiers: state_checkpoint.nullifiers } } } impl From for StateCheckpoint { fn from(state_checkpoint_info: StateCheckpointInfo) -> Self { Self { proposal: state_checkpoint_info.proposal, coins: vec![], coins_tree: BridgeTree::::new(constants::EPOCH_LENGTH * 100), nullifiers: state_checkpoint_info.nullifiers, } } } /// This struct represents a sequence of consensus state checkpoints. #[derive(Debug, Clone)] pub struct Fork { pub genesis_block: blake3::Hash, pub sequence: Vec, } impl Fork { pub fn new(genesis_block: blake3::Hash, initial_state_checkpoint: StateCheckpoint) -> Self { Self { genesis_block, sequence: vec![initial_state_checkpoint] } } /// Insertion of a valid state checkpoint. pub fn add(&mut self, state_checkpoint: &StateCheckpoint) { if self.check_state_checkpoint(state_checkpoint, self.sequence.last().unwrap()) { self.sequence.push(state_checkpoint.clone()); } } /// A fork chain is considered valid when every state checkpoint is valid, /// based on the `check_state_checkpoint` function pub fn check_chain(&self) -> bool { for (index, state_checkpoint) in self.sequence[1..].iter().enumerate() { if !self.check_state_checkpoint(state_checkpoint, &self.sequence[index]) { return false } } true } /// A state checkpoint is considered valid when its proposal parent hash is equal to the /// hash of the previous checkpoint's proposal and their slots are incremental, /// excluding the genesis block proposal. pub fn check_state_checkpoint( &self, state_checkpoint: &StateCheckpoint, previous: &StateCheckpoint, ) -> bool { if state_checkpoint.proposal.block.header.previous == self.genesis_block { info!(target: "consensus::state", "check_checkpoint(): Genesis block proposal provided."); return false } if state_checkpoint.proposal.block.header.previous != previous.proposal.hash || state_checkpoint.proposal.block.header.slot <= previous.proposal.block.header.slot { info!(target: "consensus::state", "check_checkpoint(): Provided state checkpoint proposal is invalid."); return false } // TODO: validate rest checkpoint info(like nullifiers) true } } /// Auxiliary structure used for forks syncing #[derive(Debug, Clone, SerialEncodable, SerialDecodable)] pub struct ForkInfo { pub genesis_block: blake3::Hash, pub sequence: Vec, } impl From for ForkInfo { fn from(fork: Fork) -> Self { let mut sequence = vec![]; for state_checkpoint in fork.sequence { sequence.push(state_checkpoint.into()); } Self { genesis_block: fork.genesis_block, sequence } } } impl From for Fork { fn from(fork_info: ForkInfo) -> Self { let mut sequence = vec![]; for checkpoint in fork_info.sequence { sequence.push(checkpoint.into()); } Self { genesis_block: fork_info.genesis_block, sequence } } } #[cfg(test)] mod tests { use crate::{ consensus::{ state::{Blockchain, ConsensusState}, utils::fbig2base, Float10, TESTNET_BOOTSTRAP_TIMESTAMP, TESTNET_GENESIS_HASH_BYTES, TESTNET_GENESIS_TIMESTAMP, TESTNET_INITIAL_DISTRIBUTION, }, wallet::WalletDb, Result, }; #[async_std::test] async fn calc_sigmas_test() -> Result<()> { // Generate dummy state let wallet = WalletDb::new("sqlite::memory:", "foo").await?; let sled_db = sled::Config::new().temporary(true).open()?; let blockchain = Blockchain::new(&sled_db, *TESTNET_GENESIS_TIMESTAMP, *TESTNET_GENESIS_HASH_BYTES)?; let state = ConsensusState::new( wallet, blockchain, *TESTNET_BOOTSTRAP_TIMESTAMP, *TESTNET_GENESIS_TIMESTAMP, *TESTNET_GENESIS_HASH_BYTES, *TESTNET_INITIAL_DISTRIBUTION, true, ); let precision_diff = Float10::try_from( "10000000000000000000000000000000000000000000000000000000000000000000000000", ) .unwrap(); let precision_diff_base = fbig2base(precision_diff); let f = Float10::try_from("0.01").unwrap(); let total_stake = Float10::try_from("100").unwrap(); let (sigma1, sigma2) = state.calc_sigmas(f, total_stake); let sigma1_rhs = Float10::try_from( "2909373465034095801035568917399197865646520818579502832252119592405565440", ) .unwrap(); let sigma1_rhs_base = fbig2base(sigma1_rhs); let sigma2_rhs = Float10::try_from( "9137556389643100714432609642916129738741963230846798778430644027392", ) .unwrap(); let sigma2_rhs_base = fbig2base(sigma2_rhs); let sigma1_delta = if sigma1_rhs_base > sigma1 { sigma1_rhs_base - sigma1 } else { sigma1 - sigma1_rhs_base }; let sigma2_delta = if sigma2_rhs_base > sigma2 { sigma2_rhs_base - sigma2 } else { sigma2 - sigma2_rhs_base }; //note! test cases were generated by low precision python scripts. //https://github.com/ertosns/lotterysim/blob/master/pallas_unittests.csv assert!(sigma1_delta < precision_diff_base); assert!(sigma2_delta < precision_diff_base); Ok(()) } }