use console::prelude::{ensure, Result};
pub const MAX_COINBASE_REWARD: u64 = 190_258_739; pub const fn block_reward(total_supply: u64, block_time: u16, coinbase_reward: u64, transaction_fees: u64) -> u64 {
let block_height_at_year_1 = block_height_at_year(block_time, 1);
let annual_reward = total_supply / 20;
let block_reward = annual_reward / block_height_at_year_1 as u64;
block_reward + (coinbase_reward / 2) + transaction_fees
}
pub const fn puzzle_reward(coinbase_reward: u64) -> u64 {
coinbase_reward / 2
}
pub fn coinbase_reward(
block_height: u32,
starting_supply: u64,
anchor_height: u32,
block_time: u16,
combined_proof_target: u128,
cumulative_proof_target: u64,
coinbase_target: u64,
) -> Result<u64> {
let remaining_coinbase_target = coinbase_target.saturating_sub(cumulative_proof_target);
let remaining_proof_target = combined_proof_target.min(remaining_coinbase_target as u128);
let anchor_block_reward = anchor_block_reward_at_height(block_height, starting_supply, anchor_height, block_time);
let reward = anchor_block_reward.saturating_mul(remaining_proof_target).saturating_div(coinbase_target as u128);
ensure!(reward <= MAX_COINBASE_REWARD as u128, "Coinbase reward ({reward}) exceeds maximum {MAX_COINBASE_REWARD}");
Ok(u64::try_from(reward).expect("Coinbase reward exceeds u64::MAX"))
}
const fn anchor_block_reward_at_height(
block_height: u32,
starting_supply: u64,
anchor_height: u32,
block_time: u16,
) -> u128 {
let block_height_at_year_10 = block_height_at_year(block_time, 10) as u128;
let num_remaining_blocks_to_year_10 = block_height_at_year_10.saturating_sub(block_height as u128);
let numerator = 2 * starting_supply as u128 * anchor_height as u128 * num_remaining_blocks_to_year_10;
let denominator = block_height_at_year_10 * (block_height_at_year_10 + 1);
numerator / denominator
}
pub const fn block_height_at_year(block_time: u16, num_years: u32) -> u32 {
const SECONDS_IN_A_YEAR: u32 = 60 * 60 * 24 * 365;
let block_height_at_year_1 = SECONDS_IN_A_YEAR / block_time as u32;
block_height_at_year_1 * num_years
}
pub fn coinbase_target(
previous_target: u64,
previous_block_timestamp: i64,
block_timestamp: i64,
anchor_time: u16,
num_blocks_per_epoch: u32,
genesis_target: u64,
) -> Result<u64> {
let half_life = num_blocks_per_epoch.saturating_div(2).saturating_mul(anchor_time as u32);
let candidate_target =
retarget(previous_target, previous_block_timestamp, block_timestamp, anchor_time, half_life, true)?;
Ok(candidate_target.max(genesis_target))
}
pub fn proof_target(coinbase_target: u64, genesis_proof_target: u64) -> u64 {
coinbase_target.checked_shr(7).map(|target| target.saturating_add(1)).unwrap_or(genesis_proof_target)
}
fn retarget(
previous_target: u64,
previous_block_timestamp: i64,
block_timestamp: i64,
anchor_time: u16,
half_life: u32,
is_inverse: bool,
) -> Result<u64> {
let block_time_elapsed = block_timestamp.saturating_sub(previous_block_timestamp).max(1);
let mut drift = block_time_elapsed.saturating_sub(anchor_time as i64);
if drift == 0 {
return Ok(previous_target);
}
if is_inverse {
drift *= -1;
}
const RBITS: u32 = 16;
const RADIX: u128 = 1 << RBITS;
let (integral, fractional) = {
let exponent = (RADIX as i128).saturating_mul(drift as i128) / half_life as i128;
let integral = exponent >> RBITS;
let fractional = (exponent - (integral << RBITS)) as u128;
ensure!(fractional < RADIX, "Fractional part is not within the fixed point size");
ensure!(exponent == (integral * (RADIX as i128) + fractional as i128), "Exponent is decomposed incorrectly");
(integral, fractional)
};
let fractional_multiplier = RADIX
+ ((195_766_423_245_049_u128 * fractional
+ 971_821_376_u128 * fractional.pow(2)
+ 5_127_u128 * fractional.pow(3)
+ 2_u128.pow(RBITS * 3 - 1))
>> (RBITS * 3));
let candidate_target = (previous_target as u128).saturating_mul(fractional_multiplier);
let shifts = integral - RBITS as i128;
let mut candidate_target = if shifts < 0 {
match candidate_target.checked_shr(u32::try_from(-shifts)?) {
Some(target) => core::cmp::max(target, 1),
None => 1,
}
} else {
match candidate_target.checked_shl(u32::try_from(shifts)?) {
Some(target) => core::cmp::max(target, 1),
None => u64::MAX as u128,
}
};
candidate_target = core::cmp::min(candidate_target, u64::MAX as u128);
ensure!(candidate_target.checked_shr(64) == Some(0), "The target has overflowed");
Ok(u64::try_from(candidate_target)?)
}
#[cfg(test)]
mod tests {
use super::*;
use console::network::{prelude::*, Testnet3};
type CurrentNetwork = Testnet3;
const ITERATIONS: u32 = 1000;
const EXPECTED_ANCHOR_BLOCK_REWARD_AT_BLOCK_1: u128 = MAX_COINBASE_REWARD as u128;
const EXPECTED_STAKING_REWARD: u64 = 23_782_343;
const EXPECTED_COINBASE_REWARD_AT_BLOCK_1: u64 = MAX_COINBASE_REWARD;
#[test]
fn test_anchor_block_reward() {
let reward = anchor_block_reward_at_height(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
);
assert_eq!(reward, EXPECTED_ANCHOR_BLOCK_REWARD_AT_BLOCK_1);
let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10);
let mut previous_reward = reward;
let anchor_height = CurrentNetwork::ANCHOR_HEIGHT as usize;
for height in (2..block_height_at_year_10).step_by(anchor_height).skip(1) {
let reward = anchor_block_reward_at_height(
height,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
);
assert!(reward < previous_reward, "Failed on block height {height}");
previous_reward = reward;
}
for height in block_height_at_year_10..(block_height_at_year_10 + ITERATIONS) {
let reward = anchor_block_reward_at_height(
height,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
);
assert_eq!(reward, 0);
}
}
#[test]
fn test_block_reward() {
let reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME, 0, 0);
assert_eq!(reward, EXPECTED_STAKING_REWARD);
let larger_reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME + 1, 0, 0);
assert!(reward < larger_reward);
let smaller_reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME - 1, 0, 0);
assert!(reward > smaller_reward);
}
#[test]
fn test_coinbase_reward() {
let coinbase_target: u64 = 10000;
let combined_proof_target: u128 = coinbase_target as u128;
let reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
combined_proof_target,
0,
coinbase_target,
)
.unwrap();
assert_eq!(reward, EXPECTED_COINBASE_REWARD_AT_BLOCK_1);
let smaller_reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
combined_proof_target / 2,
0,
coinbase_target,
)
.unwrap();
assert_eq!(smaller_reward, reward / 2);
let smaller_reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
combined_proof_target,
coinbase_target / 2,
coinbase_target,
)
.unwrap();
assert_eq!(smaller_reward, reward / 2);
let equivalent_reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
u128::MAX,
0,
coinbase_target,
)
.unwrap();
assert_eq!(reward, equivalent_reward);
let zero_reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
0,
0,
coinbase_target,
)
.unwrap();
assert_eq!(zero_reward, 0);
let zero_reward = coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
1,
coinbase_target + 1,
coinbase_target,
)
.unwrap();
assert_eq!(zero_reward, 0);
}
#[test]
fn test_coinbase_reward_remaining_target() {
let mut rng = TestRng::default();
fn compute_coinbase_reward(
combined_proof_target: u64,
cumulative_proof_target: u64,
coinbase_target: u64,
) -> u64 {
coinbase_reward(
1,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
combined_proof_target as u128,
cumulative_proof_target,
coinbase_target,
)
.unwrap()
}
let coinbase_target: u64 = rng.gen_range(1_000_000..1_000_000_000_000_000);
let cumulative_proof_target = coinbase_target / 2;
let combined_proof_target = coinbase_target / 4;
let reward = compute_coinbase_reward(combined_proof_target, cumulative_proof_target, coinbase_target);
for _ in 0..ITERATIONS {
let equivalent_reward = compute_coinbase_reward(
combined_proof_target,
rng.gen_range(0..(coinbase_target - combined_proof_target)),
coinbase_target,
);
assert_eq!(reward, equivalent_reward);
let lower_reward = compute_coinbase_reward(
combined_proof_target,
rng.gen_range((coinbase_target - combined_proof_target + 1)..coinbase_target),
coinbase_target,
);
assert!(lower_reward < reward);
let larger_reward = compute_coinbase_reward(
rng.gen_range(combined_proof_target + 1..u64::MAX),
cumulative_proof_target,
coinbase_target,
);
assert!(reward < larger_reward);
}
}
#[test]
fn test_coinbase_reward_up_to_year_10() {
let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10);
let mut block_height = 1;
let mut previous_reward = coinbase_reward(
block_height,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
1,
0,
1,
)
.unwrap();
block_height += 1;
let mut total_reward = previous_reward;
let coinbase_target = CurrentNetwork::ANCHOR_HEIGHT as u64;
let mut cumulative_proof_target = 0;
let mut hit_500m = false;
let mut hit_1b = false;
while block_height < block_height_at_year_10 {
let reward = coinbase_reward(
block_height,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
1,
cumulative_proof_target,
coinbase_target,
)
.unwrap();
assert!(reward <= previous_reward);
total_reward += reward;
previous_reward = reward;
block_height += 1;
cumulative_proof_target = match cumulative_proof_target + 1 {
cumulative_proof_target if cumulative_proof_target == coinbase_target => 0,
cumulative_proof_target => cumulative_proof_target,
};
if !hit_500m && total_reward > 500_000_000_000_000 {
println!("500M credits block height is {block_height}");
assert_eq!(block_height, 5_786_964, "Update me if my parameters have changed");
hit_500m = true;
} else if !hit_1b && total_reward > 1_000_000_000_000_000 {
println!("1B credits block height is {block_height}");
assert_eq!(block_height, 13_328_683, "Update me if my parameters have changed");
hit_1b = true;
}
}
assert_eq!(total_reward, 1_499_999_984_232_003, "Update me if my parameters have changed");
}
#[test]
fn test_coinbase_reward_after_year_10() {
let mut rng = TestRng::default();
let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10);
let reward = coinbase_reward(
block_height_at_year_10,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
1,
0,
1,
)
.unwrap();
assert_eq!(reward, 0);
for _ in 0..ITERATIONS {
let block_height: u32 = rng.gen_range(block_height_at_year_10..block_height_at_year_10 * 10);
let coinbase_target = rng.gen_range(1_000_000..1_000_000_000_000_000);
let cumulative_proof_target = rng.gen_range(0..coinbase_target);
let combined_proof_target = rng.gen_range(0..coinbase_target as u128);
let reward = coinbase_reward(
block_height,
CurrentNetwork::STARTING_SUPPLY,
CurrentNetwork::ANCHOR_HEIGHT,
CurrentNetwork::BLOCK_TIME,
combined_proof_target,
cumulative_proof_target,
coinbase_target,
)
.unwrap();
assert_eq!(reward, 0);
}
}
#[test]
fn test_targets() {
let mut rng = TestRng::default();
let minimum_coinbase_target: u64 = 2u64.pow(10) - 1;
fn test_new_targets(rng: &mut TestRng, minimum_coinbase_target: u64) {
let previous_coinbase_target: u64 = rng.gen_range(minimum_coinbase_target..u64::MAX);
let previous_prover_target = proof_target(previous_coinbase_target, CurrentNetwork::GENESIS_PROOF_TARGET);
let previous_timestamp = rng.gen();
let next_timestamp = previous_timestamp + CurrentNetwork::ANCHOR_TIME as i64;
let new_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
let new_prover_target = proof_target(new_coinbase_target, CurrentNetwork::GENESIS_PROOF_TARGET);
assert_eq!(new_coinbase_target, previous_coinbase_target);
assert_eq!(new_prover_target, previous_prover_target);
let new_timestamp = previous_timestamp + 2 * CurrentNetwork::ANCHOR_TIME as i64;
let new_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
new_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
let new_prover_target = proof_target(new_coinbase_target, CurrentNetwork::GENESIS_PROOF_TARGET);
assert!(new_coinbase_target < previous_coinbase_target);
assert!(new_prover_target < previous_prover_target);
let next_timestamp = previous_timestamp + (CurrentNetwork::ANCHOR_TIME / 2) as i64;
let new_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
let new_prover_target = proof_target(new_coinbase_target, CurrentNetwork::GENESIS_PROOF_TARGET);
assert!(new_coinbase_target > previous_coinbase_target);
assert!(new_prover_target > previous_prover_target);
}
for _ in 0..ITERATIONS {
test_new_targets(&mut rng, minimum_coinbase_target);
}
}
#[test]
fn test_target_halving() {
let mut rng = TestRng::default();
let minimum_coinbase_target: u64 = 2u64.pow(10) - 1;
for _ in 0..ITERATIONS {
let previous_coinbase_target: u64 = rng.gen_range(minimum_coinbase_target..u64::MAX);
let previous_timestamp = rng.gen();
let half_life = CurrentNetwork::NUM_BLOCKS_PER_EPOCH
.saturating_div(2)
.saturating_mul(CurrentNetwork::ANCHOR_TIME as u32) as i64;
let next_timestamp = previous_timestamp + half_life;
let next_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
assert!(next_coinbase_target > previous_coinbase_target / 2);
let next_timestamp = previous_timestamp + half_life + CurrentNetwork::ANCHOR_TIME as i64;
let next_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
assert_eq!(next_coinbase_target, previous_coinbase_target / 2);
let next_timestamp = previous_timestamp + half_life + 2 * CurrentNetwork::ANCHOR_TIME as i64;
let next_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
assert!(next_coinbase_target < previous_coinbase_target / 2);
}
}
#[test]
fn test_target_doubling() {
let mut rng = TestRng::default();
const ANCHOR_TIME_DELTA: i64 = 15;
const EXPECTED_NUM_BLOCKS_TO_DOUBLE: u32 = 451;
let minimum_coinbase_target: u64 = 2u64.pow(10) - 1;
let initial_coinbase_target: u64 = rng.gen_range(minimum_coinbase_target..u64::MAX / 2);
let initial_timestamp: i64 = rng.gen();
let mut previous_coinbase_target: u64 = initial_coinbase_target;
let mut previous_timestamp = initial_timestamp;
let mut num_blocks = 0;
while previous_coinbase_target < initial_coinbase_target * 2 {
let next_timestamp = previous_timestamp + ANCHOR_TIME_DELTA;
let next_coinbase_target = coinbase_target(
previous_coinbase_target,
previous_timestamp,
next_timestamp,
CurrentNetwork::ANCHOR_TIME,
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
CurrentNetwork::GENESIS_COINBASE_TARGET,
)
.unwrap();
assert!(next_coinbase_target > previous_coinbase_target);
previous_coinbase_target = next_coinbase_target;
previous_timestamp = next_timestamp;
num_blocks += 1;
}
let seconds = previous_timestamp - initial_timestamp;
println!(
"For drifts of {ANCHOR_TIME_DELTA} seconds and epochs of {} blocks, doubling the coinbase target took {num_blocks} blocks. ({seconds} seconds)",
CurrentNetwork::NUM_BLOCKS_PER_EPOCH,
);
assert_eq!(EXPECTED_NUM_BLOCKS_TO_DOUBLE, num_blocks);
}
}