tari_core 5.3.1

Core Tari protocol components
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
// Copyright 2019. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

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,
};

/// A simple struct to hold the maturity and effective height
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct MaturityTranche {
    pub maturity: u64,
    pub effective_from_height: u64,
}

/// Container struct for consensus rules. This can be cheaply cloned.
#[derive(Debug, Clone)]
pub struct BaseNodeConsensusManager {
    inner: Arc<BaseNodeConsensusManagerInner>,
}

impl BaseNodeConsensusManager {
    /// Start a builder for specified network
    pub fn builder(network: Network) -> BaseNodeConsensusManagerBuilder {
        BaseNodeConsensusManagerBuilder::new(network)
    }

    /// Returns the genesis block for the selected 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),
        }
    }

    /// Get a reference to the emission parameters
    pub fn emission_schedule(&self) -> &EmissionSchedule {
        self.inner.consensus_manager.emission()
    }

    /// Gets the block reward for the height
    pub fn get_block_reward_at(&self, height: u64) -> MicroMinotari {
        self.emission_schedule().block_reward(height)
    }

    /// Get the emission reward at height
    /// Returns None if the total supply > u64::MAX
    pub fn get_total_emission_at(&self, height: u64) -> MicroMinotari {
        self.inner.consensus_manager.emission().supply_at_block(height)
    }

    /// Get a reference to consensus constants that are effective from the given height
    pub fn consensus_constants(&self, height: u64) -> &ConsensusConstants {
        self.inner.consensus_manager.consensus_constants(height)
    }

    /// Get the vector of consensus constants applicable for all heights
    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()
    }

    /// Create a new TargetDifficulty for the given proof of work using constants that are effective from the given
    /// height
    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))
    }

    /// Creates a total_coinbase offset containing all fees for the validation from the height and kernel set
    pub fn calculate_coinbase_and_fees(
        &self,
        height: u64,
        kernels: &[TransactionKernel],
    ) -> Result<MicroMinotari, String> {
        self.inner
            .consensus_manager
            .calculate_coinbase_and_fees(height, kernels)
    }

    /// Returns a ref to the chain strength comparer
    pub fn chain_strength_comparer(&self) -> &dyn ChainStrengthComparer {
        self.inner.chain_strength_comparer.as_ref()
    }

    /// This is the currently configured chain network.
    pub fn network(&self) -> NetworkConsensus {
        self.inner.consensus_manager.network()
    }

    /// Get the maturity tranches from the consensus manager
    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<_>>()
    }

    /// Get the total spendable block rewards and pre-mine at the specified height
    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())
    }

    /// Get the total circulating block rewards and spendable pre-mine at the specified height
    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())
    }

    /// Get the total spendable pre-mine at the specified height
    pub fn pre_mine_spendable_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
        pre_mine_spendable_at_height(height, self.network().as_network())
    }

    /// Get the total spendable pre-mine at the specified height
    pub fn total_pre_mine_in_genesis_block(&self) -> MicroMinotari {
        self.consensus_constants(0).pre_mine_value()
    }

    /// Get the total pre-mine that is still time-locked at the specified height
    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)?)
    }

    /// Get the total mined block rewards at the specified height (excluding pre-mine)
    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())
    }

    /// Get the total spendable block rewards circulation at the specified height (excluding pre-mine)
    pub fn block_rewards_spendable_at_height(&self, height: u64) -> Result<MicroMinotari, String> {
        // Example initial maturity schedule up to 3 weeks ( | height | (maturity) |):
        // | 0 -> 5040 - 1 | (720) |
        //                 | 5040 -> 10080 - 1 | (540) |
        //                                     | 10080 -> 15120 - 1 | (360) |
        //                                                          | 15120 -> | (180) |

        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();

        // We have to adjust the matured rewards at height to account for the effective from height of the last
        // effective tranche
        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)
    }
}

/// This is the used to control all consensus values.
#[derive(Debug)]
struct BaseNodeConsensusManagerInner {
    pub consensus_manager: ConsensusManager,

