From e1309dd10560330eb87dbacf8f104005c7f63398 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:29:11 -0400 Subject: [PATCH 1/9] Introduce block_reward_v1 and block_reward_v2 --- ledger/block/src/helpers/target.rs | 141 +++++++++++++++++++++++++---- ledger/block/src/verify.rs | 4 +- ledger/src/advance.rs | 2 +- synthesizer/src/vm/finalize.rs | 2 +- 4 files changed, 126 insertions(+), 23 deletions(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 86c2507bd6..9a1cd99749 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -18,13 +18,17 @@ use console::prelude::{Network, Result, ensure}; /// A safety bound (sanity-check) for the coinbase reward. pub const MAX_COINBASE_REWARD: u64 = 190_258_739; // Coinbase reward at block 1. -/// Calculate the block reward, given the total supply, block time, coinbase reward, and transaction fees. +/// A the maximum block interval in seconds. This is used to cap the block interval in the V2 block reward calculation to +/// prevent the block reward from becoming too large in the event of a long block interval. +const BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL: i64 = 60; // 1 minute. + +/// Calculate the V1 block reward, given the total supply, block time, coinbase reward, and transaction fees. /// R_staking = floor((0.05 * S) / H_Y1) + CR / 3 + TX_F. /// S = Total supply. /// H_Y1 = Expected block height at year 1. /// CR = Coinbase reward. /// TX_F = Transaction fees. -pub const fn block_reward(total_supply: u64, block_time: u16, coinbase_reward: u64, transaction_fees: u64) -> u64 { +pub const fn block_reward_v1(total_supply: u64, block_time: u16, coinbase_reward: u64, transaction_fees: u64) -> u64 { // Compute the expected block height at year 1. let block_height_at_year_1 = block_height_at_year(block_time, 1); // Compute the annual reward: (0.05 * S). @@ -35,20 +39,45 @@ pub const fn block_reward(total_supply: u64, block_time: u16, coinbase_reward: u block_reward + (coinbase_reward / 3) + transaction_fees } +/// Calculate the V2 block reward, given the total supply, block interval, coinbase reward, and transaction fees. +/// R_staking = floor((0.05 * S) * min(I, 60) / S_Y) + CR / 3 + TX_F. +/// S = Total supply. +/// I = Seconds elapsed since last block. +/// S_Y = Seconds in a year (31536000). +/// CR = Coinbase reward. +/// TX_F = Transaction fees. +pub fn block_reward_v2( + total_supply: u64, + time_since_last_block: i64, + coinbase_reward: u64, + transaction_fees: u64, +) -> u64 { + // Calculate the number of seconds in a year. + const SECONDS_IN_A_YEAR: u64 = 60 * 60 * 24 * 365; + // Compute the annual reward: (0.05 * S). + let annual_reward = total_supply / 20; + // Compute the seconds since last block with a maximum of `MAX_BLOCK_INTERVAL` seconds. + // Compute the block reward: (0.05 * S) * min(I, MAX_BLOCK_INTERVAL) / S_Y. + let block_reward = + annual_reward * time_since_last_block.min(BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL) as u64 / SECONDS_IN_A_YEAR; + // Return the sum of the block reward, coinbase reward, and transaction fees. + block_reward + (coinbase_reward / 3) + transaction_fees +} + /// Calculate the puzzle reward, given the coinbase reward. pub const fn puzzle_reward(coinbase_reward: u64) -> u64 { // Return the coinbase reward multiplied by 2 and divided by 3. coinbase_reward.saturating_mul(2).saturating_div(3) } -/// Calculates the coinbase reward for a given block. +/// Calculates the V1 coinbase reward for a given block. /// R_coinbase = R_anchor(H) * min(P, C_R) / C /// R_anchor = Anchor reward at block height. /// H = Current block height. /// P = Combined proof target. /// C_R = Remaining coinbase target. /// C = Coinbase target. -pub fn coinbase_reward( +pub fn coinbase_reward_v1( block_height: u32, starting_supply: u64, anchor_height: u32, @@ -311,6 +340,7 @@ mod tests { 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; + const EXPECTED_MAX_STAKING_REWARD: u64 = 142_694_063; #[test] fn test_anchor_block_reward() { @@ -439,25 +469,98 @@ mod tests { } #[test] - fn test_block_reward() { - let reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME, 0, 0); + fn test_block_reward_v1() { + let reward = block_reward_v1(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME, 0, 0); assert_eq!(reward, EXPECTED_STAKING_REWARD); // Increasing the anchor time will increase the reward. - let larger_reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME + 1, 0, 0); + let larger_reward = block_reward_v1(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME + 1, 0, 0); assert!(reward < larger_reward); // Decreasing the anchor time will decrease the reward. - let smaller_reward = block_reward(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME - 1, 0, 0); + let smaller_reward = block_reward_v1(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME - 1, 0, 0); assert!(reward > smaller_reward); } + #[test] + fn test_block_reward_v2() { + let reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME as i64, 0, 0); + assert_eq!(reward, EXPECTED_STAKING_REWARD); + + // Increasing the anchor time will increase the reward. + let larger_reward = + block_reward_v2(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME as i64 + 1, 0, 0); + assert!(reward < larger_reward); + + // Decreasing the anchor time will decrease the reward. + let smaller_reward = + block_reward_v2(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME as i64 - 1, 0, 0); + assert!(reward > smaller_reward); + + // Increasing the block interval past `V2_MAX_BLOCK_INTERVAL` does not increase the reward. + let max_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL, 0, 0); + assert_eq!(max_reward, EXPECTED_MAX_STAKING_REWARD); + let equivalent_reward = + block_reward_v2(CurrentNetwork::STARTING_SUPPLY, BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL + 1, 0, 0); + assert_eq!(max_reward, equivalent_reward); + } + + #[test] + fn test_block_reward_v1_vs_v2() { + let mut rng = TestRng::default(); + + // Declare a tolerance for reward divergence between v1 and v2 due to truncation. + const TOLERANCE: f64 = 0.001; // 0.1% tolerance + + // Expect that the v2 block reward is equivalent to the v1 block reward if the `CurrentNetwork::BLOCK_TIME` is fixed. + let reward_v1 = block_reward_v1(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME, 0, 0); + assert_eq!(reward_v1, EXPECTED_STAKING_REWARD); + let reward_v2 = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME as i64, 0, 0); + assert_eq!(reward_v1, reward_v2); + + // Decreasing the time since last block based on `CurrentNetwork::BLOCK_TIME` will proportionally reduce the v2 rewards. + let shorter_time = CurrentNetwork::BLOCK_TIME / 2; + let smaller_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, shorter_time as i64, 0, 0); + let expected_reward = EXPECTED_STAKING_REWARD / 2; + assert!((smaller_reward as f64 - expected_reward as f64).abs() / expected_reward as f64 <= TOLERANCE); + + // Increasing the time since last block based on `CurrentNetwork::BLOCK_TIME` will proportionally increase the v2 rewards (up to a certain cap). + let longer_time = CurrentNetwork::BLOCK_TIME * 2; + let larger_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, longer_time as i64, 0, 0); + let expected_reward = EXPECTED_STAKING_REWARD * 2; + assert!((larger_reward as f64 - expected_reward as f64).abs() / expected_reward as f64 <= TOLERANCE); + + for _ in 0..10 { + // Randomly sample the time factor. + let factor = rng.gen_range(1..10); + + // Ensure that scaling the time elapsed down scales the reward down proportionally. + let shorter_time = CurrentNetwork::BLOCK_TIME / factor; + let time_factor: f64 = CurrentNetwork::BLOCK_TIME as f64 / shorter_time as f64; + let smaller_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, shorter_time as i64, 0, 0); + let expected_reward = (EXPECTED_STAKING_REWARD as f64 / time_factor) as u64; + assert!((smaller_reward as f64 - expected_reward as f64).abs() / expected_reward as f64 <= TOLERANCE); + + // Ensure that scaling the time elapsed up scales the reward up proportionally (up to a certain cap). + let longer_time = CurrentNetwork::BLOCK_TIME * factor; + let time_factor: f64 = longer_time as f64 / CurrentNetwork::BLOCK_TIME as f64; + let larger_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, longer_time as i64, 0, 0); + let expected_reward = (EXPECTED_STAKING_REWARD as f64 * time_factor) as u64; + match longer_time as i64 > BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL { + true => assert_eq!(larger_reward, EXPECTED_MAX_STAKING_REWARD), + false => { + assert!((larger_reward as f64 - expected_reward as f64).abs() / expected_reward as f64 <= TOLERANCE) + } + } + } + } + #[test] fn test_coinbase_reward() { let coinbase_target: u64 = 10000; let combined_proof_target: u128 = coinbase_target as u128; - let reward = coinbase_reward( + let reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -470,7 +573,7 @@ mod tests { assert_eq!(reward, EXPECTED_COINBASE_REWARD_AT_BLOCK_1); // Halving the combined proof target halves the reward. - let smaller_reward = coinbase_reward( + let smaller_reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -483,7 +586,7 @@ mod tests { assert_eq!(smaller_reward, reward / 2); // Halving the remaining coinbase target halves the reward. - let smaller_reward = coinbase_reward( + let smaller_reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -496,7 +599,7 @@ mod tests { assert_eq!(smaller_reward, reward / 2); // Dramatically increasing the combined proof target greater than the remaining coinbase target will not increase the reward. - let equivalent_reward = coinbase_reward( + let equivalent_reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -509,7 +612,7 @@ mod tests { assert_eq!(reward, equivalent_reward); // Decreasing the combined proof target to 0 will result in a reward of 0. - let zero_reward = coinbase_reward( + let zero_reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -522,7 +625,7 @@ mod tests { assert_eq!(zero_reward, 0); // Increasing the cumulative proof target beyond the coinbase target will result in a reward of 0. - let zero_reward = coinbase_reward( + let zero_reward = coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -544,7 +647,7 @@ mod tests { cumulative_proof_target: u64, coinbase_target: u64, ) -> u64 { - coinbase_reward( + coinbase_reward_v1( 1, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -599,7 +702,7 @@ mod tests { let mut block_height = 1; - let mut previous_reward = coinbase_reward( + let mut previous_reward = coinbase_reward_v1( block_height, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -621,7 +724,7 @@ mod tests { let mut hit_1b = false; while block_height < block_height_at_year_10 { - let reward = coinbase_reward( + let reward = coinbase_reward_v1( block_height, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -664,7 +767,7 @@ mod tests { let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10); // Check that the block at year 10 has a reward of 15. - let reward = coinbase_reward( + let reward = coinbase_reward_v1( block_height_at_year_10, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, @@ -691,7 +794,7 @@ mod tests { ); assert_eq!(anchor_reward, 19_025_874); - let reward = coinbase_reward( + let reward = coinbase_reward_v1( block_height, CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::ANCHOR_HEIGHT, diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index d7d4cf6cbd..17d3201dfd 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -388,7 +388,7 @@ impl Block { )?; // Calculate the expected coinbase reward. - let expected_coinbase_reward = coinbase_reward( + let expected_coinbase_reward = coinbase_reward_v1( height, N::STARTING_SUPPLY, N::ANCHOR_HEIGHT, @@ -404,7 +404,7 @@ impl Block { // Compute the expected block reward. let expected_block_reward = - block_reward(N::STARTING_SUPPLY, N::BLOCK_TIME, expected_coinbase_reward, expected_transaction_fees); + block_reward_v1(N::STARTING_SUPPLY, N::BLOCK_TIME, expected_coinbase_reward, expected_transaction_fees); // Compute the expected puzzle reward. let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward); diff --git a/ledger/src/advance.rs b/ledger/src/advance.rs index a5bcc2f68d..7781e0c4d4 100644 --- a/ledger/src/advance.rs +++ b/ledger/src/advance.rs @@ -295,7 +295,7 @@ impl> Ledger { )?; // Calculate the coinbase reward. - let coinbase_reward = coinbase_reward( + let coinbase_reward = coinbase_reward_v1( next_height, N::STARTING_SUPPLY, N::ANCHOR_HEIGHT, diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 2cbef6359a..91106781b5 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -478,7 +478,7 @@ impl> VM { }; // Compute the block reward. - let block_reward = ledger_block::block_reward( + let block_reward = ledger_block::block_reward_v1( N::STARTING_SUPPLY, N::BLOCK_TIME, coinbase_reward, From 7bde13d19b725ad320943d9374575807702f2c36 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:37:05 -0400 Subject: [PATCH 2/9] Introduce CONSENSUS_V2_HEIGHT to network traits --- console/network/src/canary_v0.rs | 7 +++++++ console/network/src/lib.rs | 3 +++ console/network/src/mainnet_v0.rs | 7 +++++++ console/network/src/testnet_v0.rs | 7 +++++++ 4 files changed, 24 insertions(+) diff --git a/console/network/src/canary_v0.rs b/console/network/src/canary_v0.rs index a50b528577..5b115b7b71 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -133,6 +133,13 @@ impl Network for CanaryV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which consensus V2 rules apply. + #[cfg(not(any(test, feature = "test")))] + const CONSENSUS_V2_HEIGHT: u32 = 2_500_000; + // TODO (raychu86): Update this value based on the desired canary height. + /// The block height from which consensus V2 rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 0; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 7c8a61b9b7..8a627bafed 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -91,6 +91,9 @@ pub trait Network: /// The network edition. const EDITION: u16; + /// The block height from which consensus V2 rules apply. + const CONSENSUS_V2_HEIGHT: u32; + /// The function name for the inclusion circuit. const INCLUSION_FUNCTION_NAME: &'static str; diff --git a/console/network/src/mainnet_v0.rs b/console/network/src/mainnet_v0.rs index 6aadaa3f38..76d3a51419 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -134,6 +134,13 @@ impl Network for MainnetV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which consensus V2 rules apply. + #[cfg(not(any(test, feature = "test")))] + const CONSENSUS_V2_HEIGHT: u32 = 2_000_000; + // TODO (raychu86): Update this value based on the desired mainnet height. + /// The block height from which consensus V2 rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 0; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. diff --git a/console/network/src/testnet_v0.rs b/console/network/src/testnet_v0.rs index 974db5a7c0..63ef04e2a7 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -133,6 +133,13 @@ impl Network for TestnetV0 { /// The transmission checksum type. type TransmissionChecksum = u128; + /// The block height from which consensus V2 rules apply. + #[cfg(not(any(test, feature = "test")))] + const CONSENSUS_V2_HEIGHT: u32 = 2_500_000; + // TODO (raychu86): Update this value based on the desired testnet height. + /// The block height from which consensus V2 rules apply. + #[cfg(any(test, feature = "test"))] + const CONSENSUS_V2_HEIGHT: u32 = 10; /// The network edition. const EDITION: u16 = 0; /// The genesis block coinbase target. From 6010a29f07c91ee5f283d5d2e1e56cc645b43ed1 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:41:54 -0400 Subject: [PATCH 3/9] Use new block_reward impl --- ledger/block/src/helpers/target.rs | 92 ++++++++++++++-- ledger/block/src/verify.rs | 12 ++- ledger/src/advance.rs | 1 + ledger/src/check_next_block.rs | 11 +- synthesizer/src/vm/finalize.rs | 101 +++++++++++++++--- synthesizer/src/vm/mod.rs | 16 ++- synthesizer/src/vm/verify.rs | 13 ++- .../tests/test_vm_execute_and_finalize.rs | 24 ++++- 8 files changed, 232 insertions(+), 38 deletions(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 9a1cd99749..49a9692791 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -18,9 +18,28 @@ use console::prelude::{Network, Result, ensure}; /// A safety bound (sanity-check) for the coinbase reward. pub const MAX_COINBASE_REWARD: u64 = 190_258_739; // Coinbase reward at block 1. -/// A the maximum block interval in seconds. This is used to cap the block interval in the V2 block reward calculation to +/// A the maximum block interval in seconds. This is used to upper bound the block interval in the V2 block reward calculation to /// prevent the block reward from becoming too large in the event of a long block interval. -const BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL: i64 = 60; // 1 minute. +const V2_MAX_BLOCK_INTERVAL: i64 = 60; // 1 minute. +/// A the minimum block interval in seconds. This is used to lower bound the block interval in the V2 block reward calculation to +/// prevent the block reward from becoming too small in the event of an extremely short block interval. +const V2_MIN_BLOCK_INTERVAL: i64 = 1; // 1 second. + +/// Calculate the block reward based on the network’s consensus version, determined by the given block height. +pub fn block_reward( + block_height: u32, + total_supply: u64, + block_time: u16, + time_since_last_block: i64, + coinbase_reward: u64, + transaction_fees: u64, +) -> u64 { + // Determine which block reward version to use. + match block_height < N::CONSENSUS_V2_HEIGHT { + true => block_reward_v1(total_supply, block_time, coinbase_reward, transaction_fees), + false => block_reward_v2(total_supply, time_since_last_block, coinbase_reward, transaction_fees), + } +} /// Calculate the V1 block reward, given the total supply, block time, coinbase reward, and transaction fees. /// R_staking = floor((0.05 * S) / H_Y1) + CR / 3 + TX_F. @@ -56,10 +75,10 @@ pub fn block_reward_v2( const SECONDS_IN_A_YEAR: u64 = 60 * 60 * 24 * 365; // Compute the annual reward: (0.05 * S). let annual_reward = total_supply / 20; - // Compute the seconds since last block with a maximum of `MAX_BLOCK_INTERVAL` seconds. - // Compute the block reward: (0.05 * S) * min(I, MAX_BLOCK_INTERVAL) / S_Y. - let block_reward = - annual_reward * time_since_last_block.min(BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL) as u64 / SECONDS_IN_A_YEAR; + // Compute the seconds since last block with a maximum of `V2_MAX_BLOCK_INTERVAL` seconds and minimum of `V2_MIN_BLOCK_INTERVAL` seconds; + let time_since_last_block = time_since_last_block.max(V2_MIN_BLOCK_INTERVAL).min(V2_MAX_BLOCK_INTERVAL); + // Compute the block reward: (0.05 * S) * min(max(I, MIN_BLOCK_INTERVAL), MAX_BLOCK_INTERVAL) / S_Y. + let block_reward = annual_reward * time_since_last_block as u64 / SECONDS_IN_A_YEAR; // Return the sum of the block reward, coinbase reward, and transaction fees. block_reward + (coinbase_reward / 3) + transaction_fees } @@ -331,7 +350,7 @@ pub fn to_next_targets( #[cfg(test)] mod tests { use super::*; - use console::network::{MainnetV0, prelude::*}; + use console::network::{MainnetV0, TestnetV0, prelude::*}; type CurrentNetwork = MainnetV0; @@ -468,6 +487,53 @@ mod tests { check_sum_of_anchor_rewards(15, 3329999764466813); } + #[test] + fn test_block_reward() { + let mut rng = TestRng::default(); + + // Ensure that a block height of `TestnetV0::CONSENSUS_V2_HEIGHT` uses block reward V2. + let time_since_last_block = rng.gen_range(1..=V2_MAX_BLOCK_INTERVAL); + let reward = block_reward::( + TestnetV0::CONSENSUS_V2_HEIGHT, + TestnetV0::STARTING_SUPPLY, + TestnetV0::BLOCK_TIME, + time_since_last_block, + 0, + 0, + ); + let expected_reward = block_reward_v2(TestnetV0::STARTING_SUPPLY, time_since_last_block, 0, 0); + assert_eq!(reward, expected_reward); + + for _ in 0..100 { + // Check that the block reward is correct for the first consensus version. + let consensus_v1_height = rng.gen_range(0..TestnetV0::CONSENSUS_V2_HEIGHT); + let consensus_v1_reward = block_reward::( + consensus_v1_height, + TestnetV0::STARTING_SUPPLY, + TestnetV0::BLOCK_TIME, + 0, + 0, + 0, + ); + let expected_reward = block_reward_v1(TestnetV0::STARTING_SUPPLY, TestnetV0::BLOCK_TIME, 0, 0); + 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_V2_HEIGHT..u32::MAX); + let time_since_last_block = rng.gen_range(1..=V2_MAX_BLOCK_INTERVAL); + let consensus_v2_reward = block_reward::( + consensus_v2_height, + TestnetV0::STARTING_SUPPLY, + TestnetV0::BLOCK_TIME, + time_since_last_block, + 0, + 0, + ); + let expected_reward = block_reward_v2(TestnetV0::STARTING_SUPPLY, time_since_last_block, 0, 0); + assert_eq!(consensus_v2_reward, expected_reward); + } + } + #[test] fn test_block_reward_v1() { let reward = block_reward_v1(CurrentNetwork::STARTING_SUPPLY, CurrentNetwork::BLOCK_TIME, 0, 0); @@ -498,11 +564,15 @@ mod tests { assert!(reward > smaller_reward); // Increasing the block interval past `V2_MAX_BLOCK_INTERVAL` does not increase the reward. - let max_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL, 0, 0); + let max_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, V2_MAX_BLOCK_INTERVAL, 0, 0); assert_eq!(max_reward, EXPECTED_MAX_STAKING_REWARD); - let equivalent_reward = - block_reward_v2(CurrentNetwork::STARTING_SUPPLY, BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL + 1, 0, 0); + let equivalent_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, V2_MAX_BLOCK_INTERVAL + 1, 0, 0); assert_eq!(max_reward, equivalent_reward); + + // Test that there is a minimum block reward when the time since last block is 1 second. + let min_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, 1, 0, 0); + let equivalent_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, 0, 0, 0); + assert_eq!(min_reward, equivalent_reward); } #[test] @@ -546,7 +616,7 @@ mod tests { let time_factor: f64 = longer_time as f64 / CurrentNetwork::BLOCK_TIME as f64; let larger_reward = block_reward_v2(CurrentNetwork::STARTING_SUPPLY, longer_time as i64, 0, 0); let expected_reward = (EXPECTED_STAKING_REWARD as f64 * time_factor) as u64; - match longer_time as i64 > BLOCK_REWARD_V2_MAX_BLOCK_INTERVAL { + match longer_time as i64 > V2_MAX_BLOCK_INTERVAL { true => assert_eq!(larger_reward, EXPECTED_MAX_STAKING_REWARD), false => { assert!((larger_reward as f64 - expected_reward as f64).abs() / expected_reward as f64 <= TOLERANCE) diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index 17d3201dfd..215232aaab 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -402,9 +402,17 @@ impl Block { let expected_transaction_fees = self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::>()?; + // Calculate the time since last block. + let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp()); // Compute the expected block reward. - let expected_block_reward = - block_reward_v1(N::STARTING_SUPPLY, N::BLOCK_TIME, expected_coinbase_reward, expected_transaction_fees); + let expected_block_reward = block_reward::( + height, + N::STARTING_SUPPLY, + N::BLOCK_TIME, + time_since_last_block, + expected_coinbase_reward, + expected_transaction_fees, + ); // Compute the expected puzzle reward. let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward); diff --git a/ledger/src/advance.rs b/ledger/src/advance.rs index 7781e0c4d4..dd4f22cb98 100644 --- a/ledger/src/advance.rs +++ b/ledger/src/advance.rs @@ -316,6 +316,7 @@ impl> Ledger { // Speculate over the ratifications, solutions, and transactions. let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = self.vm.speculate( state, + next_timestamp.saturating_sub(previous_block.timestamp()), Some(coinbase_reward), candidate_ratifications, &solutions, diff --git a/ledger/src/check_next_block.rs b/ledger/src/check_next_block.rs index 4c0a97a6ea..ac27f8a1e7 100644 --- a/ledger/src/check_next_block.rs +++ b/ledger/src/check_next_block.rs @@ -70,8 +70,15 @@ impl> Ledger { )?; // Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique. - let ratified_finalize_operations = - self.vm.check_speculate(state, block.ratifications(), block.solutions(), block.transactions(), rng)?; + let time_since_last_block = block.timestamp().saturating_sub(self.latest_timestamp()); + let ratified_finalize_operations = self.vm.check_speculate( + state, + time_since_last_block, + block.ratifications(), + block.solutions(), + block.transactions(), + rng, + )?; // Retrieve the committee lookback. let committee_lookback = { diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 91106781b5..3387fb2865 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -35,6 +35,7 @@ impl> VM { pub fn speculate<'a, R: Rng + CryptoRng>( &self, state: FinalizeGlobalState, + time_since_last_block: i64, // TODO (raychu86): Consider moving this value into `FinalizeGlobalState`. coinbase_reward: Option, candidate_ratifications: Vec>, candidate_solutions: &Solutions, @@ -62,6 +63,7 @@ impl> VM { let (ratifications, confirmed_transactions, speculation_aborted_transactions, ratified_finalize_operations) = self.atomic_speculate( state, + time_since_last_block, coinbase_reward, candidate_ratifications, candidate_solutions, @@ -104,6 +106,7 @@ impl> VM { pub fn check_speculate( &self, state: FinalizeGlobalState, + time_since_last_block: i64, ratifications: &Ratifications, solutions: &Solutions, transactions: &Transactions, @@ -129,7 +132,14 @@ impl> VM { // Performs a **dry-run** over the list of ratifications, solutions, and transactions. let (speculate_ratifications, confirmed_transactions, aborted_transactions, ratified_finalize_operations) = - self.atomic_speculate(state, None, candidate_ratifications, solutions, candidate_transactions.iter())?; + self.atomic_speculate( + state, + time_since_last_block, + None, + candidate_ratifications, + solutions, + candidate_transactions.iter(), + )?; // Ensure the ratifications after speculation match. if ratifications != &speculate_ratifications { @@ -193,6 +203,7 @@ impl> VM { fn atomic_speculate<'a>( &self, state: FinalizeGlobalState, + time_since_last_block: i64, coinbase_reward: Option, ratifications: Vec>, solutions: &Solutions, @@ -478,9 +489,11 @@ impl> VM { }; // Compute the block reward. - let block_reward = ledger_block::block_reward_v1( + let block_reward = ledger_block::block_reward::( + state.block_height(), N::STARTING_SUPPLY, N::BLOCK_TIME, + time_since_last_block, coinbase_reward, transaction_fees, ); @@ -1458,8 +1471,10 @@ finalize transfer_public: rng: &mut R, ) -> Result> { // Speculate on the candidate ratifications, solutions, and transactions. + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm.speculate( sample_finalize_state(previous_block.height() + 1), + time_since_last_block, None, vec![], &None.into(), @@ -1478,7 +1493,7 @@ finalize transfer_public: CurrentNetwork::GENESIS_PROOF_TARGET, previous_block.last_coinbase_target(), previous_block.last_coinbase_timestamp(), - CurrentNetwork::GENESIS_TIMESTAMP + 1, + previous_block.timestamp().saturating_add(time_since_last_block), )?; // Construct the new block header. @@ -1736,6 +1751,7 @@ finalize transfer_public: let (ratifications, confirmed_transactions, aborted_transaction_ids, _) = vm .speculate( sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, None, vec![], &None.into(), @@ -1763,7 +1779,14 @@ finalize transfer_public: // Ensure the dry run of the redeployment will cause a reject transaction to be created. let (_, candidate_transactions, aborted_transaction_ids, _) = vm - .atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), [deployment_transaction].iter()) + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + [deployment_transaction].iter(), + ) .unwrap(); assert_eq!(candidate_transactions.len(), 1); assert!(matches!(candidate_transactions[0], ConfirmedTransaction::RejectedDeploy(..))); @@ -1849,8 +1872,16 @@ finalize transfer_public: // Speculate on the transactions. let transactions = [bond_validator_transaction.clone()]; - let (_, confirmed_transactions, _, _) = - vm.atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter()).unwrap(); + let (_, confirmed_transactions, _, _) = vm + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + transactions.iter(), + ) + .unwrap(); // Assert that the transaction is rejected. assert_eq!(confirmed_transactions.len(), 1); @@ -1952,8 +1983,16 @@ finalize transfer_public: // Transfer_20 -> Balance = 20 - 20 = 0 { let transactions = [mint_10.clone(), transfer_10.clone(), transfer_20.clone()]; - let (_, confirmed_transactions, aborted_transaction_ids, _) = - vm.atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter()).unwrap(); + let (_, confirmed_transactions, aborted_transaction_ids, _) = vm + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + transactions.iter(), + ) + .unwrap(); // Assert that all the transactions are accepted. assert_eq!(confirmed_transactions.len(), 3); @@ -1972,8 +2011,16 @@ finalize transfer_public: // Transfer_30 -> Balance = 30 - 30 = 0 { let transactions = [transfer_20.clone(), mint_10.clone(), mint_20.clone(), transfer_30.clone()]; - let (_, confirmed_transactions, aborted_transaction_ids, _) = - vm.atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter()).unwrap(); + let (_, confirmed_transactions, aborted_transaction_ids, _) = vm + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + transactions.iter(), + ) + .unwrap(); // Assert that all the transactions are accepted. assert_eq!(confirmed_transactions.len(), 4); @@ -1992,8 +2039,16 @@ finalize transfer_public: // Transfer_10 -> Balance = 0 - 10 = -10 (should be rejected) { let transactions = [transfer_20.clone(), transfer_10.clone()]; - let (_, confirmed_transactions, aborted_transaction_ids, _) = - vm.atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter()).unwrap(); + let (_, confirmed_transactions, aborted_transaction_ids, _) = vm + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + transactions.iter(), + ) + .unwrap(); // Assert that the accepted and rejected transactions are correct. assert_eq!(confirmed_transactions.len(), 2); @@ -2016,8 +2071,16 @@ finalize transfer_public: // Transfer_10 -> Balance = 10 - 10 = 0 { let transactions = [mint_20.clone(), transfer_30.clone(), transfer_20.clone(), transfer_10.clone()]; - let (_, confirmed_transactions, aborted_transaction_ids, _) = - vm.atomic_speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter()).unwrap(); + let (_, confirmed_transactions, aborted_transaction_ids, _) = vm + .atomic_speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + transactions.iter(), + ) + .unwrap(); // Assert that the accepted and rejected transactions are correct. assert_eq!(confirmed_transactions.len(), 4); @@ -2116,7 +2179,15 @@ function ped_hash: // Speculatively execute the transaction. Ensure that this call does not panic and returns a rejected transaction. let (_, confirmed_transactions, aborted_transaction_ids, _) = vm - .speculate(sample_finalize_state(1), None, vec![], &None.into(), [transaction.clone()].iter(), rng) + .speculate( + sample_finalize_state(1), + CurrentNetwork::BLOCK_TIME as i64, + None, + vec![], + &None.into(), + [transaction.clone()].iter(), + rng, + ) .unwrap(); assert!(aborted_transaction_ids.is_empty()); diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index 0be0f85440..12a1da9a6f 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -361,7 +361,7 @@ impl> VM { let state = FinalizeGlobalState::new_genesis::()?; // Speculate on the ratifications, solutions, and transactions. let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = - self.speculate(state, None, ratifications, &solutions, transactions.iter(), rng)?; + self.speculate(state, 0, None, ratifications, &solutions, transactions.iter(), rng)?; ensure!( aborted_transaction_ids.is_empty(), "Failed to initialize a genesis block - found aborted transaction IDs" @@ -764,8 +764,16 @@ function compute: let previous_block = vm.block_store().get_block(&block_hash).unwrap().unwrap(); // Construct the new block header. - let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = - vm.speculate(sample_finalize_state(1), None, vec![], &None.into(), transactions.iter(), rng)?; + let time_since_last_block = MainnetV0::BLOCK_TIME as i64; + let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm.speculate( + sample_finalize_state(1), + time_since_last_block, + None, + vec![], + &None.into(), + transactions.iter(), + rng, + )?; // Construct the metadata associated with the block. let metadata = Metadata::new( @@ -778,7 +786,7 @@ function compute: MainnetV0::GENESIS_PROOF_TARGET, previous_block.last_coinbase_target(), previous_block.last_coinbase_timestamp(), - MainnetV0::GENESIS_TIMESTAMP + 1, + previous_block.timestamp().saturating_add(time_since_last_block), )?; let header = Header::from( diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 5a2a2d3617..16f442eaa4 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -547,8 +547,17 @@ mod tests { let deployment_transaction = vm.deploy(&caller_private_key, &program, Some(credits), 10, None, rng).unwrap(); // Construct the new block header. + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm - .speculate(sample_finalize_state(1), Some(0u64), vec![], &None.into(), [deployment_transaction].iter(), rng) + .speculate( + sample_finalize_state(1), + time_since_last_block, + Some(0u64), + vec![], + &None.into(), + [deployment_transaction].iter(), + rng, + ) .unwrap(); assert!(aborted_transaction_ids.is_empty()); @@ -563,7 +572,7 @@ mod tests { CurrentNetwork::GENESIS_PROOF_TARGET, genesis.last_coinbase_target(), genesis.last_coinbase_timestamp(), - CurrentNetwork::GENESIS_TIMESTAMP + 1, + genesis.timestamp().saturating_add(time_since_last_block), ) .unwrap(); diff --git a/synthesizer/tests/test_vm_execute_and_finalize.rs b/synthesizer/tests/test_vm_execute_and_finalize.rs index 0eeb15aa79..944e00173c 100644 --- a/synthesizer/tests/test_vm_execute_and_finalize.rs +++ b/synthesizer/tests/test_vm_execute_and_finalize.rs @@ -90,9 +90,11 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { rng, ) .unwrap(); + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm .speculate( construct_finalize_global_state(&vm), + time_since_last_block, Some(0u64), vec![], &None.into(), @@ -104,6 +106,7 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { let block = construct_next_block( &vm, + time_since_last_block, &genesis_private_key, ratifications, transactions, @@ -134,9 +137,11 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { } }; + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm .speculate( construct_finalize_global_state(&vm), + time_since_last_block, Some(0u64), vec![], &None.into(), @@ -148,6 +153,7 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { let block = construct_next_block( &vm, + time_since_last_block, &genesis_private_key, ratifications, transactions, @@ -280,9 +286,11 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { ); // Speculate on the ratifications, solutions, and transaction. + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = match vm .speculate( construct_finalize_global_state(&vm), + time_since_last_block, Some(0u64), vec![], &None.into(), @@ -318,6 +326,7 @@ fn run_test(test: &ProgramTest) -> serde_yaml::Mapping { // Construct the next block. let block = construct_next_block( &vm, + time_since_last_block, &private_key, ratifications, transactions, @@ -422,14 +431,24 @@ fn construct_fee_records, R: Rng + CryptoRng } } + let time_since_last_block = CurrentNetwork::BLOCK_TIME as i64; let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = vm - .speculate(construct_finalize_global_state(vm), Some(0u64), vec![], &None.into(), transactions.iter(), rng) + .speculate( + construct_finalize_global_state(vm), + time_since_last_block, + Some(0u64), + vec![], + &None.into(), + transactions.iter(), + rng, + ) .unwrap(); assert!(aborted_transaction_ids.is_empty()); // Create a block for the fee transactions and add them to the VM. let block = construct_next_block( vm, + time_since_last_block, private_key, ratifications, transactions, @@ -449,6 +468,7 @@ fn construct_fee_records, R: Rng + CryptoRng // A helper function to construct the next block. fn construct_next_block, R: Rng + CryptoRng>( vm: &VM, + time_since_last_block: i64, private_key: &PrivateKey, ratifications: Ratifications, transactions: Transactions, @@ -471,7 +491,7 @@ fn construct_next_block, R: Rng + CryptoRng> CurrentNetwork::GENESIS_PROOF_TARGET, previous_block.last_coinbase_target(), previous_block.last_coinbase_timestamp(), - CurrentNetwork::GENESIS_TIMESTAMP + 1, + previous_block.timestamp().saturating_add(time_since_last_block), )?; // Construct the block header. let header = Header::from( From ebf707579666a379d96bba61fd76f2d0800ae2a7 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 19:47:20 -0400 Subject: [PATCH 4/9] clippy --- ledger/block/src/helpers/target.rs | 2 +- synthesizer/src/vm/finalize.rs | 1 + synthesizer/tests/test_vm_execute_and_finalize.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 49a9692791..42960b9f77 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -76,7 +76,7 @@ pub fn block_reward_v2( // Compute the annual reward: (0.05 * S). let annual_reward = total_supply / 20; // Compute the seconds since last block with a maximum of `V2_MAX_BLOCK_INTERVAL` seconds and minimum of `V2_MIN_BLOCK_INTERVAL` seconds; - let time_since_last_block = time_since_last_block.max(V2_MIN_BLOCK_INTERVAL).min(V2_MAX_BLOCK_INTERVAL); + let time_since_last_block = time_since_last_block.clamp(V2_MIN_BLOCK_INTERVAL, V2_MAX_BLOCK_INTERVAL); // Compute the block reward: (0.05 * S) * min(max(I, MIN_BLOCK_INTERVAL), MAX_BLOCK_INTERVAL) / S_Y. let block_reward = annual_reward * time_since_last_block as u64 / SECONDS_IN_A_YEAR; // Return the sum of the block reward, coinbase reward, and transaction fees. diff --git a/synthesizer/src/vm/finalize.rs b/synthesizer/src/vm/finalize.rs index 3387fb2865..d703f63342 100644 --- a/synthesizer/src/vm/finalize.rs +++ b/synthesizer/src/vm/finalize.rs @@ -32,6 +32,7 @@ impl> VM { /// `Ratify::BlockReward(block_reward)` and `Ratify::PuzzleReward(puzzle_reward)` /// to the front of the `ratifications` list. #[inline] + #[allow(clippy::too_many_arguments)] pub fn speculate<'a, R: Rng + CryptoRng>( &self, state: FinalizeGlobalState, diff --git a/synthesizer/tests/test_vm_execute_and_finalize.rs b/synthesizer/tests/test_vm_execute_and_finalize.rs index 944e00173c..c15e42c4db 100644 --- a/synthesizer/tests/test_vm_execute_and_finalize.rs +++ b/synthesizer/tests/test_vm_execute_and_finalize.rs @@ -466,6 +466,7 @@ fn construct_fee_records, R: Rng + CryptoRng } // A helper function to construct the next block. +#[allow(clippy::too_many_arguments)] fn construct_next_block, R: Rng + CryptoRng>( vm: &VM, time_since_last_block: i64, From 54c6b7928b0408054326fe5ba13fa0d3791b499e Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:54:03 -0400 Subject: [PATCH 5/9] Introduce coinbase_reward_v2 --- ledger/block/src/helpers/target.rs | 502 ++++++++++++++++++++++++++++- 1 file changed, 493 insertions(+), 9 deletions(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 42960b9f77..afbe682cf0 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -59,12 +59,14 @@ pub const fn block_reward_v1(total_supply: u64, block_time: u16, coinbase_reward } /// Calculate the V2 block reward, given the total supply, block interval, coinbase reward, and transaction fees. -/// R_staking = floor((0.05 * S) * min(I, 60) / S_Y) + CR / 3 + TX_F. +/// R_staking = floor((0.05 * S) * clamp(I, MIN_BI, MAX_BI) / S_Y) + CR / 3 + TX_F. /// S = Total supply. /// I = Seconds elapsed since last block. /// S_Y = Seconds in a year (31536000). /// CR = Coinbase reward. /// TX_F = Transaction fees. +/// MIN_BI = Minimum block interval. +/// MAX_BI = Maximum block interval. pub fn block_reward_v2( total_supply: u64, time_since_last_block: i64, @@ -124,6 +126,42 @@ pub fn coinbase_reward_v1( Ok(u64::try_from(reward).expect("Coinbase reward exceeds u64::MAX")) } +/// Calculates the V2 coinbase reward for a given block. +/// R_coinbase = R_anchor(H) * min(P, C_R) / C +/// R_anchor = Anchor reward at block height. +/// H = Current block height. +/// P = Combined proof target. +/// C_R = Remaining coinbase target. +/// C = Coinbase target. +pub fn coinbase_reward_v2( + block_timestamp: i64, + genesis_timestamp: i64, + starting_supply: u64, + anchor_time: u16, + combined_proof_target: u128, + cumulative_proof_target: u64, + coinbase_target: u64, +) -> Result { + // Compute the remaining coinbase target. + let remaining_coinbase_target = coinbase_target.saturating_sub(cumulative_proof_target); + // Compute the remaining proof target. + let remaining_proof_target = combined_proof_target.min(remaining_coinbase_target as u128); + + // Compute the anchor block reward. + let anchor_block_reward = + anchor_block_reward_at_timestamp(block_timestamp, genesis_timestamp, starting_supply, anchor_time); + + // Calculate the coinbase reward. + let reward = anchor_block_reward.saturating_mul(remaining_proof_target).saturating_div(coinbase_target as u128); + + // Ensure the coinbase reward is less than the maximum coinbase reward. + ensure!(reward <= MAX_COINBASE_REWARD as u128, "Coinbase reward ({reward}) exceeds maximum {MAX_COINBASE_REWARD}"); + + // Return the coinbase reward. + // Note: This '.expect' is guaranteed to be safe, as we ensure the reward is within a safe bound. + Ok(u64::try_from(reward).expect("Coinbase reward exceeds u64::MAX")) +} + /// Calculates the anchor block reward for the given block height. /// R_anchor = max(floor((2 * S * H_A * H_R) / (H_Y10 * (H_Y10 + 1))), R_Y9). /// S = Starting supply. @@ -156,6 +194,69 @@ fn anchor_block_reward_at_height(block_height: u32, starting_supply: u64, anchor reward_at_block_height.max(reward_at_year_9) } +/// Calculates the anchor block reward for the given block timestamp. +/// This function uses timestamp rather than block height to determine the reward in order to combat +/// the volatility of block times and better align with human timescales. +/// R_anchor = max(floor((2 * S * T_A * T_R) / (T_Y10 * (T_Y10 + 1))), R_Y9). +/// S = Starting supply. +/// T_A = Anchor block time. +/// T_R = Remaining number of seconds until year 10. +/// T_Y10 = Number of seconds elapsed in 10 years. +/// R_Y9 = Reward at year 9. +fn anchor_block_reward_at_timestamp( + block_timestamp: i64, + genesis_timestamp: i64, + starting_supply: u64, + anchor_time: u16, +) -> u128 { + // A helper function to calculate the reward at a given block timestamp, without the year 9 baseline. + const fn block_reward_at_timestamp( + block_timestamp: i64, + genesis_timestamp: i64, + starting_supply: u64, + anchor_time: u16, + ) -> u128 { + // Calculate the timestamp at year 10. + let timestamp_at_year_10 = timestamp_at_year(genesis_timestamp, 10) as u128; + // Calculate the number of seconds elapsed in 10 years. + let number_of_seconds_in_10_years = timestamp_at_year(0, 10) as u128; + // Compute the remaining seconds until year 10. + let num_remaining_seconds_to_year_10 = timestamp_at_year_10.saturating_sub(block_timestamp as u128); + + // Compute the numerator. + // Note that we perform a `saturating_div(10)` on the `anchor_time` in the numerator and the `number_of_seconds_in_10_years` denominator. + // This is done to to match the truncation of `anchor_block_reward_at_height` in an attempt to + // keep the reward more consistent between the two functions. + let numerator = + 2 * starting_supply as u128 * anchor_time.saturating_div(10) as u128 * num_remaining_seconds_to_year_10; + // Compute the denominator. + let denominator = number_of_seconds_in_10_years * (number_of_seconds_in_10_years.saturating_div(10) + 1); + // Compute the quotient. + numerator / denominator + } + + // Calculate the timestamp at year 9. + let timestamp_at_year_9 = timestamp_at_year(genesis_timestamp, 9); + // Compute the unadjusted reward at year 9. + let reward_at_year_9 = + block_reward_at_timestamp(timestamp_at_year_9, genesis_timestamp, starting_supply, anchor_time); + // Compute the unadjusted reward at the given block timestamp. + let reward_at_block_timestamp = + block_reward_at_timestamp(block_timestamp, genesis_timestamp, starting_supply, anchor_time); + // Compute the anchor block reward. + reward_at_block_timestamp.max(reward_at_year_9) +} + +/// Returns the timestamp for a given year, relative to the genesis timestamp. +pub const fn timestamp_at_year(genesis_timestamp: i64, num_years: u32) -> i64 { + // Calculate the number of seconds in a year. + const SECONDS_IN_A_YEAR: u32 = 60 * 60 * 24 * 365; + // Calculate the number of seconds elapsed in `num_years`. + let seconds_elapsed = SECONDS_IN_A_YEAR.saturating_mul(num_years); + // Return the timestamp for the given year. + genesis_timestamp.saturating_add(seconds_elapsed as i64) +} + /// Returns the block height after a given number of years for a specific block time. pub const fn block_height_at_year(block_time: u16, num_years: u32) -> u32 { // Calculate the number of seconds in a year. @@ -362,7 +463,7 @@ mod tests { const EXPECTED_MAX_STAKING_REWARD: u64 = 142_694_063; #[test] - fn test_anchor_block_reward() { + fn test_anchor_block_reward_v1() { // Check the anchor block reward at block 1. let reward_at_block_1 = anchor_block_reward_at_height( 1, @@ -430,7 +531,75 @@ mod tests { } #[test] - fn test_total_anchor_block_reward() { + fn test_anchor_block_reward_v2() { + // Check the anchor block reward at block 1. + let reward_at_block_1 = anchor_block_reward_at_timestamp( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + assert_eq!(reward_at_block_1, EXPECTED_ANCHOR_BLOCK_REWARD_AT_BLOCK_1); + + // A helper function to check the the reward at the first expected block of a given year. + fn check_reward_at_year(year: u32, expected_reward: u128) { + let reward_at_year = anchor_block_reward_at_timestamp( + timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, year), + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + assert_eq!(reward_at_year, expected_reward); + } + + // Check the anchor block reward at the start of years 1 through 15. + check_reward_at_year(1, 171_232_871); + check_reward_at_year(2, 152_206_996); + check_reward_at_year(3, 133_181_122); + check_reward_at_year(4, 114_155_247); + check_reward_at_year(5, 95_129_372); + check_reward_at_year(6, 76_103_498); + check_reward_at_year(7, 57_077_623); + check_reward_at_year(8, 38_051_749); + check_reward_at_year(9, 19_025_874); + check_reward_at_year(10, 19_025_874); + check_reward_at_year(11, 19_025_874); + check_reward_at_year(12, 19_025_874); + check_reward_at_year(13, 19_025_874); + check_reward_at_year(14, 19_025_874); + check_reward_at_year(15, 19_025_874); + + // Calculate the timestamp at year 9. + let timestamp_at_year_9 = timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, 9); + + // Ensure that the reward is decreasing for blocks before year 9. + let mut previous_reward = reward_at_block_1; + let anchor_time = CurrentNetwork::ANCHOR_TIME as usize; + for timestamp in (CurrentNetwork::GENESIS_TIMESTAMP..timestamp_at_year_9).step_by(anchor_time).skip(1) { + let reward = anchor_block_reward_at_timestamp( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + assert!(reward < previous_reward, "Failed on timestamp {timestamp}"); + previous_reward = reward; + } + + // Ensure that the reward is 19_025_874 for blocks after year 9. + for timestamp in timestamp_at_year_9..(timestamp_at_year_9 + ITERATIONS as i64) { + let reward = anchor_block_reward_at_timestamp( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + assert_eq!(reward, 19_025_874); + } + } + + #[test] + fn test_total_anchor_block_reward_v1() { // A helper function used to add the anchor block reward for a given range of block heights. fn add_anchor_block_reward(total_reward: &mut u128, start_height: u32, end_height: u32) { for height in start_height..end_height { @@ -487,6 +656,68 @@ mod tests { check_sum_of_anchor_rewards(15, 3329999764466813); } + #[test] + fn test_total_anchor_block_reward_v2() { + // A helper function used to add the anchor block reward for a given range of block timestamps. + fn add_anchor_block_reward(total_reward: &mut u128, start_timestamp: i64, end_timestamp: i64) { + for timestamp in (start_timestamp..end_timestamp).step_by(CurrentNetwork::BLOCK_TIME as usize) { + *total_reward += anchor_block_reward_at_timestamp( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + } + } + + // Initialize the total reward. + let mut total_reward = 0; + + // A helper function to check the sum of all possible anchor rewards over a given year. + let mut check_sum_of_anchor_rewards = |year: u32, expected_reward: u128| { + assert!(year > 0, "Year must be greater than 0"); + let end_timestamp = timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, year); + let start_timestamp = std::cmp::max( + CurrentNetwork::GENESIS_TIMESTAMP, + timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, year - 1), + ); + add_anchor_block_reward(&mut total_reward, start_timestamp, end_timestamp); + // println!("year {year}, total_reward: {total_reward} expected_reward: {expected_reward}") + assert_eq!(total_reward, expected_reward); + }; + + // Check the sum of all anchor block rewards at block at year 1. + check_sum_of_anchor_rewards(1, 569999989861552); + // Check the sum of all anchor block rewards at block at year 2. + check_sum_of_anchor_rewards(2, 1079999981625694); + // Check the sum of all anchor block rewards at block at year 3. + check_sum_of_anchor_rewards(3, 1529999975292428); + // Check the sum of all anchor block rewards at block at year 4. + check_sum_of_anchor_rewards(4, 1919999970861747); + // Check the sum of all anchor block rewards at block at year 5. + check_sum_of_anchor_rewards(5, 2249999968333661); + // Check the sum of all anchor block rewards at block at year 6. + check_sum_of_anchor_rewards(6, 2519999967708149); + // Check the sum of all anchor block rewards at block at year 7. + check_sum_of_anchor_rewards(7, 2729999968985230); + // Check the sum of all anchor block rewards at block at year 8. + check_sum_of_anchor_rewards(8, 2879999972164900); + // Check the sum of all anchor block rewards at block at year 9. + check_sum_of_anchor_rewards(9, 2969999977247158); + // Check the sum of all anchor block rewards at block at year 10. + check_sum_of_anchor_rewards(10, 3029999973493558); + // Check the sum of all anchor block rewards at block at year 11. + check_sum_of_anchor_rewards(11, 3089999969739958); + // Check the sum of all anchor block rewards at block at year 12. + check_sum_of_anchor_rewards(12, 3149999965986358); + // Check the sum of all anchor block rewards at block at year 13. + check_sum_of_anchor_rewards(13, 3209999962232758); + // Check the sum of all anchor block rewards at block at year 14. + check_sum_of_anchor_rewards(14, 3269999958479158); + // Check the sum of all anchor block rewards at block at year 15. + check_sum_of_anchor_rewards(15, 3329999954725558); + } + #[test] fn test_block_reward() { let mut rng = TestRng::default(); @@ -626,7 +857,7 @@ mod tests { } #[test] - fn test_coinbase_reward() { + fn test_coinbase_reward_v1() { let coinbase_target: u64 = 10000; let combined_proof_target: u128 = coinbase_target as u128; @@ -709,7 +940,90 @@ mod tests { } #[test] - fn test_coinbase_reward_remaining_target() { + fn test_coinbase_reward_v2() { + let coinbase_target: u64 = 10000; + let combined_proof_target: u128 = coinbase_target as u128; + + let reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + combined_proof_target, + 0, + coinbase_target, + ) + .unwrap(); + assert_eq!(reward, EXPECTED_COINBASE_REWARD_AT_BLOCK_1); + + // Halving the combined proof target halves the reward. + let smaller_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + combined_proof_target / 2, + 0, + coinbase_target, + ) + .unwrap(); + assert_eq!(smaller_reward, reward / 2); + + // Halving the remaining coinbase target halves the reward. + let smaller_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + combined_proof_target, + coinbase_target / 2, + coinbase_target, + ) + .unwrap(); + assert_eq!(smaller_reward, reward / 2); + + // Dramatically increasing the combined proof target greater than the remaining coinbase target will not increase the reward. + let equivalent_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + u128::MAX, + 0, + coinbase_target, + ) + .unwrap(); + assert_eq!(reward, equivalent_reward); + + // Decreasing the combined proof target to 0 will result in a reward of 0. + let zero_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + 0, + 0, + coinbase_target, + ) + .unwrap(); + assert_eq!(zero_reward, 0); + + // Increasing the cumulative proof target beyond the coinbase target will result in a reward of 0. + let zero_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + 1, + coinbase_target + 1, + coinbase_target, + ) + .unwrap(); + assert_eq!(zero_reward, 0); + } + + #[test] + fn test_coinbase_reward_v1_remaining_target() { let mut rng = TestRng::default(); fn compute_coinbase_reward( @@ -767,7 +1081,65 @@ mod tests { } #[test] - fn test_coinbase_reward_up_to_year_10() { + fn test_coinbase_reward_v2_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_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + combined_proof_target as u128, + cumulative_proof_target, + coinbase_target, + ) + .unwrap() + } + + // Sample the starting conditions. + 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 { + // Check that as long as the sum of the combined proof target and cumulative proof target is less than the coinbase target, + // the reward remains the same. + // Intuition: Staying below the coinbase target preserves the reward for the combined proof target. + 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); + + // Check that increasing the cumulative proof target to devalue the combined proof target will decrease the reward. + // Intuition: Overflowing the coinbase target crowds out the combined proof target, leading to less reward for the combined proof target. + 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); + + // Check that increasing the combined proof target increases the reward. + // Intuition: If a prover contributes more proof target, they should be rewarded more. + 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_v1_up_to_year_10() { let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10); let mut block_height = 1; @@ -831,12 +1203,76 @@ mod tests { } #[test] - fn test_coinbase_reward_after_year_10() { + fn test_coinbase_reward_v2_up_to_year_10() { + let block_height_at_year_10 = timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, 10); + + let mut timestamp = CurrentNetwork::GENESIS_TIMESTAMP; + + let mut previous_reward = coinbase_reward_v2( + CurrentNetwork::GENESIS_TIMESTAMP + CurrentNetwork::BLOCK_TIME as i64, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + 1, + 0, + 1, + ) + .unwrap(); + + timestamp += CurrentNetwork::BLOCK_TIME as i64; + + 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 timestamp < block_height_at_year_10 { + let reward = coinbase_reward_v2( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + 1, + cumulative_proof_target, + coinbase_target, + ) + .unwrap(); + assert!(reward <= previous_reward); + + total_reward += reward; + previous_reward = reward; + timestamp += CurrentNetwork::BLOCK_TIME as i64; + + // Update the cumulative proof target. + 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 timestamp is {timestamp}"); + assert_eq!(timestamp, 1783331630, "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 timestamp is {timestamp}"); + assert_eq!(timestamp, 1858748810, "Update me if my parameters have changed"); + hit_1b = true; + } + } + + assert_eq!(total_reward, 1_515_000_074_780_540, "Update me if my parameters have changed"); + } + + #[test] + fn test_coinbase_reward_v1_after_year_10() { let mut rng = TestRng::default(); let block_height_at_year_10 = block_height_at_year(CurrentNetwork::BLOCK_TIME, 10); - // Check that the block at year 10 has a reward of 15. + // Check that the block at year 10 has a reward of 19. let reward = coinbase_reward_v1( block_height_at_year_10, CurrentNetwork::STARTING_SUPPLY, @@ -849,7 +1285,7 @@ mod tests { .unwrap(); assert_eq!(reward, 19_025_874); - // Check that the subsequent blocks have an anchor reward of 15 and reward less than or equal to 15. + // Check that the subsequent blocks have an anchor reward of 19 and reward less than or equal to 19. 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); @@ -878,6 +1314,54 @@ mod tests { } } + #[test] + fn test_coinbase_reward_v2_after_year_10() { + let mut rng = TestRng::default(); + + let timestamp_at_year_10 = timestamp_at_year(CurrentNetwork::GENESIS_TIMESTAMP, 10); + + // Check that the block at year 10 has a reward of 19. + let reward = coinbase_reward_v2( + timestamp_at_year_10, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + 1, + 0, + 1, + ) + .unwrap(); + assert_eq!(reward, 19_025_874); + + // Check that the subsequent blocks have an anchor reward of 19 and reward less than or equal to 19. + for _ in 0..ITERATIONS { + let timestamp: i64 = rng.gen_range(timestamp_at_year_10..timestamp_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 anchor_reward = anchor_block_reward_at_timestamp( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + ); + assert_eq!(anchor_reward, 19_025_874); + + let reward = coinbase_reward_v2( + timestamp, + CurrentNetwork::GENESIS_TIMESTAMP, + CurrentNetwork::STARTING_SUPPLY, + CurrentNetwork::ANCHOR_TIME, + combined_proof_target, + cumulative_proof_target, + coinbase_target, + ) + .unwrap(); + assert!(reward <= 19_025_874); + } + } + #[test] fn test_targets() { let mut rng = TestRng::default(); From a3c30235489c2306150cea83701df34b186754eb Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:34:17 -0400 Subject: [PATCH 6/9] Use new coinbase_reward impl --- ledger/block/src/helpers/target.rs | 129 +++++++++++++++++++++++++++++ ledger/block/src/verify.rs | 5 +- ledger/src/advance.rs | 5 +- 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index afbe682cf0..23bdaff014 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -91,6 +91,42 @@ pub const fn puzzle_reward(coinbase_reward: u64) -> u64 { coinbase_reward.saturating_mul(2).saturating_div(3) } +/// Calculate the coinbase reward based on the network’s consensus version, determined by the given block height. +pub fn coinbase_reward( + block_height: u32, + block_timestamp: i64, + genesis_timestamp: i64, + starting_supply: u64, + anchor_time: u16, + anchor_height: u32, + block_time: u16, + combined_proof_target: u128, + cumulative_proof_target: u64, + coinbase_target: u64, +) -> Result { + // Determine which coinbase reward version to use. + match block_height < N::CONSENSUS_V2_HEIGHT { + true => coinbase_reward_v1( + block_height, + starting_supply, + anchor_height, + block_time, + combined_proof_target, + cumulative_proof_target, + coinbase_target, + ), + false => coinbase_reward_v2( + block_timestamp, + genesis_timestamp, + starting_supply, + anchor_time, + combined_proof_target, + cumulative_proof_target, + coinbase_target, + ), + } +} + /// Calculates the V1 coinbase reward for a given block. /// R_coinbase = R_anchor(H) * min(P, C_R) / C /// R_anchor = Anchor reward at block height. @@ -856,6 +892,99 @@ mod tests { } } + #[test] + fn test_coinbase_reward() { + let mut rng = TestRng::default(); + + // Ensure that a block height of `TestnetV0::CONSENSUS_V2_HEIGHT` uses coinbase reward V2. + let block_timestamp = TestnetV0::GENESIS_TIMESTAMP + .saturating_add(TestnetV0::CONSENSUS_V2_HEIGHT.saturating_mul(TestnetV0::BLOCK_TIME as u32) as i64); + let reward = coinbase_reward::( + TestnetV0::CONSENSUS_V2_HEIGHT, + block_timestamp, + TestnetV0::GENESIS_TIMESTAMP, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_TIME, + TestnetV0::ANCHOR_HEIGHT, + TestnetV0::BLOCK_TIME, + 1, + 0, + 1, + ) + .unwrap(); + let expected_reward = coinbase_reward_v2( + block_timestamp, + TestnetV0::GENESIS_TIMESTAMP, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_TIME, + 1, + 0, + 1, + ) + .unwrap(); + assert_eq!(reward, expected_reward); + + for _ in 0..100 { + // Check that the block reward is correct for the first consensus version. + let consensus_v1_height = rng.gen_range(0..TestnetV0::CONSENSUS_V2_HEIGHT); + let block_timestamp = TestnetV0::GENESIS_TIMESTAMP + .saturating_add(consensus_v1_height.saturating_mul(TestnetV0::BLOCK_TIME as u32) as i64); + let consensus_v1_reward = coinbase_reward::( + consensus_v1_height, + block_timestamp, + TestnetV0::GENESIS_TIMESTAMP, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_TIME, + TestnetV0::ANCHOR_HEIGHT, + TestnetV0::BLOCK_TIME, + 1, + 0, + 1, + ) + .unwrap(); + let expected_reward = coinbase_reward_v1( + consensus_v1_height, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_HEIGHT, + TestnetV0::BLOCK_TIME, + 1, + 0, + 1, + ) + .unwrap(); + 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_V2_HEIGHT..u32::MAX); + 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::( + consensus_v2_height, + block_timestamp, + TestnetV0::GENESIS_TIMESTAMP, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_TIME, + TestnetV0::ANCHOR_HEIGHT, + TestnetV0::BLOCK_TIME, + 1, + 0, + 1, + ) + .unwrap(); + let expected_reward = coinbase_reward_v2( + block_timestamp, + TestnetV0::GENESIS_TIMESTAMP, + TestnetV0::STARTING_SUPPLY, + TestnetV0::ANCHOR_TIME, + 1, + 0, + 1, + ) + .unwrap(); + assert_eq!(consensus_v2_reward, expected_reward); + } + } + #[test] fn test_coinbase_reward_v1() { let coinbase_target: u64 = 10000; diff --git a/ledger/block/src/verify.rs b/ledger/block/src/verify.rs index 215232aaab..c4c2f15146 100644 --- a/ledger/block/src/verify.rs +++ b/ledger/block/src/verify.rs @@ -388,9 +388,12 @@ impl Block { )?; // Calculate the expected coinbase reward. - let expected_coinbase_reward = coinbase_reward_v1( + let expected_coinbase_reward = coinbase_reward::( height, + timestamp, + N::GENESIS_TIMESTAMP, N::STARTING_SUPPLY, + N::ANCHOR_TIME, N::ANCHOR_HEIGHT, N::BLOCK_TIME, combined_proof_target, diff --git a/ledger/src/advance.rs b/ledger/src/advance.rs index dd4f22cb98..0a51fb567f 100644 --- a/ledger/src/advance.rs +++ b/ledger/src/advance.rs @@ -295,9 +295,12 @@ impl> Ledger { )?; // Calculate the coinbase reward. - let coinbase_reward = coinbase_reward_v1( + let coinbase_reward = coinbase_reward::( next_height, + next_timestamp, + N::GENESIS_TIMESTAMP, N::STARTING_SUPPLY, + N::ANCHOR_TIME, N::ANCHOR_HEIGHT, N::BLOCK_TIME, combined_proof_target, From 9098995c53f6d01f0dc2171a6c51d72c8d1b5de7 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:25:41 -0500 Subject: [PATCH 7/9] nits --- ledger/block/src/helpers/target.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 23bdaff014..9dc368a92c 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -25,6 +25,9 @@ const V2_MAX_BLOCK_INTERVAL: i64 = 60; // 1 minute. /// prevent the block reward from becoming too small in the event of an extremely short block interval. const V2_MIN_BLOCK_INTERVAL: i64 = 1; // 1 second. +/// The number of seconds in a year with 365 days. Leap years are ignored for simplicity. +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. pub fn block_reward( block_height: u32, @@ -73,14 +76,12 @@ pub fn block_reward_v2( coinbase_reward: u64, transaction_fees: u64, ) -> u64 { - // Calculate the number of seconds in a year. - const SECONDS_IN_A_YEAR: u64 = 60 * 60 * 24 * 365; // Compute the annual reward: (0.05 * S). let annual_reward = total_supply / 20; // Compute the seconds since last block with a maximum of `V2_MAX_BLOCK_INTERVAL` seconds and minimum of `V2_MIN_BLOCK_INTERVAL` seconds; let time_since_last_block = time_since_last_block.clamp(V2_MIN_BLOCK_INTERVAL, V2_MAX_BLOCK_INTERVAL); // Compute the block reward: (0.05 * S) * min(max(I, MIN_BLOCK_INTERVAL), MAX_BLOCK_INTERVAL) / S_Y. - let block_reward = annual_reward * time_since_last_block as u64 / SECONDS_IN_A_YEAR; + let block_reward = annual_reward * time_since_last_block as u64 / SECONDS_IN_A_YEAR as u64; // Return the sum of the block reward, coinbase reward, and transaction fees. block_reward + (coinbase_reward / 3) + transaction_fees } @@ -255,7 +256,7 @@ fn anchor_block_reward_at_timestamp( // Calculate the timestamp at year 10. let timestamp_at_year_10 = timestamp_at_year(genesis_timestamp, 10) as u128; // Calculate the number of seconds elapsed in 10 years. - let number_of_seconds_in_10_years = timestamp_at_year(0, 10) as u128; + let number_of_seconds_in_10_years = (SECONDS_IN_A_YEAR as u128).saturating_mul(10); // Compute the remaining seconds until year 10. let num_remaining_seconds_to_year_10 = timestamp_at_year_10.saturating_sub(block_timestamp as u128); @@ -284,9 +285,8 @@ fn anchor_block_reward_at_timestamp( } /// Returns the timestamp for a given year, relative to the genesis timestamp. -pub const fn timestamp_at_year(genesis_timestamp: i64, num_years: u32) -> i64 { - // Calculate the number of seconds in a year. - const SECONDS_IN_A_YEAR: u32 = 60 * 60 * 24 * 365; +/// We assume a year is 365 days and ignore leap years for simplicity. +const fn timestamp_at_year(genesis_timestamp: i64, num_years: u32) -> i64 { // Calculate the number of seconds elapsed in `num_years`. let seconds_elapsed = SECONDS_IN_A_YEAR.saturating_mul(num_years); // Return the timestamp for the given year. @@ -294,9 +294,7 @@ pub const fn timestamp_at_year(genesis_timestamp: i64, num_years: u32) -> i64 { } /// Returns the block height after a given number of years for a specific block time. -pub const fn block_height_at_year(block_time: u16, num_years: u32) -> u32 { - // Calculate the number of seconds in a year. - const SECONDS_IN_A_YEAR: u32 = 60 * 60 * 24 * 365; +const fn block_height_at_year(block_time: u16, num_years: u32) -> u32 { // Calculate the one-year block height. let block_height_at_year_1 = SECONDS_IN_A_YEAR / block_time as u32; // Return the block height for the given number of years. From 47d80b09e61ccf1aef4b0258908e9e080ae63d1b Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:08:36 -0500 Subject: [PATCH 8/9] Update CONSENSUS_V2_HEIGHTS --- console/network/src/canary_v0.rs | 2 +- console/network/src/mainnet_v0.rs | 2 +- console/network/src/testnet_v0.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/console/network/src/canary_v0.rs b/console/network/src/canary_v0.rs index 5b115b7b71..5aae6930ef 100644 --- a/console/network/src/canary_v0.rs +++ b/console/network/src/canary_v0.rs @@ -135,7 +135,7 @@ impl Network for CanaryV0 { /// The block height from which consensus V2 rules apply. #[cfg(not(any(test, feature = "test")))] - const CONSENSUS_V2_HEIGHT: u32 = 2_500_000; + const CONSENSUS_V2_HEIGHT: u32 = 2_900_000; // TODO (raychu86): Update this value based on the desired canary height. /// The block height from which consensus V2 rules apply. #[cfg(any(test, feature = "test"))] diff --git a/console/network/src/mainnet_v0.rs b/console/network/src/mainnet_v0.rs index 76d3a51419..40e3013b09 100644 --- a/console/network/src/mainnet_v0.rs +++ b/console/network/src/mainnet_v0.rs @@ -136,7 +136,7 @@ impl Network for MainnetV0 { /// The block height from which consensus V2 rules apply. #[cfg(not(any(test, feature = "test")))] - const CONSENSUS_V2_HEIGHT: u32 = 2_000_000; + const CONSENSUS_V2_HEIGHT: u32 = 2_800_000; // TODO (raychu86): Update this value based on the desired mainnet height. /// The block height from which consensus V2 rules apply. #[cfg(any(test, feature = "test"))] diff --git a/console/network/src/testnet_v0.rs b/console/network/src/testnet_v0.rs index 63ef04e2a7..0e4e9d1cd7 100644 --- a/console/network/src/testnet_v0.rs +++ b/console/network/src/testnet_v0.rs @@ -135,7 +135,7 @@ impl Network for TestnetV0 { /// The block height from which consensus V2 rules apply. #[cfg(not(any(test, feature = "test")))] - const CONSENSUS_V2_HEIGHT: u32 = 2_500_000; + const CONSENSUS_V2_HEIGHT: u32 = 2_950_000; // TODO (raychu86): Update this value based on the desired testnet height. /// The block height from which consensus V2 rules apply. #[cfg(any(test, feature = "test"))] From 44740f09156914234dfc8a08d3b0d204b72c9248 Mon Sep 17 00:00:00 2001 From: raychu86 <14917648+raychu86@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:09:02 -0500 Subject: [PATCH 9/9] Update documentation --- ledger/block/src/helpers/target.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ledger/block/src/helpers/target.rs b/ledger/block/src/helpers/target.rs index 9dc368a92c..abff6b833e 100644 --- a/ledger/block/src/helpers/target.rs +++ b/ledger/block/src/helpers/target.rs @@ -87,6 +87,8 @@ pub fn block_reward_v2( } /// Calculate the puzzle reward, given the coinbase reward. +/// The puzzle reward is 2/3 of the total coinbase reward and paid out to the provers. The other 1/3 of +/// the coinbase reward is included in the block reward and paid out to stakers. pub const fn puzzle_reward(coinbase_reward: u64) -> u64 { // Return the coinbase reward multiplied by 2 and divided by 3. coinbase_reward.saturating_mul(2).saturating_div(3) @@ -200,6 +202,8 @@ pub fn coinbase_reward_v2( } /// Calculates the anchor block reward for the given block height. +/// The anchor block reward is upper bound of the coinbase reward for the given block before +/// calculating the final pro-rata coinbase reward based on the targets. /// R_anchor = max(floor((2 * S * H_A * H_R) / (H_Y10 * (H_Y10 + 1))), R_Y9). /// S = Starting supply. /// H_A = Anchor block height. @@ -232,6 +236,8 @@ fn anchor_block_reward_at_height(block_height: u32, starting_supply: u64, anchor } /// Calculates the anchor block reward for the given block timestamp. +/// The anchor block reward is upper bound of the coinbase reward for the given block before +/// calculating the final pro-rata coinbase reward based on the targets. /// This function uses timestamp rather than block height to determine the reward in order to combat /// the volatility of block times and better align with human timescales. /// R_anchor = max(floor((2 * S * T_A * T_R) / (T_Y10 * (T_Y10 + 1))), R_Y9).