/* 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::{HashMap, HashSet};
use darkfi_sdk::{crypto::MerkleTree, tx::TransactionHash};
use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
use num_bigint::BigUint;
use sled_overlay::database::SledDbOverlayStateDiff;
use smol::lock::RwLock;
use tracing::{debug, error, info, warn};
use crate::{
blockchain::{
block_store::{BlockDifficulty, BlockRanks},
BlockInfo, Blockchain, BlockchainOverlay, BlockchainOverlayPtr, Header, HeaderHash,
},
runtime::vm_runtime::GAS_LIMIT,
tx::{Transaction, MAX_TX_CALLS},
validator::{
pow::{PoWModule, RANDOMX_KEY_CHANGE_DELAY, RANDOMX_KEY_CHANGING_HEIGHT},
utils::{best_fork_index, block_rank, find_extended_fork_index},
verification::{verify_proposal, verify_transaction},
},
zk::VerifyingKey,
Error, Result,
};
/// Gas limit for total block transactions(50 full transactions).
pub const BLOCK_GAS_LIMIT: u64 = GAS_LIMIT * MAX_TX_CALLS as u64 * 50;
/// 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: RwLock>,
/// Canonical blockchain PoW module state
pub module: RwLock,
/// Lock to restrict when proposals appends can happen
pub append_lock: RwLock<()>,
}
impl Consensus {
/// Generate a new Consensus state.
pub fn new(
blockchain: Blockchain,
confirmation_threshold: usize,
pow_target: u32,
pow_fixed_difficulty: Option,
) -> Result {
let forks = RwLock::new(vec![]);
let module = RwLock::new(PoWModule::new(
blockchain.clone(),
pow_target,
pow_fixed_difficulty,
None,
)?);
let append_lock = RwLock::new(());
Ok(Self { blockchain, confirmation_threshold, forks, module, append_lock })
}
/// Generate a new empty fork.
pub async fn generate_empty_fork(&self) -> Result<()> {
debug!(target: "validator::consensus::generate_empty_fork", "Generating new empty fork...");
let mut forks = self.forks.write().await;
// Check if we already have an empty fork
for fork in forks.iter() {
if fork.proposals.is_empty() {
debug!(target: "validator::consensus::generate_empty_fork", "An empty fork already exists.");
drop(forks);
return Ok(())
}
}
let fork = Fork::new(self.blockchain.clone(), self.module.read().await.clone()).await?;
forks.push(fork);
drop(forks);
debug!(target: "validator::consensus::generate_empty_fork", "Fork generated!");
Ok(())
}
/// 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(&self, proposal: &Proposal, verify_fees: bool) -> Result<()> {
debug!(target: "validator::consensus::append_proposal", "Appending proposal {}", proposal.hash);
// Check if proposal already exists
let lock = self.forks.read().await;
for fork in lock.iter() {
for p in fork.proposals.iter().rev() {
if p == &proposal.hash {
drop(lock);
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 {
drop(lock);
debug!(target: "validator::consensus::append_proposal", "Proposal {} already exists", proposal.hash);
return Err(Error::ProposalAlreadyExists)
}
}
drop(lock);
// Verify proposal and grab corresponding fork
let (mut fork, index) = verify_proposal(self, proposal, verify_fees).await?;
// Append proposal to the fork
fork.append_proposal(proposal).await?;
// TODO: to keep memory usage low, we should only append forks that
// are higher ranking than our current best one
// If a fork index was found, replace forks with the mutated one,
// otherwise push the new fork.
let mut lock = self.forks.write().await;
match index {
Some(i) => {
if i < lock.len() && lock[i].proposals == fork.proposals[..fork.proposals.len() - 1]
{
lock[i] = fork;
} else {
lock.push(fork);
}
}
None => {
lock.push(fork);
}
}
drop(lock);
info!(target: "validator::consensus::append_proposal", "Appended proposal {}", proposal.hash);
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)> {
// Grab a lock over current forks
let forks = self.forks.read().await;
// Check if proposal extends any fork
let found = find_extended_fork_index(&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 forks.iter().enumerate() {
if fork.proposals.is_empty() {
return Ok((forks[f_index].full_clone()?, Some(f_index)))
}
}
// Generate a new fork extending canonical
let fork = Fork::new(self.blockchain.clone(), self.module.read().await.clone()).await?;
return Ok((fork, None))
}
let (f_index, p_index) = found.unwrap();
let original_fork = &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.read().await.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])?;
// Grab next mine target and difficulty
let (next_target, next_difficulty) = fork.module.next_mine_target_and_difficulty()?;
// Calculate block rank
let (target_distance_sq, hash_distance_sq) = block_rank(block, &next_target)?;
// 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;
}
// Drop forks lock
drop(forks);
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 blockchain (confirme).
///
/// 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