/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 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::{
collections::{BTreeSet, HashMap},
str::FromStr,
};
use darkfi_sdk::{crypto::MerkleTree, tx::TransactionHash};
use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
use kvdb_overlay::DatabaseOverlayStateDiff;
use num_bigint::BigUint;
use tracing::{debug, info, warn};
use crate::{
blockchain::{
block_store::{BlockDifficulty, BlockRanks},
parse_record, BlockInfo, Blockchain, BlockchainOverlay, BlockchainOverlayPtr, Header,
HeaderHash,
},
tx::Transaction,
util::time::Timestamp,
validator::{
pow::{PoWModule, RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT},
utils::{best_fork_index, block_rank, find_extended_fork_index, worst_fork_index},
verification::{verify_proposal, verify_transaction},
},
zk::VerifyingKey,
Error, Result,
};
/// Gas limit for a full block.
pub const BLOCK_GAS_LIMIT: u64 = 16_000_000_000;
/// This struct represents the information required by the consensus
/// algorithm.
pub struct Consensus {
/// Canonical (confirmed) blockchain
pub blockchain: Blockchain,
/// Fork size(length) after which it can be confirmed
pub confirmation_threshold: usize,
/// Fork chains containing block proposals
pub forks: Vec,
/// Max in-memory forks to maintain.
max_forks: usize,
/// Canonical blockchain PoW module state
pub module: PoWModule,
}
impl Consensus {
/// Generate a new Consensus state.
pub fn new(
blockchain: Blockchain,
confirmation_threshold: usize,
max_forks: usize,
pow_target: u32,
pow_fixed_difficulty: Option,
) -> Result {
let max_forks = if max_forks == 0 { 1 } else { max_forks };
let module = PoWModule::new(blockchain.clone(), pow_target, pow_fixed_difficulty, None)?;
Ok(Self { blockchain, confirmation_threshold, forks: vec![], max_forks, module })
}
/// Try to generate a new empty fork. If the forks bound has been
/// reached, try to replace the worst ranking one with the new
/// empty fork.
pub async fn generate_empty_fork(&mut self) -> Result<()> {
debug!(target: "validator::consensus::generate_empty_fork", "Generating new empty fork...");
// Check if we already have an empty fork
for fork in &self.forks {
if fork.proposals.is_empty() {
debug!(target: "validator::consensus::generate_empty_fork", "An empty fork already exists.");
return Ok(())
}
}
let fork = Fork::new(self.blockchain.clone(), self.module.clone()).await?;
self.push_fork(fork);
debug!(target: "validator::consensus::generate_empty_fork", "Fork generated!");
Ok(())
}
/// Auxiliary function to push a fork into the forks vector
/// respecting the bounding confirguration. The fork will be
/// inserted iff the bound has not be reached or it ranks higher
/// than the lowest ranking existing fork.
fn push_fork(&mut self, fork: Fork) {
// Check if we have reached the bound
if self.forks.len() < self.max_forks {
self.forks.push(fork);
return
}
// Grab worst fork. We don't care about competing forks since
// any of them can be replaced. It's safe to unwrap here since
// we already checked forks length. `best_fork_index` returns
// an error iff we pass an empty forks vector.
let index = worst_fork_index(&self.forks).unwrap();
// Check if the provided one ranks lower
if fork.targets_rank < self.forks[index].targets_rank {
return
}
// Break tie using their hash distances rank
if fork.targets_rank == self.forks[index].targets_rank &&
fork.hashes_rank <= self.forks[index].hashes_rank
{
return
}
// Replace the current worst fork with the provided one
self.forks[index] = fork;
}
/// Given a proposal, the node verifys it and finds which fork it
/// extends. If the proposal extends the canonical blockchain, a
/// new fork chain is created.
pub async fn append_proposal(
&mut self,
proposal: &Proposal,
timestamp_bound: Option,
verify_fees: bool,
) -> Result<()> {
debug!(target: "validator::consensus::append_proposal", "Appending proposal {}", proposal.hash);
// Check if proposal already exists
for fork in &self.forks {
for p in fork.proposals.iter().rev() {
if p == &proposal.hash {
debug!(target: "validator::consensus::append_proposal", "Proposal {} already exists", proposal.hash);
return Err(Error::ProposalAlreadyExists)
}
}
}
// Check if proposal is canonical
if let Ok(canonical_headers) =
self.blockchain.blocks.get_order(&[proposal.block.header.height], true)
{
if canonical_headers[0].unwrap() == proposal.hash {
debug!(target: "validator::consensus::append_proposal", "Proposal {} already exists", proposal.hash);
return Err(Error::ProposalAlreadyExists)
}
}
// Verify proposal and grab corresponding fork
let (mut fork, index) =
verify_proposal(self, proposal, timestamp_bound, verify_fees).await?;
// Append proposal to the fork
fork.append_proposal(proposal).await?;
// If a fork index was found, replace fork with the mutated
// one, otherwise try to push the new fork.
match index {
Some(i)
if i < self.forks.len() &&
self.forks[i].proposals == fork.proposals[..fork.proposals.len() - 1] =>
{
self.forks[i] = fork
}
_ => self.push_fork(fork),
}
// Remove proposal transactions from mempool
if !proposal.block.txs.is_empty() {
self.blockchain.remove_pending_txs(&proposal.block.txs)?;
}
info!(target: "validator::consensus::append_proposal", "Appended proposal {} - {}", proposal.hash, proposal.block.header.height);
Ok(())
}
/// Given a proposal, find the fork chain it extends, and return
/// its full clone. If the proposal extends the fork not on its
/// tail, a new fork is created and we re-apply the proposals up to
/// the extending one. If proposal extends canonical, a new fork is
/// created. Additionally, we return the fork index if a new fork
/// was not created, so caller can replace the fork.
pub async fn find_extended_fork(&self, proposal: &Proposal) -> Result<(Fork, Option)> {
// Check if proposal extends any fork
let found = find_extended_fork_index(&self.forks, proposal);
if found.is_err() {
if let Err(Error::ProposalAlreadyExists) = found {
return Err(Error::ProposalAlreadyExists)
}
// Check if proposal extends canonical
let (last_height, last_block) = self.blockchain.last()?;
if proposal.block.header.previous != last_block ||
proposal.block.header.height <= last_height
{
return Err(Error::ExtendedChainIndexNotFound)
}
// Check if we have an empty fork to use
for (f_index, fork) in self.forks.iter().enumerate() {
if fork.proposals.is_empty() {
return Ok((self.forks[f_index].full_clone()?, Some(f_index)))
}
}
// Generate a new fork extending canonical
let fork = Fork::new(self.blockchain.clone(), self.module.clone()).await?;
return Ok((fork, None))
}
let (f_index, p_index) = found.unwrap();
let original_fork = &self.forks[f_index];
// Check if proposal extends fork at last proposal
if p_index == (original_fork.proposals.len() - 1) {
return Ok((original_fork.full_clone()?, Some(f_index)))
}
// Rebuild fork
let mut fork = Fork::new(self.blockchain.clone(), self.module.clone()).await?;
fork.proposals = original_fork.proposals[..p_index + 1].to_vec();
fork.diffs = original_fork.diffs[..p_index + 1].to_vec();
// Retrieve proposals blocks from original fork
let blocks = &original_fork.overlay.lock().unwrap().get_blocks_by_hash(&fork.proposals)?;
for (index, block) in blocks.iter().enumerate() {
// Apply block diffs
fork.overlay.lock().unwrap().overlay.lock().unwrap().add_diff(&fork.diffs[index])?;
// Calculate block rank
let (next_difficulty, target_distance_sq, hash_distance_sq) =
block_rank(&mut fork.module, block)?;
// Update PoW module
fork.module.append(&block.header, &next_difficulty)?;
// Update fork ranks
fork.targets_rank += target_distance_sq;
fork.hashes_rank += hash_distance_sq;
}
Ok((fork, None))
}
/// Check if best fork proposals can be confirmed.
/// Consensus confirmation logic:
/// - If the current best fork has reached greater length than the
/// security threshold, and no other fork exist with same rank,
/// first proposal(s) in that fork can be appended to
/// canonical/confirmed blockchain.
///
/// When best fork can be confirmed, first block(s) should be
/// appended to canonical, and forks should be rebuilt.
pub async fn confirmation(&self) -> Result