    pub gen_block: Option<ChainBlock>,
    /// The comparer used to determine which chain is stronger for reorgs.
    pub chain_strength_comparer: Box<dyn ChainStrengthComparer + Send + Sync>,
}

/// Constructor for the consensus manager struct
pub struct BaseNodeConsensusManagerBuilder {
    consensus_manager_builder: ConsensusManagerBuilder,
    /// This is can only used be used if the network is localnet
    gen_block: Option<ChainBlock>,
    chain_strength_comparer: Option<Box<dyn ChainStrengthComparer + Send + Sync>>,
}

impl BaseNodeConsensusManagerBuilder {
    /// Creates a new ConsensusManagerBuilder with the specified network
    pub fn new(network: Network) -> Self {
        BaseNodeConsensusManagerBuilder {
            consensus_manager_builder: ConsensusManagerBuilder::new(network),
            gen_block: None,
            chain_strength_comparer: None,
        }
    }

    /// Adds in a custom consensus constants to be used
    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
    }

    /// Adds in a custom block to be used. This will be overwritten if the network is anything else than localnet
    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
    }

    /// Builds a consensus manager
    pub fn build(self) -> Result<BaseNodeConsensusManager, BaseConsensusBuilderError> {
        // should not be allowed to set the gen block and have the network type anything else than LocalNet
        // If feature != base_node, gen_block is not available
        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 {
    /// The average amount of blocks per day based on the target block time
    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"), // mined
                MicroMinotari::from_str("        0.000000 T"), // spendable
                MicroMinotari::from_str("756000002.000000 T"), // pre_mine
                MicroMinotari::from_str("756000002.000000 T"), // total
            ),
            (
                1000,
                MicroMinotari::from_str(" 13946753.809464 T"), // mined
                MicroMinotari::from_str("  3906326.802521 T"), // spendable
                MicroMinotari::from_str("756000002.000000 T"), // pre_mine
                MicroMinotari::from_str("759906328.802521 T"), // total
            ),
            (
                10000,
                MicroMinotari::from_str("138917413.875832 T"), // mined
                MicroMinotari::from_str("131447021.355866 T"), // spendable
                MicroMinotari::from_str("756000002.000000 T"), // pre_mine
                MicroMinotari::from_str("887447023.355866 T"), // total
            ),
            (
                180 * BLOCKS_PER_DAY,
                MicroMinotari::from_str("1709098961.342784 T"), // mined
                MicroMinotari::from_str("1706857672.130454 T"), // spendable
                MicroMinotari::from_str(" 867125003.916666 T"), // pre_mine
                MicroMinotari::from_str("2573982676.047120 T"), // total
            ),
            (
                (180 + 20) * BLOCKS_PER_DAY,
                MicroMinotari::from_str("1887258043.208972 T"), // mined
                MicroMinotari::from_str("1885044943.492867 T"), // spendable
                MicroMinotari::from_str(" 867125003.916666 T"), // pre_mine
                MicroMinotari::from_str("2752169947.409533 T"), // total
            ),
            (
                365 * BLOCKS_PER_DAY,
                MicroMinotari::from_str("3274120131.965798 T"), // mined
                MicroMinotari::from_str("3272126467.754857 T"), // spendable
                MicroMinotari::from_str("1652875003.416662 T"), // pre_mine
                MicroMinotari::from_str("4925001471.171519 T"), // total
            ),
            (
                (365 + 20) * BLOCKS_PER_DAY,
                MicroMinotari::from_str("3432595650.489607 T"), // mined
                MicroMinotari::from_str("3430627060.613596 T"), // spendable
                MicroMinotari::from_str("1652875003.416662 T"), // pre_mine
                MicroMinotari::from_str("5083502064.030258 T"), // total
            ),
            (
                (365 + 200) * BLOCKS_PER_DAY,
                MicroMinotari::from_str("4772127517.495734 T"), // mined
                MicroMinotari::from_str("4770370867.355004 T"), // spendable
                MicroMinotari::from_str("2946125002.916658 T"), // pre_mine
                MicroMinotari::from_str("7716495870.271662 T"), // total
            ),
        ] {
            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);
        }
    }
}