Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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(any(test, feature = "test")))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be using the test feature or the test_consensus_heights feature? I am still hoping we can phase out the former, as its name is not very descriptive.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want this to be for tests only rather than test_consensus_heights, because test_consensus_heights can be used for devnets.

I've kept the feature flag consistent with the other network variables, so if we would like to cleanup, we should do in a subsequent PR.

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 (8) for testing purposes only.
Comment thread
raychu86 marked this conversation as resolved.
Outdated
#[cfg(any(test, 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
61 changes: 61 additions & 0 deletions ledger/block/src/helpers/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,67 @@ 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
);

assert_eq!(block_height, N::MAX_SUPPLY_LIMIT_HEIGHT);
}

#[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
31 changes: 18 additions & 13 deletions ledger/src/advance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,18 +344,23 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
)?;

// Calculate the coinbase reward.
let coinbase_reward = coinbase_reward::<N>(
next_height,
next_timestamp,
N::GENESIS_TIMESTAMP,
N::STARTING_SUPPLY,
N::ANCHOR_TIME,
N::ANCHOR_HEIGHT,
N::BLOCK_TIME,
combined_proof_target,
u64::try_from(latest_cumulative_proof_target)?,
latest_coinbase_target,
)?;
let coinbase_reward = match next_height >= N::MAX_SUPPLY_LIMIT_HEIGHT {
// A `None` value indicates that no coinbase reward and no block reward should be given.
true => None,
// Otherwise, compute the coinbase reward as usual.
false => Some(coinbase_reward::<N>(
next_height,
next_timestamp,
N::GENESIS_TIMESTAMP,
N::STARTING_SUPPLY,
N::ANCHOR_TIME,
N::ANCHOR_HEIGHT,
N::BLOCK_TIME,
combined_proof_target,
u64::try_from(latest_cumulative_proof_target)?,
latest_coinbase_target,
)?),
};

// Determine if the block timestamp should be included.
let next_block_timestamp =
Expand All @@ -373,7 +378,7 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = self.vm.speculate(
state,
next_timestamp.saturating_sub(previous_block.timestamp()),
Some(coinbase_reward),
coinbase_reward,
candidate_ratifications,
&solutions,
candidate_transactions.iter(),
Expand Down
12 changes: 12 additions & 0 deletions ledger/src/check_next_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
)
.with_context(|| "Failed to speculate over unconfirmed transactions")?;

// Ensure that a block at or beyond the max supply limit height does not contain `BlockReward` or `PuzzleReward` ratifications.
if block.height() >= N::MAX_SUPPLY_LIMIT_HEIGHT {
ensure!(
!block
.ratifications()
.iter()
.any(|ratification| matches!(ratification, Ratify::BlockReward(..) | Ratify::PuzzleReward(..))),
"Blocks at or beyond height {} cannot contain `BlockReward` or `CoinbaseReward` ratifications",
N::MAX_SUPPLY_LIMIT_HEIGHT
);
}

// Retrieve the committee lookback.
let committee_lookback = self
.get_committee_lookback_for_round(block.round())?
Expand Down
61 changes: 61 additions & 0 deletions ledger/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2515,6 +2515,67 @@ function foo:
}
}

// #[cfg(feature = "test")]
#[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 there are no rewards in the block.
assert!(next_block.ratifications().is_empty());

// 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 there are no rewards in the block.
assert!(next_block.ratifications().is_empty());

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

// These tests require the proof targets to be low enough to be able to generate **valid** solutions.
// This requires the 'test' feature to be enabled for the `console` dependency.
#[cfg(feature = "test")]
Expand Down