/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2022 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 std::time::Duration;
use chrono::{NaiveDateTime, Utc};
use darkfi_sdk::crypto::{constants::MERKLE_DEPTH, MerkleNode};
use darkfi_serial::{SerialDecodable, SerialEncodable};
use incrementalmerkletree::bridgetree::BridgeTree;
use log::info;
use pasta_curves::{group::ff::PrimeField, pallas};
use rand::{thread_rng, Rng};
use super::{
constants,
leadcoin::{LeadCoin, LeadCoinSecrets},
utils::fbig2base,
Block, BlockProposal, Float10,
};
use crate::{blockchain::Blockchain, net, tx::Transaction, util::time::Timestamp, Error, Result};
use dashu::base::Abs;
/// This struct represents the information required by the consensus algorithm
pub struct ConsensusState {
/// Canonical (finalized) blockchain
pub blockchain: Blockchain,
/// Genesis block creation timestamp
pub genesis_ts: Timestamp,
/// Genesis block hash
pub genesis_block: blake3::Hash,
/// Participating start slot
pub participating: Option,
/// Last slot node check for finalization
pub checked_finalization: u64,
/// Slots offset since genesis,
pub offset: Option,
/// Fork chains containing block proposals
pub forks: Vec,
/// Current epoch
pub epoch: u64,
/// Current epoch eta
pub epoch_eta: pallas::Base,
/// Hot/live slot checkpoints
pub slot_checkpoints: Vec,
/// Leaders count history
pub leaders_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(
blockchain: Blockchain,
genesis_ts: Timestamp,
genesis_data: blake3::Hash,
) -> Result {
let genesis_block = Block::genesis_block(genesis_ts, genesis_data).blockhash();
Ok(Self {
blockchain,
genesis_ts,
genesis_block,
participating: None,
checked_finalization: 0,
offset: None,
forks: vec![],
epoch: 0,
epoch_eta: pallas::Base::one(),
slot_checkpoints: vec![],
leaders_history: vec![0],
coins: vec![],
coins_tree: BridgeTree::::new(constants::EPOCH_LENGTH * 100),
nullifiers: vec![],
})
}
/// Calculates current epoch.
pub fn current_epoch(&self) -> u64 {
self.slot_epoch(self.current_slot())
}
/// Calculates the epoch of the provided slot.
/// Epoch duration is configured using the `EPOCH_LENGTH` value.
pub fn slot_epoch(&self, slot: u64) -> u64 {
slot / constants::EPOCH_LENGTH as u64
}
/// Calculates current slot, based on elapsed time from the genesis block.
/// Slot duration is configured using the `SLOT_TIME` constant.
pub fn current_slot(&self) -> u64 {
self.genesis_ts.elapsed() / constants::SLOT_TIME
}
/// Calculates the relative number of the provided slot.
pub fn relative_slot(&self, slot: u64) -> u64 {
slot % constants::EPOCH_LENGTH as u64
}
/// 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)
}
/// Calculates seconds until next Nth slot starting time.
/// Slots duration is configured using the SLOT_TIME constant.
pub fn next_n_slot_start(&self, n: u64) -> Duration {
assert!(n > 0);
let start_time = NaiveDateTime::from_timestamp_opt(self.genesis_ts.0, 0).unwrap();
let current_slot = self.current_slot() + n;
let next_slot_start =
(current_slot * constants::SLOT_TIME) + (start_time.timestamp() as u64);
let next_slot_start = NaiveDateTime::from_timestamp_opt(next_slot_start as i64, 0).unwrap();
let current_time = NaiveDateTime::from_timestamp_opt(Utc::now().timestamp(), 0).unwrap();
let diff = next_slot_start - current_time;
Duration::new(diff.num_seconds().try_into().unwrap(), 0)
}
/// Calculate slots until next Nth epoch.
/// Epoch duration is configured using the EPOCH_LENGTH value.
pub fn slots_to_next_n_epoch(&self, n: u64) -> u64 {
assert!(n > 0);
let slots_till_next_epoch =
constants::EPOCH_LENGTH as u64 - self.relative_slot(self.current_slot());
((n - 1) * constants::EPOCH_LENGTH as u64) + slots_till_next_epoch
}
/// Calculates seconds until next Nth epoch starting time.
pub fn next_n_epoch_start(&self, n: u64) -> Duration {
self.next_n_slot_start(self.slots_to_next_n_epoch(n))
}
/// Set participating slot to next.
pub fn set_participating(&mut self) -> Result<()> {
self.participating = Some(self.current_slot() + 1);
Ok(())
}
/// Generate current slot checkpoint
fn generate_slot_checkpoint(&mut self, sigma1: pallas::Base, sigma2: pallas::Base) {
let slot = self.current_slot();
let checkpoint = SlotCheckpoint { slot, eta: self.epoch_eta, sigma1, sigma2 };
self.slot_checkpoints.push(checkpoint);
}
/// Check if new epoch has started, to create new epoch coins.
/// Returns flag to signify if epoch has changed and vector of
/// new epoch competing coins.
pub async fn epoch_changed(
&mut self,
sigma1: pallas::Base,
sigma2: pallas::Base,
) -> Result {
let epoch = self.current_epoch();
if epoch <= self.epoch {
self.generate_slot_checkpoint(sigma1.clone(), sigma2.clone());
return Ok(false)
}
let eta = self.get_eta();
if self.coins.len() == 0 {
self.coins = self.create_coins(eta).await?;
self.update_forks_checkpoints();
}
self.epoch = epoch;
self.epoch_eta = eta;
self.generate_slot_checkpoint(sigma1.clone(), sigma2.clone());
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();
// Generate sigmas
let mut total_stake = self.total_stake(); // Only used for fine-tuning
// at genesis epoch first slot, of absolute index 0,
// the total stake would be 0, to avoid division by zero,
// we asume total stake at first division is GENESIS_TOTAL_STAKE.
if total_stake == 0 {
total_stake = constants::GENESIS_TOTAL_STAKE;
}
info!("sigmas(): f: {}", f);
info!("sigmas(): stake: {}", total_stake);
let one = constants::FLOAT10_ONE.clone();
let two = constants::FLOAT10_TWO.clone();
let field_p = Float10::from_str_native(constants::P)
.unwrap()
.with_precision(constants::RADIX_BITS)
.value();
let total_sigma =
Float10::try_from(total_stake).unwrap().with_precision(constants::RADIX_BITS).value();
let x = one - f;
let c = x.ln();
let sigma1_fbig = c.clone() / total_sigma.clone() * field_p.clone();
let sigma1 = fbig2base(sigma1_fbig);
let sigma2_fbig = (c / total_sigma).powf(two.clone()) * (field_p / two);
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, eta: pallas::Base) -> Result> {
let slot = self.current_slot();
// TODO: cleanup LeadCoinSecrets, no need to keep a vector
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());
}
let epoch_secrets = LeadCoinSecrets::generate();
// LeadCoin matrix containing node competing coins.
let mut coins: Vec = Vec::with_capacity(constants::EPOCH_LENGTH);
// TODO: TESTNET: Here we would look into the wallet to find coins we're able to use.
// The wallet has specific tables for consensus coins.
// TODO: TESTNET: Token ID still has to be enforced properly in the consensus.
// Temporarily, we compete with zero stake
let coin = LeadCoin::new(
eta,
rand::thread_rng().gen_range(0..1000),
slot,
epoch_secrets.secret_keys[0].inner(),
epoch_secrets.merkle_roots[0],
0,
epoch_secrets.merkle_paths[0],
seeds[0],
epoch_secrets.secret_keys[0],
&mut self.coins_tree,
);
coins.push(coin);
Ok(coins)
}
/// leadership reward, assuming constant reward
/// TODO (res) implement reward mechanism with accord to DRK,DARK token-economics
fn reward() -> u64 {
constants::REWARD
}
/// Auxillary function to receive current slot offset.
/// If offset is None, its setted up as last block slot offset.
pub fn get_current_offset(&mut self, current_slot: u64) -> u64 {
// This is the case were we restarted our node, didn't receive offset from other nodes,
// so we need to find offset from last block, exluding network dead period.
if self.offset.is_none() {
let (last_slot, last_offset) = self.blockchain.get_last_offset().unwrap();
let offset = last_offset + (current_slot - last_slot);
info!("get_current_offset(): Setting slot offset: {}", offset);
self.offset = Some(offset);
}
self.offset.unwrap()
}
/// Auxillary function to calculate overall empty slots.
/// We keep an offset from genesis indicating when the first slot actually started.
/// This offset is shared between nodes.
fn overall_empty_slots(&mut self, current_slot: u64) -> u64 {
// Retrieve existing blocks excluding genesis
let blocks = (self.blockchain.len() as u64) - 1;
// Setup offset if only have genesis and havent received offset from other nodes
if blocks == 0 && self.offset.is_none() {
info!(
"overall_empty_slots(): Blockchain contains only genesis, setting slot offset: {}",
current_slot
);
self.offset = Some(current_slot);
}
// Retrieve longest fork length, to also those proposals in the calculation
let max_fork_length = self.longest_chain_length() as u64;
current_slot - blocks - self.get_current_offset(current_slot) - max_fork_length
}
/// total stake
/// assuming constant Reward.
fn total_stake(&mut self) -> i64 {
let current_slot = self.current_slot();
((current_slot - self.overall_empty_slots(current_slot)) * Self::reward()) as i64
}
/// Calculate how many leaders existed in previous slot and appends
/// it to history, to report it if win. On finalization sync period,
/// node replaces its leaders history with the sequence extracted by
/// the longest fork.
fn extend_leaders_history(&mut self) -> Float10 {
let slot = self.current_slot();
let previous_slot = slot - 1;
let mut count = 0;
for chain in &self.forks {
// Previous slot proposals exist at end of each fork
if chain.sequence.last().unwrap().proposal.block.header.slot == previous_slot {
count += 1;
}
}
self.leaders_history.push(count);
info!("extend_leaders_history(): Current leaders history: {:?}", self.leaders_history);
Float10::try_from(count as i64).unwrap().with_precision(constants::RADIX_BITS).value()
}
fn pid_error(feedback: Float10) -> Float10 {
let target = constants::FLOAT10_ONE.clone();
target - feedback
}
fn f_dif(&mut self) -> Float10 {
Self::pid_error(self.extend_leaders_history())
}
fn max_windowed_forks(&self) -> Float10 {
let mut max : u64= 5;
let window_size = 10;
let len = self.leaders_history.len();
let window_begining = if len <= (window_size+1) {
0
} else {
len - (window_size +1)
};
for item in &self.leaders_history[window_begining..] {
if *item>max {
max = *item;
}
}
Float10::try_from(max as i64).unwrap().with_precision(constants::RADIX_BITS).value()
}
fn tuned_kp(&self) -> Float10 {
(constants::KP.clone() * constants::FLOAT10_FIVE.clone())/self.max_windowed_forks()
}
fn weighted_f_dif(&mut self) -> Float10 {
self.tuned_kp() * self.f_dif()
}
fn f_der(&self) -> Float10 {
let len = self.leaders_history.len();
let last = Float10::try_from(self.leaders_history[len - 1] as i64)
.unwrap()
.with_precision(constants::RADIX_BITS)
.value();
let second_to_last = Float10::try_from(self.leaders_history[len - 2] as i64)
.unwrap()
.with_precision(constants::RADIX_BITS)
.value();
let mut der =
(Self::pid_error(second_to_last) - Self::pid_error(last)) / constants::DT.clone();
der = if der > constants::MAX_DER.clone() { constants::MAX_DER.clone() } else { der };
der = if der < constants::MIN_DER.clone() { constants::MIN_DER.clone() } else { der };
der
}
fn weighted_f_der(&self) -> Float10 {
constants::KD.clone() * self.f_der()
}
fn f_int(&self) -> Float10 {
let mut sum = constants::FLOAT10_ZERO.clone();
let lead_history_len = self.leaders_history.len();
let history_begin_index = if lead_history_len > 10 { lead_history_len - 10 } else { 0 };
for lf in &self.leaders_history[history_begin_index..] {
sum += Float10::try_from(lf.clone()).unwrap().abs();
}
sum
}
fn tuned_ki(&self) -> Float10 {
(constants::KI.clone() * constants::FLOAT10_FIVE.clone())/self.max_windowed_forks()
}
fn weighted_f_int(&self) -> Float10 {
constants::KI.clone() * self.f_int()
}
fn zero_leads_len(&self) -> Float10 {
let mut count = constants::FLOAT10_ZERO.clone();
let hist_len = self.leaders_history.len();
for i in 1..hist_len {
if self.leaders_history[hist_len - i] == 0 {
count = count + constants::FLOAT10_ONE.clone();
} else {
break
}
}
count
}
/// the probability inverse of winnig lottery having all the stake
/// returns f
fn win_inv_prob_with_full_stake(&mut self) -> Float10 {
let p = self.weighted_f_dif();
let i = self.weighted_f_int();
let d = self.weighted_f_der();
info!("win_inv_prob_with_full_stake(): PID P: {:?}", p);
info!("win_inv_prob_with_full_stake(): PID I: {:?}", i);
info!("win_inv_prob_with_full_stake(): PID D: {:?}", d);
let f = p + i.clone() + d;
info!("win_inv_prob_with_full_stake(): PID f: {}", f);
if f == constants::FLOAT10_ZERO.clone() {
return constants::MIN_F.clone()
} else if f >= constants::FLOAT10_ONE.clone() {
return constants::MAX_F.clone()
}
let hist_len = self.leaders_history.len();
if self.leaders_history[hist_len - 1] == 0 &&
self.leaders_history[hist_len - 2] == 0 &&
self.leaders_history[hist_len - 3] == 0 &&
i.clone() == constants::FLOAT10_ZERO.clone()
{
return f * constants::DEG_RATE.clone().powf(self.zero_leads_len())
}
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, usize) {
let competing_coins = &self.coins.clone();
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!("is_slot_leader: coin stake: {:?}", coin.value);
info!("is_slot_leader: total stake: {}", total_stake);
info!("is_slot_leader: relative stake: {}", (coin.value as f64) / total_stake as f64);
let first_winning = coin.is_leader(sigma1, sigma2);
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, highest_stake_idx)
}
/// Finds the longest blockchain the node holds and
/// returns the last block hash and the chain index.
pub fn longest_chain_last_hash(&self) -> Result<(blake3::Hash, i64)> {
let mut longest: Option = None;
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 {
longest = Some(chain.clone());
length = chain.sequence.len();
index = i as i64;
}
}
}
let hash = match longest {
Some(chain) => chain.sequence.last().unwrap().proposal.hash,
None => self.blockchain.last()?.1,
};
Ok((hash, 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!("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!("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
}
/// Auxillary function to set nodes leaders count history to the largest fork sequence
/// of leaders, by using provided index.
pub fn set_leader_history(&mut self, index: i64) {
// Check if we found longest fork to extract sequence from
match index {
-1 => {
info!("set_leader_history(): No fork exists.");
}
_ => {
info!("set_leader_history(): Checking last proposal of fork: {}", index);
let last_proposal = &self.forks[index as usize].sequence.last().unwrap().proposal;
if last_proposal.block.header.slot == self.current_slot() {
// Replacing our last history element with the leaders one
self.leaders_history.pop();
self.leaders_history.push(last_proposal.block.lead_info.leaders);
info!("set_leader_history(): New leaders history: {:?}", self.leaders_history);
return
}
}
}
self.leaders_history.push(0);
}
/// Utility function to extract leader selection lottery randomness(eta),
/// defined as the hash of the previous lead proof converted to pallas base.
fn get_eta(&self) -> pallas::Base {
let proof_tx_hash = self.blockchain.get_last_proof_hash().unwrap();
let mut bytes: [u8; 32] = *proof_tx_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.len() > 0 {
if let Some(slot_checkpoint) = &slot_checkpoints[0] {
return Ok(slot_checkpoint.clone())
}
}
}
Err(Error::SlotCheckpointNotFound(slot))
}
/// 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();
}
}
}
}
/// Auxiliary structure used for consensus syncing.
#[derive(Debug, 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 {
/// Slots offset since genesis,
pub offset: Option,
/// Hot/live data used by the consensus algorithm
pub forks: Vec,
/// Pending transactions
pub unconfirmed_txs: Vec,
/// Hot/live slot checkpoints
pub slot_checkpoints: Vec,
/// Leaders count history
pub leaders_history: Vec,
/// Seen nullifiers from proposals
pub nullifiers: Vec,
}
impl net::Message for ConsensusResponse {
fn name() -> &'static str {
"consensusresponse"
}
}
/// Auxiliary structure used to keep track of slot validation parameters.
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
pub struct SlotCheckpoint {
/// Slot UID
pub slot: u64,
/// Slot eta
pub eta: pallas::Base,
/// Slot sigma1
pub sigma1: pallas::Base,
/// Slot sigma2
pub sigma2: pallas::Base,
}
impl SlotCheckpoint {
pub fn new(slot: u64, eta: pallas::Base, sigma1: pallas::Base, sigma2: pallas::Base) -> Self {
Self { slot, eta, sigma1, sigma2 }
}
/// Generate the genesis slot checkpoint.
pub fn genesis_slot_checkpoint() -> Self {
let eta = pallas::Base::zero();
let sigma1 = pallas::Base::zero();
let sigma2 = pallas::Base::zero();
Self::new(0, eta, 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!("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!("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 }
}
}