Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions console/network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ pub trait Network:

/// The starting supply of Aleo credits.
const STARTING_SUPPLY: u64 = 1_500_000_000_000_000; // 1.5B credits
/// The maximum supply of Aleo credits.
/// This value represents the absolute upper bound on all ALEO created over the lifetime of the network.
const MAX_SUPPLY: u64 = 5_000_000_000_000_000; // 5B credits
/// The block height that upper bounds the total supply of Aleo credits to 5 billion.
#[cfg(not(feature = "test"))]
const MAX_SUPPLY_LIMIT_HEIGHT: u32 = 263_527_685;
/// The block height that upper bounds the total supply of Aleo credits to 5 billion.
/// This is deliberately set to a low value for testing purposes only.
#[cfg(feature = "test")]
const MAX_SUPPLY_LIMIT_HEIGHT: u32 = 5;
/// The cost in microcredits per byte for the deployment transaction.
const DEPLOYMENT_FEE_MULTIPLIER: u64 = 1_000; // 1 millicredit per byte
/// The multiplier in microcredits for each command in the constructor.
Expand Down
115 changes: 111 additions & 4 deletions ledger/block/src/helpers/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const V2_MIN_BLOCK_INTERVAL: i64 = 1; // 1 second.
const SECONDS_IN_A_YEAR: u32 = 60 * 60 * 24 * 365;

