diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 801778b137..18c70ba81f 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -169,7 +169,12 @@ pub trait Network: /// The expected time per block in seconds. const BLOCK_TIME: u16 = 10; /// The number of blocks per epoch. + #[cfg(not(feature = "test"))] const NUM_BLOCKS_PER_EPOCH: u32 = 3600 / Self::BLOCK_TIME as u32; // 360 blocks == ~1 hour + /// The number of blocks per epoch. + /// This is deliberately set to a low value for testing purposes only. + #[cfg(feature = "test")] + const NUM_BLOCKS_PER_EPOCH: u32 = 10; /// The maximum number of entries in data. const MAX_DATA_ENTRIES: usize = 32; diff --git a/ledger/src/advance.rs b/ledger/src/advance.rs index d9a980f6be..746c46edb8 100644 --- a/ledger/src/advance.rs +++ b/ledger/src/advance.rs @@ -152,7 +152,10 @@ impl> Ledger { error!("Failed to update the current epoch hash at block {}", block.height()); } } - // Clear the epoch provers cache. + // Clear the epoch provers cache. This is done because once the ledger enters a new epoch, + // all solutions from the previous epoch are no longer relevant. + // Note: solutions that land on exactly the epoch boundary are still considered part of the previous epoch, + // because they were created prior to the advancement to the new epoch (using the previous epoch's puzzle). self.epoch_provers_cache.write().clear(); } else { // If the block is not part of a new epoch, add the new provers to the epoch prover cache. diff --git a/ledger/src/lib.rs b/ledger/src/lib.rs index 9530d03f8c..e64bc923d7 100644 --- a/ledger/src/lib.rs +++ b/ledger/src/lib.rs @@ -263,12 +263,22 @@ impl> Ledger { /// Loads the provers and the number of solutions they have submitted for the current epoch. pub fn load_epoch_provers(&self) -> IndexMap, u32> { - // Fetch the block heights that belong to the current epoch. + // Fetch the current block height. let current_block_height = self.vm().block_store().current_block_height(); - let start_of_epoch = current_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH); - let existing_epoch_blocks: Vec<_> = (start_of_epoch..=current_block_height).collect(); + + // Determine the first block to start checking. + // Note that the epoch boundary (where current_block_height % N::NUM_BLOCKS_PER_EPOCH == 0) can contain solutions + // for the previous epoch X. The subsequent block is the first block to contain solutions for the current epoch X+1. + let next_block_height = current_block_height.saturating_add(1); + let start = next_block_height.saturating_sub(current_block_height % N::NUM_BLOCKS_PER_EPOCH); + + // If the epoch contains no blocks that have solutions for the epoch. + if start > current_block_height { + return IndexMap::new(); + } // Collect the addresses of the solutions submitted in the current epoch. + let existing_epoch_blocks: Vec<_> = (start..=current_block_height).collect(); let solution_addresses = cfg_iter!(existing_epoch_blocks) .flat_map(|height| match self.get_solutions(*height).as_deref() { Ok(Some(solutions)) => solutions.iter().map(|(_, s)| s.address()).collect::>(), diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 2129bbfbe4..35daa83a90 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -2648,12 +2648,12 @@ mod valid_solutions { #[test] fn test_cumulative_proof_target_correctness() { - // The number of blocks to test. - const NUM_BLOCKS: u32 = 20; - // Initialize an RNG. let rng = &mut TestRng::default(); + // The number of blocks to test. + let num_blocks = CurrentNetwork::NUM_BLOCKS_PER_EPOCH * 2; + // Initialize the test environment. let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); @@ -2674,8 +2674,8 @@ mod valid_solutions { // Track the total number of solutions in the current epoch. let mut total_epoch_solutions = 0; - // Run through 25 blocks of target adjustment. - while block_height < NUM_BLOCKS { + // Run through `num_blocks` blocks of target adjustment. + while block_height < num_blocks { // Get coinbase puzzle data from the latest block. let block = ledger.latest_block(); let coinbase_target = block.coinbase_target(); @@ -2713,23 +2713,9 @@ mod valid_solutions { combined_targets = 0; } - // Get a transfer transaction to ensure solutions can be included in the block. - let inputs = [Value::from_str(&format!("{prover_address}")).unwrap(), Value::from_str("10u64").unwrap()]; - let transfer_transaction = ledger - .vm - .execute(&private_key, ("credits.aleo", "transfer_public"), inputs.iter(), None, 0, None, rng) - .unwrap(); - // Generate the next prospective block. - let next_block = ledger - .prepare_advance_to_next_beacon_block( - &private_key, - vec![], - solutions, - vec![transfer_transaction.clone()], - rng, - ) - .unwrap(); + let next_block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], solutions, vec![], rng).unwrap(); // Ensure the combined target matches the expected value. assert_eq!(combined_targets as u128, next_block.cumulative_proof_target()); @@ -2743,23 +2729,139 @@ mod valid_solutions { // Set the latest block height. block_height = ledger.latest_height(); + // Update the epoch solutions count. + if block_height.is_multiple_of(CurrentNetwork::NUM_BLOCKS_PER_EPOCH) { + // Reset the epoch solutions count at the epoch boundary. + total_epoch_solutions = 0; + } else { + total_epoch_solutions += num_solutions; + } + + // Fetch the epoch provers cache. + let epoch_provers = ledger.epoch_provers_cache.read(); + // Load the epoch provers from the blocks in the current epoch. + let expected_epoch_provers = ledger.load_epoch_provers(); + // Check that the epoch solutions are correct + assert_eq!(epoch_provers.values().sum::(), u32::try_from(total_epoch_solutions).unwrap()); + assert_eq!(epoch_provers.len(), expected_epoch_provers.len()); + for ((expected_address, expected_count), (address, count)) in + expected_epoch_provers.iter().zip(epoch_provers.iter()) + { + assert_eq!(expected_address, address); + assert_eq!(expected_count, count); + } + } + } + + #[test] + fn test_epoch_provers_cache_cleared_at_epoch_boundary() { + // Initialize an RNG. + let rng = &mut TestRng::default(); + + // Initialize the test environment. + let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); + + // Set up the prover account with sufficient balance to generate solutions. + let prover_private_key = PrivateKey::::new(rng).unwrap(); + let prover_address = Address::try_from(&prover_private_key).unwrap(); + setup_prover_account(&ledger, &private_key, &prover_private_key, rng); + + // Retrieve the puzzle parameters. + let puzzle = ledger.puzzle(); + + // Initialize block height. + let mut block_height = ledger.latest_height(); + + // Start a local counter of proof targets. + let mut combined_targets = 0; + + // Track the total number of solutions in the current epoch. + let mut total_epoch_solutions = 0; + + // Run through blocks until the end of the epoch. + while block_height < CurrentNetwork::NUM_BLOCKS_PER_EPOCH { + // Check the epoch provers cache. + { + // Fetch the epoch provers cache. + let epoch_provers = ledger.epoch_provers_cache.read(); + // Load the epoch provers from the blocks in the current epoch. + let expected_epoch_provers = ledger.load_epoch_provers(); + // Check that the epoch solutions are correct + assert_eq!(epoch_provers.values().sum::(), u32::try_from(total_epoch_solutions).unwrap()); + assert_eq!(epoch_provers.len(), expected_epoch_provers.len()); + for ((expected_address, expected_count), (address, count)) in + expected_epoch_provers.iter().zip(epoch_provers.iter()) + { + assert_eq!(expected_address, address); + assert_eq!(expected_count, count); + assert_eq!(*expected_count, u32::try_from(total_epoch_solutions).unwrap()); + } + } + + // Get coinbase puzzle data from the latest block. + let block = ledger.latest_block(); + let coinbase_target = block.coinbase_target(); + let coinbase_threshold = coinbase_target.saturating_div(2); + let latest_epoch_hash = ledger.latest_epoch_hash().unwrap(); + let latest_proof_target = ledger.latest_proof_target(); + + // Sample the number of solutions to generate. + let num_solutions = rng.gen_range(1..=CurrentNetwork::MAX_SOLUTIONS); + + // Initialize a vector for valid solutions for this block. + let mut solutions = Vec::with_capacity(num_solutions); + + // Loop through proofs until two that meet the threshold are found. + loop { + if let Ok(solution) = + puzzle.prove(latest_epoch_hash, prover_address, rng.r#gen(), Some(latest_proof_target)) + { + // Get the proof target. + let proof_target = puzzle.get_proof_target(&solution).unwrap(); + + // Update the local combined target counter and store the solution. + combined_targets += proof_target; + solutions.push(solution); + + // If two have been found, exit the solver loop. + if solutions.len() >= num_solutions { + break; + } + } + } + + // If the combined target exceeds the coinbase threshold reset it. + if combined_targets >= coinbase_threshold { + combined_targets = 0; + } + + // Generate the next prospective block. + let next_block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], solutions, vec![], rng).unwrap(); + + // Ensure the next block is correct. + ledger.check_next_block(&next_block, rng).unwrap(); + + // Advanced to the next block. + ledger.advance_to_next_block(&next_block).unwrap(); + + // Set the latest block height. + block_height = ledger.latest_height(); + // Update the epoch solutions count. total_epoch_solutions += num_solutions; } + // Ensure that we are at the end of the epoch. + assert!(block_height.is_multiple_of(CurrentNetwork::NUM_BLOCKS_PER_EPOCH)); + // Fetch the epoch provers cache. let epoch_provers = ledger.epoch_provers_cache.read(); // Load the epoch provers from the blocks in the current epoch. let expected_epoch_provers = ledger.load_epoch_provers(); - // Check that the epoch solutions are correct - assert_eq!(epoch_provers.values().sum::(), u32::try_from(total_epoch_solutions).unwrap()); - assert_eq!(epoch_provers.len(), expected_epoch_provers.len()); - for ((expected_address, expected_count), (address, count)) in - expected_epoch_provers.iter().zip(epoch_provers.iter()) - { - assert_eq!(expected_address, address); - assert_eq!(expected_count, count); - } + // Check that the epoch solutions are both empty. + assert_eq!(epoch_provers.len(), 0); + assert_eq!(expected_epoch_provers.len(), 0); } #[test] @@ -3007,7 +3109,6 @@ mod valid_solutions { assert_eq!(block_aborted_solution_ids, invalid_solutions, "Invalid solutions do not match"); } - // TODO (raychu86): Fix this test #[test] fn test_excess_valid_solution_ids() { // Note that this should be greater than the maximum number of solutions.