diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index bb4e9df7a5..801778b137 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -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. diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 93a3bc67a4..9ae8af1820 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -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( block_height: u32, total_supply: u64, @@ -37,6 +38,11 @@ pub fn block_reward( coinbase_reward: u64, transaction_fees: u64, ) -> Result { + // 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 { @@ -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( block_height: u32, block_timestamp: i64, @@ -108,6 +115,11 @@ pub fn coinbase_reward( cumulative_proof_target: u64, coinbase_target: u64, ) -> Result { + // 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 { @@ -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::( consensus_v2_height, @@ -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::( + after_max_supply_limit_height, + TestnetV0::STARTING_SUPPLY, + TestnetV0::BLOCK_TIME, + time_since_last_block, + 0, + 0, + ) + .unwrap(); + assert_eq!(block_reward, 0); } } @@ -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::( @@ -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::( + 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); } } @@ -1505,6 +1549,69 @@ mod tests { } } + fn check_total_supply_cap() { + 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::(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::( + 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::(); + check_total_supply_cap::(); + check_total_supply_cap::(); + } + #[test] fn test_targets() { let mut rng = TestRng::default(); diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 412d9c931c..2129bbfbe4 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -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