diff --git a/doc/src/architecture/consensus.md b/doc/src/architecture/consensus.md
index 64826812c..ebceaf875 100644
--- a/doc/src/architecture/consensus.md
+++ b/doc/src/architecture/consensus.md
@@ -38,10 +38,6 @@ Pseudocode:
loop {
wait_for_next_slot_start()
- if epoch_changed() {
- create_competing_coins()
- }
-
if is_slot_leader() {
block = propose_block()
p2p.broadcast_block(block)
diff --git a/src/consensus/proto/protocol_proposal.rs b/src/consensus/proto/protocol_proposal.rs
index 181be839b..09165a127 100644
--- a/src/consensus/proto/protocol_proposal.rs
+++ b/src/consensus/proto/protocol_proposal.rs
@@ -82,7 +82,7 @@ impl ProtocolProposal {
// Verify we have the proposal already
let mut lock = self.state.write().await;
- if lock.proposal_exists(&proposal_copy.hash) {
+ if lock.consensus.proposal_exists(&proposal_copy.hash) {
debug!("ProtocolProposal::handle_receive_proposal(): Proposal already received.");
continue
}
diff --git a/src/consensus/state.rs b/src/consensus/state.rs
index 9e1dfa147..46cd54444 100644
--- a/src/consensus/state.rs
+++ b/src/consensus/state.rs
@@ -16,18 +16,29 @@
* 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 pasta_curves::pallas;
+use log::{debug, info};
+use pasta_curves::{group::ff::PrimeField, pallas};
+use rand::{thread_rng, Rng};
-use super::{constants, leadcoin::LeadCoin, Block, Float10, ProposalChain};
+use super::{
+ constants,
+ leadcoin::{LeadCoin, LeadCoinSecrets},
+ utils::fbig2base,
+ Block, BlockProposal, Float10, ProposalChain,
+};
-use crate::{net, tx::Transaction, util::time::Timestamp, Result};
+use crate::{blockchain::Blockchain, net, tx::Transaction, util::time::Timestamp, Error, Result};
/// This struct represents the information required by the consensus algorithm
-#[derive(Debug)]
pub struct ConsensusState {
+ /// Canonical (finalized) blockchain
+ pub blockchain: Blockchain,
/// Genesis block creation timestamp
pub genesis_ts: Timestamp,
/// Genesis block hash
@@ -68,9 +79,14 @@ pub struct ConsensusState {
}
impl ConsensusState {
- pub fn new(genesis_ts: Timestamp, genesis_data: blake3::Hash) -> Result {
+ 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,
@@ -91,6 +107,522 @@ impl ConsensusState {
prev_sigma2: pallas::Base::zero(),
})
}
+
+ /// 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.proposals {
+ for proposal in &chain.proposals {
+ if proposal.block.header.slot > slot {
+ slot = 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();
+ self.prev_sigma1 = sigma1;
+ self.prev_sigma2 = sigma2;
+ if epoch <= self.epoch {
+ self.generate_slot_checkpoint(sigma1.clone(), sigma2.clone());
+
+ return Ok(false)
+ }
+ let eta = self.get_eta();
+ // At start of epoch, relative slot is 0.
+ if self.coins.len() == 0 {
+ //TODO: DRK coin need to be burned, and consensus coin to be minted.
+ self.coins = self.create_epoch_coins(eta, epoch).await?;
+ }
+ self.epoch = epoch;
+ self.prev_epoch_eta = self.epoch_eta;
+ 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_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!("consensus::sigmas(): f: {}", f);
+ info!("consensus::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 epoch-competing coins
+ async fn create_epoch_coins(
+ &mut self,
+ eta: pallas::Base,
+ epoch: u64,
+ ) -> Result>> {
+ info!("Consensus: Creating coins for epoch: {}", epoch);
+ self.create_coins(eta).await
+ }
+
+ /// Generate coins for provided sigmas.
+ /// NOTE: The strategy here is having a single competing coin per slot.
+ async fn create_coins(&mut self, eta: pallas::Base) -> Result>> {
+ let slot = self.current_slot();
+ 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();
+
+ //let mut tree_cm = BridgeTree::::new(constants::EPOCH_LENGTH);
+ // LeadCoin matrix where each row represents a slot and contains its 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
+ for i in 0..constants::EPOCH_LENGTH {
+ let coin = LeadCoin::new(
+ eta,
+ constants::LOTTERY_HEAD_START, // TODO: TESTNET: Why is this constant being used?
+ slot + i as u64,
+ epoch_secrets.secret_keys[i].inner(),
+ epoch_secrets.merkle_roots[i],
+ i,
+ epoch_secrets.merkle_paths[i],
+ seeds[i],
+ epoch_secrets.secret_keys[i],
+ &mut self.coins_tree,
+ );
+
+ coins.push(vec![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.proposals {
+ // Previous slot proposals exist at end of each fork
+ if chain.proposals.last().unwrap().block.header.slot == previous_slot {
+ count += 1;
+ }
+ }
+ self.leaders_history.push(count);
+ debug!("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 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 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 += Self::pid_error(Float10::try_from(lf.clone()).unwrap());
+ }
+ sum
+ }
+
+ fn pid(p: Float10, i: Float10, d: Float10) -> Float10 {
+ constants::KP.clone() * p + constants::KI.clone() * i + constants::KD.clone() * d
+ }
+
+ /// the probability of winnig lottery having all the stake
+ /// returns f
+ fn win_prob_with_full_stake(&mut self) -> Float10 {
+ let p = self.f_dif();
+ let i = self.f_int();
+ let d = self.f_der();
+ info!("PID: P: {:?}", p);
+ info!("PID: I: {:?}", i);
+ info!("PID: D: {:?}", d);
+ let mut f = Self::pid(p, i, d);
+ info!("Consensus::win_prob_with_full_stake(): pid f: {}", f);
+ f = if f >= constants::FLOAT10_ONE.clone() {
+ constants::MAX_F.clone()
+ } else if f <= constants::FLOAT10_ZERO.clone() {
+ constants::MIN_F.clone()
+ } else {
+ f
+ };
+ info!("Consensus::win_prob_with_full_stake(): clipped f: {}", f);
+ f
+ }
+
+ /// Check that the provided 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) {
+ // Slot relative index
+ let slot = self.relative_slot(self.current_slot());
+ // Stakeholder's epoch coins
+ let coins = &self.coins;
+
+ info!("Consensus::is_leader(): slot: {}, coins len: {}", slot, coins.len());
+ assert!((slot as usize) < coins.len());
+
+ let competing_coins = &coins[slot as usize];
+
+ let mut won = false;
+ let mut highest_stake = 0;
+ let mut highest_stake_idx = 0;
+
+ for (winning_idx, coin) in competing_coins.iter().enumerate() {
+ 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.proposals.is_empty() {
+ for (i, chain) in self.proposals.iter().enumerate() {
+ if chain.proposals.len() > length {
+ longest = Some(chain.clone());
+ length = chain.proposals.len();
+ index = i as i64;
+ }
+ }
+ }
+
+ let hash = match longest {
+ Some(chain) => chain.proposals.last().unwrap().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 proposal in &self.proposals {
+ if proposal.proposals.len() > max {
+ max = proposal.proposals.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 prop_index = 0;
+ for (c_index, chain) in self.proposals.iter().enumerate() {
+ // Traverse proposals in reverse
+ for (p_index, prop) in chain.proposals.iter().enumerate().rev() {
+ if proposal.block.header.previous == prop.hash {
+ chain_index = c_index as i64;
+ prop_index = p_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
+ {
+ debug!("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.proposals[chain_index as usize];
+ // Proposal extends fork at last proposal
+ if prop_index == (chain.proposals.len() - 1) {
+ return Ok(chain_index)
+ }
+
+ debug!("find_extended_chain_index(): Proposal to fork a forkchain was received.");
+ let mut chain = self.proposals[chain_index as usize].clone();
+ // We keep all proposals until the one it extends
+ chain.proposals.drain((prop_index + 1)..);
+ self.proposals.push(chain);
+ Ok(self.proposals.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.proposals.iter() {
+ for proposal in chain.proposals.iter() {
+ if input_proposal == &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 => {
+ debug!("set_leader_history(): No fork exists.");
+ }
+ _ => {
+ debug!("set_leader_history(): Checking last proposal of fork: {}", index);
+ let last_proposal = self.proposals[index as usize].proposals.last().unwrap();
+ 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);
+ debug!("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))
+ }
}
/// Auxiliary structure used for consensus syncing.
diff --git a/src/consensus/task/proposal.rs b/src/consensus/task/proposal.rs
index cbb8dec7a..e9605431e 100644
--- a/src/consensus/task/proposal.rs
+++ b/src/consensus/task/proposal.rs
@@ -31,7 +31,7 @@ use crate::{
pub async fn proposal_task(consensus_p2p: P2pPtr, sync_p2p: P2pPtr, state: ValidatorStatePtr) {
// Node waits just before the current or next epoch last finalization syncing period, so it can
// start syncing latest state.
- let mut seconds_until_next_epoch = state.read().await.next_n_epoch_start(1);
+ let mut seconds_until_next_epoch = state.read().await.consensus.next_n_epoch_start(1);
let sync_offset = Duration::new(constants::FINAL_SYNC_DUR + 1, 0);
loop {
@@ -42,7 +42,7 @@ pub async fn proposal_task(consensus_p2p: P2pPtr, sync_p2p: P2pPtr, state: Valid
info!("consensus: Waiting for next epoch ({:?} sec)", seconds_until_next_epoch);
sleep(seconds_until_next_epoch.as_secs()).await;
- seconds_until_next_epoch = state.read().await.next_n_epoch_start(1);
+ seconds_until_next_epoch = state.read().await.consensus.next_n_epoch_start(1);
}
info!("consensus: Waiting for next epoch ({:?} sec)", seconds_until_next_epoch);
@@ -57,14 +57,14 @@ pub async fn proposal_task(consensus_p2p: P2pPtr, sync_p2p: P2pPtr, state: Valid
};
// Node modifies its participating slot to next.
- match state.write().await.set_participating() {
+ match state.write().await.consensus.set_participating() {
Ok(()) => info!("consensus: Node will start participating in the next slot"),
Err(e) => error!("consensus: Failed to set participation slot: {}", e),
}
loop {
// Node sleeps until finalization sync period start (2 seconds before next slot)
- let seconds_sync_period = (state.read().await.next_n_slot_start(1) -
+ let seconds_sync_period = (state.read().await.consensus.next_n_slot_start(1) -
Duration::new(constants::FINAL_SYNC_DUR, 0))
.as_secs();
info!("consensus: Waiting for finalization sync period ({} sec)", seconds_sync_period);
@@ -92,14 +92,14 @@ pub async fn proposal_task(consensus_p2p: P2pPtr, sync_p2p: P2pPtr, state: Valid
}
// Node sleeps until next slot
- let seconds_next_slot = state.read().await.next_n_slot_start(1).as_secs();
+ let seconds_next_slot = state.read().await.consensus.next_n_slot_start(1).as_secs();
info!("consensus: Waiting for next slot ({} sec)", seconds_next_slot);
sleep(seconds_next_slot).await;
// Retrieve slot sigmas
- let (sigma1, sigma2) = state.write().await.sigmas();
+ let (sigma1, sigma2) = state.write().await.consensus.sigmas();
// Node checks if epoch has changed, to generate new epoch coins
- let epoch_changed = state.write().await.epoch_changed(sigma1, sigma2).await;
+ let epoch_changed = state.write().await.consensus.epoch_changed(sigma1, sigma2).await;
match epoch_changed {
Ok(changed) => {
if changed {
@@ -113,7 +113,7 @@ pub async fn proposal_task(consensus_p2p: P2pPtr, sync_p2p: P2pPtr, state: Valid
};
// Node checks if it's the slot leader to generate a new proposal
// for that slot.
- let (won, idx) = state.write().await.is_slot_leader(sigma1, sigma2);
+ let (won, idx) = state.write().await.consensus.is_slot_leader(sigma1, sigma2);
let result = if won { state.write().await.propose(idx, sigma1, sigma2) } else { Ok(None) };
let proposal = match result {
Ok(prop) => {
diff --git a/src/consensus/validator.rs b/src/consensus/validator.rs
index 1de41b5d3..25c18993c 100644
--- a/src/consensus/validator.rs
+++ b/src/consensus/validator.rs
@@ -16,10 +16,9 @@
* along with this program. If not, see .
*/
-use std::{collections::HashMap, io::Cursor, time::Duration};
+use std::{collections::HashMap, io::Cursor};
use async_std::sync::{Arc, RwLock};
-use chrono::{NaiveDateTime, Utc};
use darkfi_sdk::{
crypto::{
constants::MERKLE_DEPTH,
@@ -32,15 +31,14 @@ use darkfi_serial::{deserialize, serialize, Decodable, Encodable, WriteExt};
use incrementalmerkletree::{bridgetree::BridgeTree, Tree};
use log::{debug, error, info, warn};
use pasta_curves::{group::ff::PrimeField, pallas};
-use rand::{rngs::OsRng, thread_rng, Rng};
+use rand::rngs::OsRng;
use serde_json::json;
use super::{
constants,
- leadcoin::{LeadCoin, LeadCoinSecrets},
+ leadcoin::LeadCoin,
state::{ConsensusState, SlotCheckpoint},
- utils::fbig2base,
- BlockInfo, BlockProposal, Float10, Header, LeadInfo, LeadProof, ProposalChain,
+ BlockInfo, BlockProposal, Header, LeadInfo, LeadProof, ProposalChain,
};
use crate::{
@@ -119,8 +117,8 @@ impl ValidatorState {
None
};
- let consensus = ConsensusState::new(genesis_ts, genesis_data)?;
let blockchain = Blockchain::new(db, genesis_ts, genesis_data)?;
+ let consensus = ConsensusState::new(blockchain.clone(), genesis_ts, genesis_data)?;
let unconfirmed_txs = vec![];
@@ -246,374 +244,6 @@ impl ValidatorState {
true
}
- /// 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.consensus.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.consensus.proposals {
- for proposal in &chain.proposals {
- if proposal.block.header.slot > slot {
- slot = 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.consensus.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.consensus.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.consensus.epoch_eta, sigma1, sigma2 };
- self.consensus.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();
- self.consensus.prev_sigma1 = sigma1;
- self.consensus.prev_sigma2 = sigma2;
- if epoch <= self.consensus.epoch {
- self.generate_slot_checkpoint(sigma1.clone(), sigma2.clone());
-
- return Ok(false)
- }
- let eta = self.get_eta();
- // At start of epoch, relative slot is 0.
- if self.consensus.coins.len() == 0 {
- //TODO: DRK coin need to be burned, and consensus coin to be minted.
- self.consensus.coins = self.create_epoch_coins(eta, epoch).await?;
- }
- self.consensus.epoch = epoch;
- self.consensus.prev_epoch_eta = self.consensus.epoch_eta;
- self.consensus.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_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!("consensus::sigmas(): f: {}", f);
- info!("consensus::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 epoch-competing coins
- async fn create_epoch_coins(
- &mut self,
- eta: pallas::Base,
- epoch: u64,
- ) -> Result>> {
- info!("Consensus: Creating coins for epoch: {}", epoch);
- self.create_coins(eta).await
- }
-
- /// Generate coins for provided sigmas.
- /// NOTE: The strategy here is having a single competing coin per slot.
- async fn create_coins(&mut self, eta: pallas::Base) -> Result>> {
- let slot = self.current_slot();
- 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();
-
- //let mut tree_cm = BridgeTree::::new(constants::EPOCH_LENGTH);
- // LeadCoin matrix where each row represents a slot and contains its 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
- for i in 0..constants::EPOCH_LENGTH {
- let coin = LeadCoin::new(
- eta,
- constants::LOTTERY_HEAD_START, // TODO: TESTNET: Why is this constant being used?
- slot + i as u64,
- epoch_secrets.secret_keys[i].inner(),
- epoch_secrets.merkle_roots[i],
- i,
- epoch_secrets.merkle_paths[i],
- seeds[i],
- epoch_secrets.secret_keys[i],
- &mut self.consensus.coins_tree,
- );
-
- coins.push(vec![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.
- 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.consensus.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.consensus.offset = Some(offset);
- }
-
- self.consensus.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.consensus.offset.is_none() {
- info!(
- "overall_empty_slots(): Blockchain contains only genesis, setting slot offset: {}",
- current_slot
- );
- self.consensus.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.consensus.proposals {
- // Previous slot proposals exist at end of each fork
- if chain.proposals.last().unwrap().block.header.slot == previous_slot {
- count += 1;
- }
- }
- self.consensus.leaders_history.push(count);
- debug!(
- "extend_leaders_history(): Current leaders history: {:?}",
- self.consensus.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 f_der(&self) -> Float10 {
- let len = self.consensus.leaders_history.len();
- let last = Float10::try_from(self.consensus.leaders_history[len - 1] as i64)
- .unwrap()
- .with_precision(constants::RADIX_BITS)
- .value();
- let second_to_last = Float10::try_from(self.consensus.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 f_int(&self) -> Float10 {
- let mut sum = constants::FLOAT10_ZERO.clone();
- let lead_history_len = self.consensus.leaders_history.len();
- let history_begin_index = if lead_history_len > 10 { lead_history_len - 10 } else { 0 };
-
- for lf in &self.consensus.leaders_history[history_begin_index..] {
- sum += Self::pid_error(Float10::try_from(lf.clone()).unwrap());
- }
- sum
- }
-
- fn pid(p: Float10, i: Float10, d: Float10) -> Float10 {
- constants::KP.clone() * p + constants::KI.clone() * i + constants::KD.clone() * d
- }
-
- /// the probability of winnig lottery having all the stake
- /// returns f
- fn win_prob_with_full_stake(&mut self) -> Float10 {
- let p = self.f_dif();
- let i = self.f_int();
- let d = self.f_der();
- info!("PID: P: {:?}", p);
- info!("PID: I: {:?}", i);
- info!("PID: D: {:?}", d);
- let mut f = Self::pid(p, i, d);
- info!("Consensus::win_prob_with_full_stake(): pid f: {}", f);
- f = if f >= constants::FLOAT10_ONE.clone() {
- constants::MAX_F.clone()
- } else if f <= constants::FLOAT10_ZERO.clone() {
- constants::MIN_F.clone()
- } else {
- f
- };
- info!("Consensus::win_prob_with_full_stake(): clipped f: {}", f);
- f
- }
-
- /// Check that the provided 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) {
- // Slot relative index
- let slot = self.relative_slot(self.current_slot());
- // Stakeholder's epoch coins
- let coins = &self.consensus.coins;
-
- info!("Consensus::is_leader(): slot: {}, coins len: {}", slot, coins.len());
- assert!((slot as usize) < coins.len());
-
- let competing_coins = &coins[slot as usize];
-
- let mut won = false;
- let mut highest_stake = 0;
- let mut highest_stake_idx = 0;
-
- for (winning_idx, coin) in competing_coins.iter().enumerate() {
- 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)
- }
-
/// Generate a block proposal for the current slot, containing all
/// unconfirmed transactions. Proposal extends the longest fork
/// chain the node is holding.
@@ -623,8 +253,8 @@ impl ValidatorState {
sigma1: pallas::Base,
sigma2: pallas::Base,
) -> Result