use std::sync::Arc;
use tari_common::configuration::Network;
use tari_node_components::blocks::ChainBlock;
use tari_transaction_components::{
MicroMinotari,
consensus::{
ConsensusConstants,
ConsensusManager,
ConsensusManagerBuilder,
NetworkConsensus,
emission::{Emission, EmissionSchedule},
},
tari_proof_of_work::PowAlgorithm,
transaction_components::TransactionKernel,
};
use crate::{
blocks::pre_mine::pre_mine_spendable_at_height,
consensus::chain_strength_comparer::{ChainStrengthComparer, strongest_chain},
proof_of_work::TargetDifficultyWindow,
};
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct MaturityTranche {
pub maturity: u64,
pub effective_from_height: u64,
}
#[derive(Debug, Clone)]
pub struct BaseNodeConsensusManager {
inner: Arc<BaseNodeConsensusManagerInner>,
}
impl BaseNodeConsensusManager {
pub fn builder(network: Network) -> BaseNodeConsensusManagerBuilder {
BaseNodeConsensusManagerBuilder::new(network)
}
pub fn get_genesis_block(&self) -> ChainBlock {
use crate::blocks::genesis_block::get_genesis_block;
let network = self.inner.consensus_manager.network().as_network();
match network {
Network::LocalNet => self
.inner
.gen_block
.clone()
.unwrap_or_else(|| get_genesis_block(network)),
_ => get_genesis_block(network),
}
}
pub fn emission_schedule(&self) -> &EmissionSchedule {
self.inner.consensus_manager.emission()
}
pub fn get_block_reward_at(&self, height: u64) -> MicroMinotari {
self.emission_schedule().block_reward(height)
}
pub fn get_total_emission_at(&self, height: u64) -> MicroMinotari {
self.inner.consensus_manager.emission().supply_at_block(height)
}
pub fn consensus_constants(&self, height: u64) -> &ConsensusConstants {
self.inner.consensus_manager.consensus_constants(height)
}
pub fn consensus_constants_vec(&self) -> &[ConsensusConstants] {
self.inner.consensus_manager.consensus_constants_vec()
}
pub fn consensus_manager(&self) -> ConsensusManager {
self.inner.consensus_manager.clone()
}
pub(crate) fn new_target_difficulty(
&self,
pow_algo: PowAlgorithm,
height: u64,
) -> Result<TargetDifficultyWindow, String> {
let constants = self.consensus_constants(height);
let block_window = constants.difficulty_block_window();
let block_window_u =
usize::try_from(block_window).map_err(|e| format!("difficulty block window exceeds usize::MAX: {e}"))?;
TargetDifficultyWindow::new(block_window_u, constants.pow_target_block_interval(pow_algo))
}
pub fn calculate_coinbase_and_fees(
&self,
height: u64,
kernels: &[TransactionKernel],
) -> Result<MicroMinotari, String> {
self.inner
.consensus_manager
.calculate_coinbase_and_fees(height, kernels)
}
pub fn chain_strength_comparer(&self) -> &dyn ChainStrengthComparer {
self.inner.chain_strength_comparer.as_ref()
}
pub fn network(&self) -> NetworkConsensus {
self.inner.consensus_manager.network()
}
pub fn get_maturity_tranches(&self) -> Vec<MaturityTranche> {
self.consensus_constants_vec()
.iter()
.map(|c| MaturityTranche {
maturity: c.coinbase_min_maturity(),
effective_from_height: c.effective_from_height(),
})
.collect::<Vec<_>>()
}
pub fn total_tokens_spendable_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
let spendable_rewards = self.block_rewards_spendable_at_height(height)?;
let spendable_pre_mine = self.pre_mine_spendable_at_height(height)?;
spendable_rewards
.checked_add(spendable_pre_mine)
.ok_or_else(|| "total_tokens_spendable_at_height overflowed u128".to_string())
}
pub fn total_tokens_circulating_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
let mined_rewards = self.block_rewards_mined_at_height(height)?;
let spendable_pre_mine = self.pre_mine_spendable_at_height(height)?;
mined_rewards
.checked_add(spendable_pre_mine)
.ok_or_else(|| "total_circulating_tokens_at_height overflowed u128".to_string())
}
pub fn pre_mine_spendable_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
pre_mine_spendable_at_height(height, self.network().as_network())
}
pub fn total_pre_mine_in_genesis_block(&self) -> MicroMinotari {
self.consensus_constants(0).pre_mine_value()
}
pub fn time_locked_pre_mine(&self, height: u64) -> Result<MicroMinotari, String> {
Ok(self.total_pre_mine_in_genesis_block() - self.pre_mine_spendable_at_height(height)?)
}
pub fn block_rewards_mined_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
Ok(self.get_total_emission_at(height) - self.consensus_constants(height).pre_mine_value())
}
pub fn block_rewards_spendable_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
let maturity_tranches = self.get_maturity_tranches();
let last_effective_tranche = maturity_tranches
.iter()
.filter(|v| v.effective_from_height <= height)
.max_by_key(|v| v.effective_from_height)
.ok_or_else(|| format!("Last effective maturity tranche for height {height} not found"))?;
let last_effective_index = maturity_tranches
.iter()
.position(|v| v == last_effective_tranche)
.ok_or_else(|| format!("Last effective maturity tranche index for height {height} not found"))?;
let previous_effective_tranch = maturity_tranches
.get(last_effective_index.saturating_sub(1))
.expect("Index should exist")
.clone();
let emission_schedule = self.emission_schedule();
let matured_rewards_at_height = if last_effective_tranche.maturity < previous_effective_tranch.maturity &&
height < last_effective_tranche.effective_from_height + previous_effective_tranch.maturity
{
emission_schedule
.supply_at_block(height.saturating_sub(previous_effective_tranch.maturity))
.saturating_sub(self.consensus_constants(height).pre_mine_value())
} else {
emission_schedule
.supply_at_block(height.saturating_sub(last_effective_tranche.maturity))
.saturating_sub(self.consensus_constants(height).pre_mine_value())
};
Ok(matured_rewards_at_height)
}
}
#[derive(Debug)]
struct BaseNodeConsensusManagerInner {
pub consensus_manager: ConsensusManager,
pub gen_block: Option<ChainBlock>,
pub chain_strength_comparer: Box<dyn ChainStrengthComparer + Send + Sync>,
}
pub struct BaseNodeConsensusManagerBuilder {
consensus_manager_builder: ConsensusManagerBuilder,
gen_block: Option<ChainBlock>,
chain_strength_comparer: Option<Box<dyn ChainStrengthComparer + Send + Sync>>,
}
impl BaseNodeConsensusManagerBuilder {
pub fn new(network: Network) -> Self {
BaseNodeConsensusManagerBuilder {
consensus_manager_builder: ConsensusManagerBuilder::new(network),
gen_block: None,
chain_strength_comparer: None,
}
}
pub fn add_consensus_constants(mut self, consensus_constants: ConsensusConstants) -> Self {
self.consensus_manager_builder = self
.consensus_manager_builder
.add_consensus_constants(consensus_constants);
self
}
pub fn with_block(mut self, block: ChainBlock) -> Self {
self.gen_block = Some(block);
self
}
pub fn on_ties(mut self, chain_strength_comparer: Box<dyn ChainStrengthComparer + Send + Sync>) -> Self {
self.chain_strength_comparer = Some(chain_strength_comparer);
self
}
pub fn build(self) -> Result<BaseNodeConsensusManager, BaseConsensusBuilderError> {
if self.consensus_manager_builder.network.as_network() != Network::LocalNet && self.gen_block.is_some() {
return Err(BaseConsensusBuilderError::CannotSetGenesisBlock);
}
let consensus_manager = self.consensus_manager_builder.build();
let inner = BaseNodeConsensusManagerInner {
consensus_manager,
gen_block: self.gen_block,
chain_strength_comparer: self.chain_strength_comparer.unwrap_or_else(|| {
strongest_chain()
.by_accumulated_difficulty()
.then()
.by_height()
.then()
.by_tari_randomx_difficulty()
.then()
.by_monero_randomx_difficulty()
.then()
.by_sha3x_difficulty()
.then()
.by_cuckaroo_cycle_difficulty()
.build()
}),
};
Ok(BaseNodeConsensusManager { inner: Arc::new(inner) })
}
}
#[derive(Debug, thiserror::Error)]
pub enum BaseConsensusBuilderError {
#[error("Cannot set a genesis block with a network other than LocalNet")]
CannotSetGenesisBlock,
}
#[cfg(test)]
mod test {
pub const BLOCKS_PER_DAY: u64 = 24 * 60 / 2;
use std::str::FromStr;
use tari_transaction_components::consensus::consensus_constants::MAINNET_PRE_MINE_VALUE;
use super::*;
#[test]
fn test_supply_at_block() {
let network = Network::MainNet;
let consensus_manager = BaseNodeConsensusManager::builder(network).build().unwrap();
for (height, mined, spendable, pre_mine, total) in [
(
0,
MicroMinotari::from_str(" 0.000000 T"), MicroMinotari::from_str(" 0.000000 T"), MicroMinotari::from_str("756000002.000000 T"), MicroMinotari::from_str("756000002.000000 T"), ),
(
1000,
MicroMinotari::from_str(" 13946753.809464 T"), MicroMinotari::from_str(" 3906326.802521 T"), MicroMinotari::from_str("756000002.000000 T"), MicroMinotari::from_str("759906328.802521 T"), ),
(
10000,
MicroMinotari::from_str("138917413.875832 T"), MicroMinotari::from_str("131447021.355866 T"), MicroMinotari::from_str("756000002.000000 T"), MicroMinotari::from_str("887447023.355866 T"), ),
(
180 * BLOCKS_PER_DAY,
MicroMinotari::from_str("1709098961.342784 T"), MicroMinotari::from_str("1706857672.130454 T"), MicroMinotari::from_str(" 867125003.916666 T"), MicroMinotari::from_str("2573982676.047120 T"), ),
(
(180 + 20) * BLOCKS_PER_DAY,
MicroMinotari::from_str("1887258043.208972 T"), MicroMinotari::from_str("1885044943.492867 T"), MicroMinotari::from_str(" 867125003.916666 T"), MicroMinotari::from_str("2752169947.409533 T"), ),
(
365 * BLOCKS_PER_DAY,
MicroMinotari::from_str("3274120131.965798 T"), MicroMinotari::from_str("3272126467.754857 T"), MicroMinotari::from_str("1652875003.416662 T"), MicroMinotari::from_str("4925001471.171519 T"), ),
(
(365 + 20) * BLOCKS_PER_DAY,
MicroMinotari::from_str("3432595650.489607 T"), MicroMinotari::from_str("3430627060.613596 T"), MicroMinotari::from_str("1652875003.416662 T"), MicroMinotari::from_str("5083502064.030258 T"), ),
(
(365 + 200) * BLOCKS_PER_DAY,
MicroMinotari::from_str("4772127517.495734 T"), MicroMinotari::from_str("4770370867.355004 T"), MicroMinotari::from_str("2946125002.916658 T"), MicroMinotari::from_str("7716495870.271662 T"), ),
] {
let mined = mined.unwrap();
let spendable = spendable.unwrap();
let pre_mine = pre_mine.unwrap();
let total = total.unwrap();
let mined_rewards = consensus_manager.block_rewards_mined_at_height(height).unwrap();
let spendable_rewards = consensus_manager.block_rewards_spendable_at_height(height).unwrap();
let total_spendable = consensus_manager.total_tokens_spendable_at_height(height).unwrap();
let pre_mine_spendable = consensus_manager.pre_mine_spendable_at_height(height).unwrap();
let circulating_supply = consensus_manager.total_tokens_circulating_at_height(height).unwrap();
let total_pre_mine = consensus_manager.total_pre_mine_in_genesis_block();
let time_locked_pre_mine = consensus_manager.time_locked_pre_mine(height).unwrap();
assert_eq!(mined_rewards, mined);
assert_eq!(spendable_rewards, spendable);
assert_eq!(pre_mine_spendable, pre_mine);
assert_eq!(total_spendable, total);
assert_eq!(circulating_supply, mined + pre_mine);
assert_eq!(total_pre_mine, MAINNET_PRE_MINE_VALUE);
assert_eq!(time_locked_pre_mine, MAINNET_PRE_MINE_VALUE - pre_mine);
}
}
}