Skip to content

Commit

Permalink
feat! inflating tail emission
Browse files Browse the repository at this point in the history
Adds a feature `tari_feature_mainnet_emission` to allow for tail emission
inflation. See #6122

This change necessitates the addition of 2 new consensus constants:
`inflation_bips` -- the annual inflation rate of the total supply in
basis points (100 bips = 1 percentage point).
and `tail_emission_epoch_length`, which controls the tail emission
inflation.

These replace `tail_emission`.

We update the Protobuf definition for `ConsensusConstants` to account for
the new fields.

Updates tests

Adds `as_u128` to `MicroMinotari` since we need to perform a large
multiplication when calculating the inflation issuance.

Note: Replaces part of #6131
  • Loading branch information
CjS77 committed Feb 21, 2024
1 parent f5860a8 commit 1e8ca53
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 234 deletions.
4 changes: 3 additions & 1 deletion applications/minotari_app_grpc/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ message ConsensusConstants {
uint64 median_timestamp_count = 9;
uint64 emission_initial = 10;
repeated uint64 emission_decay = 11;
uint64 emission_tail = 12;
uint64 emission_tail = 12 [deprecated=true];
uint64 min_sha3x_pow_difficulty = 13;
uint64 block_weight_inputs = 14;
uint64 block_weight_outputs = 15;
Expand All @@ -141,4 +141,6 @@ message ConsensusConstants {
uint64 validator_node_registration_min_lock_height = 32;
uint64 validator_node_registration_shuffle_interval_epoch = 33;
repeated PermittedRangeProofs permitted_range_proof_types = 34;
uint64 inflation_bips = 35;
uint64 tail_epoch_length = 36;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::tari_rpc as grpc;
impl From<ConsensusConstants> for grpc::ConsensusConstants {
#[allow(clippy::too_many_lines)]
fn from(cc: ConsensusConstants) -> Self {
let (emission_initial, emission_decay, emission_tail) = cc.emission_amounts();
let (emission_initial, emission_decay, inflation_bips, tail_epoch_length) = cc.emission_amounts();
let weight_params = cc.transaction_weight_params().params();
let input_version_range = cc.input_version_range().clone().into_inner();
let input_version_range = grpc::Range {
Expand Down Expand Up @@ -110,7 +110,9 @@ impl From<ConsensusConstants> for grpc::ConsensusConstants {
median_timestamp_count: u64::try_from(cc.median_timestamp_count()).unwrap_or(0),
emission_initial: emission_initial.into(),
emission_decay: emission_decay.to_vec(),
emission_tail: emission_tail.into(),
emission_tail: 0,
inflation_bips,
tail_epoch_length,
min_sha3x_pow_difficulty: cc.min_pow_difficulty(PowAlgorithm::Sha3x).into(),
block_weight_inputs: weight_params.input_weight,
block_weight_outputs: weight_params.output_weight,
Expand Down
149 changes: 88 additions & 61 deletions base_layer/core/src/consensus/consensus_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::{
consensus::network::NetworkConsensus,
proof_of_work::{Difficulty, PowAlgorithm},
transactions::{
tari_amount::{uT, MicroMinotari, T},
tari_amount::{uT, MicroMinotari},
transaction_components::{
OutputFeatures,
OutputFeaturesVersion,
Expand All @@ -50,6 +50,8 @@ use crate::{
},
};

const ANNUAL_BLOCKS: u64 = 30 /* blocks/hr */ * 24 /* hr /d */ * 366 /* days / yr */;

/// This is the inner struct used to control all consensus values.
#[derive(Debug, Clone)]
pub struct ConsensusConstants {
Expand Down Expand Up @@ -77,8 +79,10 @@ pub struct ConsensusConstants {
/// This is the emission curve decay factor as a sum of fraction powers of two. e.g. [1,2] would be 1/2 + 1/4. [2]
/// would be 1/4
pub(in crate::consensus) emission_decay: &'static [u64],
/// This is the emission curve tail amount
pub(in crate::consensus) emission_tail: MicroMinotari,
/// The tail emission inflation rate in basis points (bips). 100 bips = 1 percentage_point
pub(in crate::consensus) inflation_bips: u64,
/// The length, in blocks of each tail emission epoch (where the reward is held constant)
pub(in crate::consensus) tail_epoch_length: u64,
/// This is the maximum age a Monero merge mined seed can be reused
/// Monero forces a change every height mod 2048 blocks
max_randomx_seed_height: u64,
Expand Down Expand Up @@ -165,9 +169,14 @@ impl ConsensusConstants {
self.effective_from_height
}

/// This gets the emission curve values as (initial, decay, tail)
pub fn emission_amounts(&self) -> (MicroMinotari, &'static [u64], MicroMinotari) {
(self.emission_initial, self.emission_decay, self.emission_tail)
/// This gets the emission curve values as (initial, decay, inflation_bips, epoch_length)
pub fn emission_amounts(&self) -> (MicroMinotari, &'static [u64], u64, u64) {
(
self.emission_initial,
self.emission_decay,
self.inflation_bips,
self.tail_epoch_length,
)
}

/// The min height maturity a coinbase utxo must have.
Expand Down Expand Up @@ -380,7 +389,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: 18_462_816_327 * uT,
emission_decay: &ESMERALDA_DECAY_PARAMS,
emission_tail: 800 * T,
inflation_bips: 1000,
tail_epoch_length: 100,
max_randomx_seed_height: u64::MAX,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -443,7 +453,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: 5_538_846_115 * uT,
emission_decay: &EMISSION_DECAY,
emission_tail: 100.into(),
inflation_bips: 100,
tail_epoch_length: ANNUAL_BLOCKS,
max_randomx_seed_height: u64::MAX,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -499,7 +510,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: ESMERALDA_INITIAL_EMISSION,
emission_decay: &ESMERALDA_DECAY_PARAMS,
emission_tail: 800 * T,
inflation_bips: 100,
tail_epoch_length: ANNUAL_BLOCKS,
max_randomx_seed_height: 3000,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -554,7 +566,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: INITIAL_EMISSION,
emission_decay: &EMISSION_DECAY,
emission_tail: 800 * T,
inflation_bips: 100,
tail_epoch_length: ANNUAL_BLOCKS,
max_randomx_seed_height: 3000,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -603,7 +616,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: INITIAL_EMISSION,
emission_decay: &EMISSION_DECAY,
emission_tail: 800 * T,
inflation_bips: 100,
tail_epoch_length: ANNUAL_BLOCKS,
max_randomx_seed_height: 3000,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -654,7 +668,8 @@ impl ConsensusConstants {
median_timestamp_count: 11,
emission_initial: 10_000_000.into(),
emission_decay: &EMISSION_DECAY,
emission_tail: 100.into(),
inflation_bips: 100,
tail_epoch_length: ANNUAL_BLOCKS,
max_randomx_seed_height: u64::MAX,
max_extra_field_size: 200,
proof_of_work: algos,
Expand Down Expand Up @@ -835,11 +850,13 @@ impl ConsensusConstantsBuilder {
mut self,
intial_amount: MicroMinotari,
decay: &'static [u64],
tail_amount: MicroMinotari,
inflation_bips: u64,
epoch_length: u64,
) -> Self {
self.consensus.emission_initial = intial_amount;
self.consensus.emission_decay = decay;
self.consensus.emission_tail = tail_amount;
self.consensus.inflation_bips = inflation_bips;
self.consensus.tail_epoch_length = epoch_length;
self
}

Expand Down Expand Up @@ -868,15 +885,13 @@ impl ConsensusConstantsBuilder {

#[cfg(test)]
mod test {
use std::convert::TryFrom;

use crate::{
consensus::{
emission::{Emission, EmissionSchedule},
ConsensusConstants,
},
transactions::{
tari_amount::{uT, MicroMinotari},
tari_amount::{uT, MicroMinotari, T},
transaction_components::{OutputType, RangeProofType},
},
};
Expand Down Expand Up @@ -940,33 +955,41 @@ mod test {
let schedule = EmissionSchedule::new(
esmeralda[0].emission_initial,
esmeralda[0].emission_decay,
esmeralda[0].emission_tail,
esmeralda[0].inflation_bips,
esmeralda[0].tail_epoch_length,
esmeralda[0].faucet_value(),
);
// No genesis block coinbase
assert_eq!(schedule.block_reward(0), MicroMinotari(0));
// Coinbases starts at block 1
let coinbase_offset = 1;
let first_reward = schedule.block_reward(coinbase_offset);
assert_eq!(first_reward, esmeralda[0].emission_initial * uT);
assert_eq!(schedule.supply_at_block(coinbase_offset), first_reward);
assert_eq!(first_reward, esmeralda[0].emission_initial);
assert_eq!(
schedule.supply_at_block(coinbase_offset),
first_reward + esmeralda[0].faucet_value()
);
// 'half_life_block' at approximately '(total supply - faucet value) / 2'
#[allow(clippy::cast_possible_truncation)]
let half_life_block = (365.0 * 24.0 * 30.0 * 2.76) as u64;
let half_life_block = 365 * 24 * 30 * 3;
assert_eq!(
schedule.supply_at_block(half_life_block + coinbase_offset),
7_483_280_506_356_578 * uT
7_935_818_494_624_306 * uT + esmeralda[0].faucet_value()
);
// Tail emission starts after block 3,255,552 + coinbase_offset
let mut rewards = schedule
.iter()
.skip(3255552 + usize::try_from(coinbase_offset).unwrap());
// 21 billion
let mut rewards = schedule.iter().skip(3255552 + coinbase_offset as usize);
let (block_num, reward, supply) = rewards.next().unwrap();
assert_eq!(block_num, 3255553 + coinbase_offset);
assert_eq!(reward, 800_000_415 * uT);
let total_supply_up_to_tail_emission = supply + esmeralda[0].faucet_value;
assert_eq!(total_supply_up_to_tail_emission, 20_999_999_999_819_869 * uT);
assert_eq!(supply, 20_999_999_999_819_869 * uT);
let (_, reward, _) = rewards.next().unwrap();
assert_eq!(reward, esmeralda[0].emission_tail);
assert_eq!(reward, 799_999_715 * uT);
// Inflating tail emission
let mut rewards = schedule.iter().skip(3259845);
let (block_num, reward, supply) = rewards.next().unwrap();
assert_eq!(block_num, 3259846);
assert_eq!(reward, 797 * T);
assert_eq!(supply, 21_003_427_156_818_122 * uT);
}

#[test]
Expand All @@ -975,33 +998,33 @@ mod test {
let schedule = EmissionSchedule::new(
nextnet[0].emission_initial,
nextnet[0].emission_decay,
nextnet[0].emission_tail,
nextnet[0].inflation_bips,
nextnet[0].tail_epoch_length,
nextnet[0].faucet_value(),
);
// No genesis block coinbase
assert_eq!(schedule.block_reward(0), MicroMinotari(0));
// Coinbases starts at block 1
let coinbase_offset = 1;
let first_reward = schedule.block_reward(coinbase_offset);
assert_eq!(first_reward, nextnet[0].emission_initial * uT);
assert_eq!(schedule.supply_at_block(coinbase_offset), first_reward);
assert_eq!(
schedule.supply_at_block(coinbase_offset),
first_reward + nextnet[0].faucet_value()
);
// 'half_life_block' at approximately '(total supply - faucet value) / 2'
#[allow(clippy::cast_possible_truncation)]
let half_life_block = (365.0 * 24.0 * 30.0 * 2.76) as u64;
assert_eq!(
schedule.supply_at_block(half_life_block + coinbase_offset),
7_483_280_506_356_578 * uT
7_483_280_506_356_578 * uT + nextnet[0].faucet_value()
);
// Tail emission starts after block 3,255,552 + coinbase_offset
let mut rewards = schedule
.iter()
.skip(3255552 + usize::try_from(coinbase_offset).unwrap());
// Tail emission
let mut rewards = schedule.iter().skip(3259845);
let (block_num, reward, supply) = rewards.next().unwrap();
assert_eq!(block_num, 3255553 + coinbase_offset);
assert_eq!(reward, 800_000_415 * uT);
let total_supply_up_to_tail_emission = supply + nextnet[0].faucet_value;
assert_eq!(total_supply_up_to_tail_emission, 20_999_999_999_819_869 * uT);
let (_, reward, _) = rewards.next().unwrap();
assert_eq!(reward, nextnet[0].emission_tail);
assert_eq!(block_num, 3259846);
assert_eq!(reward, 797 * T);
assert_eq!(supply, 21_003_427_156_818_122 * uT);
}

#[test]
Expand All @@ -1010,39 +1033,45 @@ mod test {
let schedule = EmissionSchedule::new(
stagenet[0].emission_initial,
stagenet[0].emission_decay,
stagenet[0].emission_tail,
stagenet[0].inflation_bips,
stagenet[0].tail_epoch_length,
stagenet[0].faucet_value(),
);
// No genesis block coinbase
assert_eq!(schedule.block_reward(0), MicroMinotari(0));
// Coinbases starts at block 1
let coinbase_offset = 1;
let first_reward = schedule.block_reward(coinbase_offset);
assert_eq!(first_reward, stagenet[0].emission_initial * uT);
assert_eq!(schedule.supply_at_block(coinbase_offset), first_reward);
assert_eq!(
schedule.supply_at_block(coinbase_offset),
first_reward + stagenet[0].faucet_value()
);
// 'half_life_block' at approximately '(total supply - faucet value) / 2'
#[allow(clippy::cast_possible_truncation)]
let half_life_block = (365.0 * 24.0 * 30.0 * 2.76) as u64;
assert_eq!(
schedule.supply_at_block(half_life_block + coinbase_offset),
7_483_280_506_356_578 * uT
7_483_280_506_356_578 * uT + stagenet[0].faucet_value()
);
// Tail emission starts after block 3,255,552 + coinbase_offset
let mut rewards = schedule
.iter()
.skip(3255552 + usize::try_from(coinbase_offset).unwrap());
// Tail emission
let mut rewards = schedule.iter().skip(3259845);
let (block_num, reward, supply) = rewards.next().unwrap();
assert_eq!(block_num, 3255553 + coinbase_offset);
assert_eq!(reward, 800_000_415 * uT);
let total_supply_up_to_tail_emission = supply + stagenet[0].faucet_value;
assert_eq!(total_supply_up_to_tail_emission, 20_999_999_999_819_869 * uT);
let (_, reward, _) = rewards.next().unwrap();
assert_eq!(reward, stagenet[0].emission_tail);
assert_eq!(block_num, 3259846);
assert_eq!(reward, 797 * T);
assert_eq!(supply, 21_003_427_156_818_122 * uT);
}

#[test]
fn igor_schedule() {
let igor = ConsensusConstants::igor();
let schedule = EmissionSchedule::new(igor[0].emission_initial, igor[0].emission_decay, igor[0].emission_tail);
let schedule = EmissionSchedule::new(
igor[0].emission_initial,
igor[0].emission_decay,
igor[0].inflation_bips,
igor[0].tail_epoch_length,
igor[0].faucet_value(),
);
// No genesis block coinbase
assert_eq!(schedule.block_reward(0), MicroMinotari(0));
// Coinbases starts at block 1
Expand All @@ -1055,11 +1084,9 @@ mod test {
let mut previous_reward = MicroMinotari(0);
for (block_num, reward, supply) in rewards {
if reward == previous_reward {
assert_eq!(block_num, 11_084_819 + 1);
assert_eq!(supply, MicroMinotari(6_326_198_792_915_738));
// These set of constants does not result in a tail emission equal to the specified tail emission
assert_ne!(reward, igor[0].emission_tail);
assert_eq!(reward, MicroMinotari(2_097_151));
assert_eq!(block_num, 11_084_796);
assert_eq!(supply, MicroMinotari(8_010_884_615_082_026));
assert_eq!(reward, MicroMinotari(303_000_000));
break;
}
previous_reward = reward;
Expand Down
9 changes: 5 additions & 4 deletions base_layer/core/src/consensus/consensus_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,7 @@ impl ConsensusManager {
}
}

/// Get a pointer to the emission schedule
/// The height provided here, decides the emission curve to use. It swaps to the integer curve upon reaching
/// 1_000_000_000
/// Get a reference to the emission parameters
pub fn emission_schedule(&self) -> &EmissionSchedule {
&self.inner.emission
}
Expand Down Expand Up @@ -241,8 +239,11 @@ impl ConsensusManagerBuilder {
let emission = EmissionSchedule::new(
self.consensus_constants[0].emission_initial,
self.consensus_constants[0].emission_decay,
self.consensus_constants[0].emission_tail,
self.consensus_constants[0].inflation_bips,
self.consensus_constants[0].tail_epoch_length,
self.consensus_constants[0].faucet_value(),
);

let inner = ConsensusManagerInner {
consensus_constants: self.consensus_constants,
network: self.network,
Expand Down

0 comments on commit 1e8ca53

Please sign in to comment.