/// Calculate the block reward based on the network’s consensus version, determined by the given block height.
/// If the block height is at or beyond the max supply limit height, the block reward is zero.
pub fn block_reward<N: Network>(
block_height: u32,
total_supply: u64,
Expand All @@ -37,6 +38,11 @@ pub fn block_reward<N: Network>(
coinbase_reward: u64,
transaction_fees: u64,
) -> Result<u64> {
// If the height is at or beyond the max supply limit height, set rewards to zero.
if block_height >= N::MAX_SUPPLY_LIMIT_HEIGHT {
return Ok(0);
}

// Determine which block reward version to use.
let consensus_version = N::CONSENSUS_VERSION(block_height)?;
match consensus_version == ConsensusVersion::V1 {
Expand Down Expand Up @@ -96,6 +102,7 @@ pub const fn puzzle_reward(coinbase_reward: u64) -> u64 {
}

/// Calculate the coinbase reward based on the network’s consensus version, determined by the given block height.
/// If the block height is at or beyond the max supply limit height, the coinbase reward is zero.
pub fn coinbase_reward<N: Network>(
block_height: u32,
block_timestamp: i64,
Expand All @@ -108,6 +115,11 @@ pub fn coinbase_reward<N: Network>(
cumulative_proof_target: u64,
coinbase_target: u64,
) -> Result<u64> {
// If the height is at or beyond the max supply limit height, set rewards to zero.
if block_height >= N::MAX_SUPPLY_LIMIT_HEIGHT {
return Ok(0);
}

// Determine which coinbase reward version to use.
let consensus_version = N::CONSENSUS_VERSION(block_height)?;
if consensus_version == ConsensusVersion::V1 {
Expand Down Expand Up @@ -795,8 +807,9 @@ mod tests {
assert_eq!(consensus_v1_reward, expected_reward);

// Check that the block reward is correct for the second consensus version.
let consensus_v2_height =
rng.gen_range(TestnetV0::CONSENSUS_HEIGHT(ConsensusVersion::V2).unwrap()..u32::MAX);
let consensus_v2_height = rng.gen_range(
TestnetV0::CONSENSUS_HEIGHT(ConsensusVersion::V2).unwrap()..TestnetV0::MAX_SUPPLY_LIMIT_HEIGHT,
);
let time_since_last_block = rng.gen_range(1..=V2_MAX_BLOCK_INTERVAL);
let consensus_v2_reward = block_reward::<TestnetV0>(
consensus_v2_height,
Expand All @@ -809,6 +822,19 @@ mod tests {
.unwrap();
let expected_reward = block_reward_v2(TestnetV0::STARTING_SUPPLY, time_since_last_block, 0, 0);
assert_eq!(consensus_v2_reward, expected_reward);

// Check that the block reward is 0 after the max supply limit height.
let after_max_supply_limit_height = rng.gen_range(TestnetV0::MAX_SUPPLY_LIMIT_HEIGHT..u32::MAX);
let block_reward = block_reward::<TestnetV0>(
after_max_supply_limit_height,
TestnetV0::STARTING_SUPPLY,
TestnetV0::BLOCK_TIME,
time_since_last_block,
0,
0,
)
.unwrap();
assert_eq!(block_reward, 0);
}
}

Expand Down Expand Up @@ -968,8 +994,9 @@ mod tests {
assert_eq!(consensus_v1_reward, expected_reward);

// Check that the block reward is correct for the second consensus version.
let consensus_v2_height =
rng.gen_range(TestnetV0::CONSENSUS_HEIGHT(ConsensusVersion::V2).unwrap()..u32::MAX);
let consensus_v2_height = rng.gen_range(
TestnetV0::CONSENSUS_HEIGHT(ConsensusVersion::V2).unwrap()..TestnetV0::MAX_SUPPLY_LIMIT_HEIGHT,
);
let block_timestamp = TestnetV0::GENESIS_TIMESTAMP
.saturating_add(consensus_v2_height.saturating_mul(TestnetV0::BLOCK_TIME as u32) as i64);
let consensus_v2_reward = coinbase_reward::<TestnetV0>(
Expand All @@ -996,6 +1023,23 @@ mod tests {
)
.unwrap();
assert_eq!(consensus_v2_reward, expected_reward);

// Check that the coinbase reward is 0 after the max supply limit height.
let after_max_supply_limit_height = rng.gen_range(TestnetV0::MAX_SUPPLY_LIMIT_HEIGHT..u32::MAX);
let coinbase_reward = coinbase_reward::<TestnetV0>(
after_max_supply_limit_height,
block_timestamp,
TestnetV0::GENESIS_TIMESTAMP,
TestnetV0::STARTING_SUPPLY,
TestnetV0::ANCHOR_TIME,
TestnetV0::ANCHOR_HEIGHT,
TestnetV0::BLOCK_TIME,
1,
0,
1,
)
.unwrap();
assert_eq!(coinbase_reward, 0);
}
}

Expand Down Expand Up @@ -1505,6 +1549,69 @@ mod tests {
}
}

fn check_total_supply_cap<N: Network>() {
const AVG_BLOCK_TIME: i64 = 3;

let blocks_per_year = block_height_at_year(AVG_BLOCK_TIME as u16, 1);

// The tracking state for the simluation
let mut total_supply = N::STARTING_SUPPLY;
let mut total_block_rewards = 0u64;
let mut total_coinbase_rewards = 0u64;
let mut block_height = 1u32;
let mut latest_timetamp = 0;

// Iterate until we reach 5 billion credits
while total_supply < N::MAX_SUPPLY {
// Calculate the block reward.
let block_reward =
block_reward::<N>(block_height, N::STARTING_SUPPLY, N::BLOCK_TIME, AVG_BLOCK_TIME, 0, 0).unwrap();

// Calculate the coinbase reward.
let timestamp = N::GENESIS_TIMESTAMP + (block_height as i64 * AVG_BLOCK_TIME);
let coinbase_reward = coinbase_reward::<N>(
block_height,
timestamp,
N::GENESIS_TIMESTAMP,
N::STARTING_SUPPLY,
N::ANCHOR_TIME,
N::ANCHOR_HEIGHT,
N::BLOCK_TIME,
1,
0,
1,
)
.unwrap();

// Calculate the average expected coinbase reward per block based on the retargeting interval.
// This is the upper bound, because we consider hitting 50% of the coinbase target eligible for retargeting.
let avg_coinbase_reward_per_block = coinbase_reward * AVG_BLOCK_TIME as u64 / N::ANCHOR_TIME as u64;

// Update the trackers.
block_height += 1;
total_block_rewards += block_reward;
total_coinbase_rewards += avg_coinbase_reward_per_block;
total_supply += block_reward + avg_coinbase_reward_per_block;
latest_timetamp = timestamp;
}

println!(
"At block height {block_height} (year {}, timestamp: {latest_timetamp}), total block rewards is {total_block_rewards}, total coinbase rewards is {total_coinbase_rewards}, total supply is {total_supply} credits",
block_height / blocks_per_year
);

// Check that block height matches the expected max supply limit height.
assert_eq!(block_height, N::MAX_SUPPLY_LIMIT_HEIGHT);
assert_eq!(N::MAX_SUPPLY_LIMIT_HEIGHT, 263_527_685);
}

#[test]
fn test_total_supply_cap() {
check_total_supply_cap::<CanaryV0>();
check_total_supply_cap::<TestnetV0>();
check_total_supply_cap::<MainnetV0>();
}

#[test]
fn test_targets() {
let mut rng = TestRng::default();
Expand Down
89 changes: 89 additions & 0 deletions ledger/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3235,6 +3235,95 @@ mod valid_solutions {
let block_aborted_solution_id = block.aborted_solution_ids().first().unwrap();
assert_eq!(*block_aborted_solution_id, invalid_solution.id(), "Aborted solutions do not match");
}

#[test]
fn test_no_rewards_after_limit_height() {
let rng = &mut TestRng::default();

// Initialize the test environment.
let crate::test_helpers::TestEnv { ledger, private_key, address, .. } =
crate::test_helpers::sample_test_env(rng);

// Advance the ledger to the reward limit height.
let supply_limit_height = CurrentNetwork::MAX_SUPPLY_LIMIT_HEIGHT;

// Advance until before the supply limit height.
while ledger.latest_height() + 1 < supply_limit_height {
let block = ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![], rng).unwrap();
ledger.advance_to_next_block(&block).unwrap();

// Check that there exists rewards in the block.
assert!(!block.ratifications().is_empty());
let ratifications: Vec<_> = block.ratifications().iter().collect();
match ratifications[0] {
Ratify::BlockReward(block_reward) => {
assert!(*block_reward > 0);
}
_ => panic!("Expected a block reward ratification"),
}
}

// Create one additional block at the supply limit height.
let next_block =
ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![], rng).unwrap();
ledger.advance_to_next_block(&next_block).unwrap();

// Check that the block and puzzle rewards are 0.
assert!(!next_block.ratifications().is_empty());
let ratifications: Vec<_> = next_block.ratifications().iter().collect();
match ratifications[0] {
Ratify::BlockReward(block_reward) => {
assert_eq!(*block_reward, 0);
}
_ => panic!("Expected a block reward ratification"),
}
match ratifications[1] {
Ratify::PuzzleReward(puzzle_reward) => {
assert_eq!(*puzzle_reward, 0);
}
_ => panic!("Expected a puzzle reward ratification"),
}

// Create another block with a valid solution that does not give any rewards.

// Retrieve the puzzle parameters.
let puzzle = ledger.puzzle();
let latest_epoch_hash = ledger.latest_epoch_hash().unwrap();
let minimum_proof_target = ledger.latest_proof_target();

// Create solutions that are greater than the minimum proof target.
let valid_solution = loop {
let solution = puzzle.prove(latest_epoch_hash, address, rng.r#gen(), None).unwrap();
if puzzle.get_proof_target(&solution).unwrap() >= minimum_proof_target {
break solution;
}
};

// Create a block with the valid solution.
let next_block_with_solution = ledger
.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![valid_solution], vec![], rng)
.unwrap();
ledger.advance_to_next_block(&next_block_with_solution).unwrap();

// Check that the block and puzzle rewards are 0.
assert!(!next_block.ratifications().is_empty());
let ratifications: Vec<_> = next_block.ratifications().iter().collect();
match ratifications[0] {
Ratify::BlockReward(block_reward) => {
assert_eq!(*block_reward, 0);
}
_ => panic!("Expected a block reward ratification"),
}
match ratifications[1] {
Ratify::PuzzleReward(puzzle_reward) => {
assert_eq!(*puzzle_reward, 0);
}
_ => panic!("Expected a puzzle reward ratification"),
}

// Check that the solution was accepted.
assert_eq!(next_block_with_solution.solutions().len(), 1);
}
}

/// Tests multiple attacks where the subDAG of a block is invalid
Expand Down