snarkvm_ledger/advance.rs
1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::*;
17
18use anyhow::Context;
19
20impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
21 /// Returns a candidate for the next block in the ledger, using a committed subdag and its transmissions.
22 /// This candidate can then be passed to [`Ledger::advance_to_next_block`] to be added to the ledger.
23 ///
24 /// # Panics
25 /// This function panics if called from an async context.
26 pub fn prepare_advance_to_next_quorum_block<R: Rng + CryptoRng>(
27 &self,
28 subdag: Subdag<N>,
29 transmissions: IndexMap<TransmissionID<N>, Transmission<N>>,
30 rng: &mut R,
31 ) -> Result<Block<N>> {
32 // Retrieve the latest block as the previous block (for the next block).
33 let previous_block = self.latest_block();
34
35 // Decouple the transmissions into ratifications, solutions, and transactions.
36 let (ratifications, solutions, transactions) = decouple_transmissions(transmissions.into_iter())?;
37 // Currently, we do not support ratifications from the memory pool.
38 ensure!(ratifications.is_empty(), "Ratifications are currently unsupported from the memory pool");
39 // Construct the block template.
40 let (header, ratifications, solutions, aborted_solution_ids, transactions, aborted_transaction_ids) =
41 self.construct_block_template(&previous_block, Some(&subdag), ratifications, solutions, transactions, rng)?;
42
43 // Construct the new quorum block.
44 Block::new_quorum(
45 previous_block.hash(),
46 header,
47 subdag,
48 ratifications,
49 solutions,
50 aborted_solution_ids,
51 transactions,
52 aborted_transaction_ids,
53 )
54 }
55
56 /// Returns a candidate for the next block in the ledger.
57 /// This candidate can then be passed to [`Ledger::advance_to_next_block`] to be added to the ledger.
58 ///
59 /// Note, that beacon blocks are only used for testing purposes.
60 /// Production code will most likely used `[Ledger::prepare_advance_to_next_quorum_block`] instead.
61 ///
62 /// # Panics
63 /// This function panics if called from an async context.
64 pub fn prepare_advance_to_next_beacon_block<R: Rng + CryptoRng>(
65 &self,
66 private_key: &PrivateKey<N>,
67 candidate_ratifications: Vec<Ratify<N>>,
68 candidate_solutions: Vec<Solution<N>>,
69 candidate_transactions: Vec<Transaction<N>>,
70 rng: &mut R,
71 ) -> Result<Block<N>> {
72 // Currently, we do not support ratifications from the memory pool.
73 ensure!(candidate_ratifications.is_empty(), "Ratifications are currently unsupported from the memory pool");
74
75 // Retrieve the latest block as the previous block (for the next block).
76 let previous_block = self.latest_block();
77
78 // Construct the block template.
79 let (header, ratifications, solutions, aborted_solution_ids, transactions, aborted_transaction_ids) = self
80 .construct_block_template(
81 &previous_block,
82 None,
83 candidate_ratifications,
84 candidate_solutions,
85 candidate_transactions,
86 rng,
87 )?;
88
89 // Construct the new beacon block.
90 Block::new_beacon(
91 private_key,
92 previous_block.hash(),
93 header,
94 ratifications,
95 solutions,
96 aborted_solution_ids,
97 transactions,
98 aborted_transaction_ids,
99 rng,
100 )
101 }
102
103 /// Adds the given block as the next block in the ledger.
104 ///
105 /// This function expects a valid block, that either was created by a trusted source, or successfully passed
106 /// the blocks checks (e.g. [`Ledger::check_next_block`]).
107 /// Note, that it is still possible that this function returns an error for a valid block, if there are concurrent tasks
108 /// updating the ledger.
109 ///
110 /// # Panics
111 /// This function panics if called from an async context.
112 pub fn advance_to_next_block(&self, block: &Block<N>) -> Result<()> {
113 // Acquire the write lock on the current block.
114 let mut current_block = self.current_block.write();
115 // Check again for any possible race conditions.
116 if current_block.is_genesis()? {
117 // current block is initialized as the genesis block, but the ledger will
118 // also advance to it on startup.
119 ensure!(
120 current_block.height() == block.height() || current_block.height() + 1 == block.height(),
121 "The given block is not the direct successor of the latest block"
122 );
123 } else {
124 ensure!(block.height() != 0, "Non-genesis blocks cannot have height 0");
125 ensure!(
126 current_block.height() + 1 == block.height(),
127 "The given block is not the direct successor of the latest block"
128 );
129 }
130 // Update the VM.
131 self.vm.add_next_block(block).with_context(|| "Failed to add block to VM")?;
132 // Update the current block.
133 *current_block = block.clone();
134 // Drop the write lock on the current block.
135 drop(current_block);
136
137 // Update the cached committee from storage.
138 if let Ok(current_committee) = self.vm.finalize_store().committee_store().current_committee() {
139 *self.current_committee.write() = Some(current_committee);
140 }
141
142 // If the block is the start of a new epoch, or the epoch hash has not been set,
143 // update the current epoch hash and clear the epoch prover cache.
144 if block.height() % N::NUM_BLOCKS_PER_EPOCH == 0 || self.current_epoch_hash.read().is_none() {
145 // Update and log the current epoch hash.
146 match self.get_epoch_hash(block.height()).ok() {
147 Some(epoch_hash) => {
148 trace!("Updating the current epoch hash at block {} to '{epoch_hash}'", block.height());
149 *self.current_epoch_hash.write() = Some(epoch_hash);
150 }
151 None => {
152 error!("Failed to update the current epoch hash at block {}", block.height());
153 }
154 }
155 // Clear the epoch provers cache. This is done because once the ledger enters a new epoch,
156 // all solutions from the previous epoch are no longer relevant.
157 // Note: solutions that land on exactly the epoch boundary are still considered part of the previous epoch,
158 // because they were created prior to the advancement to the new epoch (using the previous epoch's puzzle).
159 self.epoch_provers_cache.write().clear();
160 } else {
161 // If the block is not part of a new epoch, add the new provers to the epoch prover cache.
162 if let Some(solutions) = block.solutions().as_deref() {
163 let mut epoch_provers_cache = self.epoch_provers_cache.write();
164 for (_, s) in solutions.iter() {
165 let _ = *epoch_provers_cache.entry(s.address()).and_modify(|e| *e += 1).or_insert(1);
166 }
167 }
168 }
169
170 Ok(())
171 }
172}
173
174/// Splits candidate solutions into a collection of accepted ones and aborted ones.
175pub fn split_candidate_solutions<T, F>(
176 mut candidate_solutions: Vec<T>,
177 max_solutions: usize,
178 mut verification_fn: F,
179) -> (Vec<T>, Vec<T>)
180where
181 T: Sized + Copy,
182 F: FnMut(&mut T) -> bool,
183{
184 // Separate the candidate solutions into valid and aborted solutions.
185 let mut valid_candidate_solutions = Vec::with_capacity(max_solutions);
186 let mut aborted_candidate_solutions = Vec::new();
187 // Reverse the candidate solutions in order to be able to chunk them more efficiently.
188 candidate_solutions.reverse();
189 // Verify the candidate solutions in chunks. This is done so that we can potentially
190 // perform these operations in parallel while keeping the end result deterministic.
191 let chunk_size = 16;
192 while !candidate_solutions.is_empty() {
193 // Check if the collection of valid solutions is full.
194 if valid_candidate_solutions.len() >= max_solutions {
195 // If that's the case, mark the rest of the candidates as aborted.
196 aborted_candidate_solutions.extend(candidate_solutions.into_iter().rev());
197 break;
198 }
199
200 // Split off a chunk of the candidate solutions.
201 let mut candidates_chunk = if candidate_solutions.len() > chunk_size {
202 candidate_solutions.split_off(candidate_solutions.len() - chunk_size)
203 } else {
204 std::mem::take(&mut candidate_solutions)
205 };
206
207 // Verify the solutions in the chunk.
208 let verification_results = candidates_chunk.iter_mut().rev().map(|solution| {
209 let verified = verification_fn(solution);
210 (solution, verified)
211 });
212
213 // Process the results of the verification.
214 for (solution, is_valid) in verification_results.into_iter() {
215 if is_valid && valid_candidate_solutions.len() < max_solutions {
216 valid_candidate_solutions.push(*solution);
217 } else {
218 aborted_candidate_solutions.push(*solution);
219 }
220 }
221 }
222
223 // The `aborted_candidate_solutions` can contain both verified and unverified solutions.
224 // When `check_solution_mut` is used as `verification_fn`, these aborted solutions
225 // may include both mutated and un-mutated variants. This occurs because the verification
226 // check is skipped once the `max_solutions` limit is reached.
227 //
228 // This approach is SAFE because currently, only the `solutionID` of aborted solutions is stored.
229 // However, if full aborted solutions need to be stored in the future, this logic will need to be revisited.
230 (valid_candidate_solutions, aborted_candidate_solutions)
231}
232
233impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
234 /// Constructs a block template for the next block in the ledger.
235 ///
236 /// # Panics
237 /// This function panics if called from an async context.
238 #[allow(clippy::type_complexity)]
239 fn construct_block_template<R: Rng + CryptoRng>(
240 &self,
241 previous_block: &Block<N>,
242 subdag: Option<&Subdag<N>>,
243 candidate_ratifications: Vec<Ratify<N>>,
244 candidate_solutions: Vec<Solution<N>>,
245 candidate_transactions: Vec<Transaction<N>>,
246 rng: &mut R,
247 ) -> Result<(Header<N>, Ratifications<N>, Solutions<N>, Vec<SolutionID<N>>, Transactions<N>, Vec<N::TransactionID>)>
248 {
249 // Construct the solutions.
250 let (solutions, aborted_solutions, solutions_root, combined_proof_target) = match candidate_solutions.is_empty()
251 {
252 true => (None, vec![], Field::<N>::zero(), 0u128),
253 false => {
254 // Retrieve the latest epoch hash.
255 let latest_epoch_hash = self.latest_epoch_hash()?;
256 // Retrieve the latest proof target.
257 let latest_proof_target = self.latest_proof_target();
258 // Separate the candidate solutions into valid and aborted solutions.
259 let mut accepted_solutions: IndexMap<Address<N>, u64> = IndexMap::new();
260 let (valid_candidate_solutions, aborted_candidate_solutions) =
261 split_candidate_solutions(candidate_solutions, N::MAX_SOLUTIONS, |solution| {
262 let prover_address = solution.address();
263 let num_accepted_solutions = accepted_solutions.get(&prover_address).copied().unwrap_or(0);
264 // Check if the prover has reached their solution limit.
265 if self.is_solution_limit_reached(&prover_address, num_accepted_solutions) {
266 return false;
267 }
268 // Check if the solution is valid and update the number of accepted solutions.
269 match self.puzzle().check_solution_mut(solution, latest_epoch_hash, latest_proof_target) {
270 // Increment the number of accepted solutions for the prover.
271 Ok(()) => {
272 *accepted_solutions.entry(prover_address).or_insert(0) += 1;
273 true
274 }
275 // The solution is invalid, so we do not increment the number of accepted solutions.
276 Err(_) => false,
277 }
278 });
279
280 // Check if there are any valid solutions.
281 match valid_candidate_solutions.is_empty() {
282 true => (None, aborted_candidate_solutions, Field::<N>::zero(), 0u128),
283 false => {
284 // Construct the solutions.
285 let solutions = PuzzleSolutions::new(valid_candidate_solutions)?;
286 // Compute the solutions root.
287 let solutions_root = solutions.to_accumulator_point()?;
288 // Compute the combined proof target.
289 let combined_proof_target = self.puzzle().get_combined_proof_target(&solutions)?;
290 // Output the solutions, solutions root, and combined proof target.
291 (Some(solutions), aborted_candidate_solutions, solutions_root, combined_proof_target)
292 }
293 }
294 }
295 };
296 // Prepare the solutions.
297 let solutions = Solutions::from(solutions);
298
299 // Construct the aborted solution IDs.
300 let aborted_solution_ids = aborted_solutions.iter().map(Solution::id).collect::<Vec<_>>();
301
302 // Retrieve the latest state root.
303 let latest_state_root = self.latest_state_root();
304 // Retrieve the latest cumulative proof target.
305 let latest_cumulative_proof_target = previous_block.cumulative_proof_target();
306 // Retrieve the latest coinbase target.
307 let latest_coinbase_target = previous_block.coinbase_target();
308 // Retrieve the latest cumulative weight.
309 let latest_cumulative_weight = previous_block.cumulative_weight();
310 // Retrieve the last coinbase target.
311 let last_coinbase_target = previous_block.last_coinbase_target();
312 // Retrieve the last coinbase timestamp.
313 let last_coinbase_timestamp = previous_block.last_coinbase_timestamp();
314
315 // Compute the next round number.
316 let next_round = match subdag {
317 Some(subdag) => subdag.anchor_round(),
318 None => previous_block.round().saturating_add(1),
319 };
320 // Compute the next height.
321 let next_height = previous_block.height().saturating_add(1);
322 // Determine the timestamp for the next block.
323 let next_timestamp = match subdag {
324 Some(subdag) => {
325 // Retrieve the previous committee lookback.
326 let previous_committee_lookback = {
327 // Calculate the penultimate round, which is the round before the anchor round.
328 let penultimate_round = subdag.anchor_round().saturating_sub(1);
329 // Output the committee lookback for the penultimate round.
330 self.get_committee_lookback_for_round(penultimate_round)?
331 .ok_or(anyhow!("Failed to fetch committee lookback for round {penultimate_round}"))?
332 };
333 // Return the timestamp for the given committee lookback.
334 subdag.timestamp(&previous_committee_lookback)
335 }
336 None => OffsetDateTime::now_utc().unix_timestamp(),
337 };
338
339 // Calculate the next coinbase targets and timestamps.
340 let (
341 next_coinbase_target,
342 next_proof_target,
343 next_cumulative_proof_target,
344 next_cumulative_weight,
345 next_last_coinbase_target,
346 next_last_coinbase_timestamp,
347 ) = to_next_targets::<N>(
348 latest_cumulative_proof_target,
349 combined_proof_target,
350 latest_coinbase_target,
351 latest_cumulative_weight,
352 last_coinbase_target,
353 last_coinbase_timestamp,
354 next_timestamp,
355 )?;
356
357 // Calculate the coinbase reward.
358 let coinbase_reward = coinbase_reward::<N>(
359 next_height,
360 next_timestamp,
361 N::GENESIS_TIMESTAMP,
362 N::STARTING_SUPPLY,
363 N::ANCHOR_TIME,
364 N::ANCHOR_HEIGHT,
365 N::BLOCK_TIME,
366 combined_proof_target,
367 u64::try_from(latest_cumulative_proof_target)?,
368 latest_coinbase_target,
369 )?;
370
371 // Determine if the block timestamp should be included.
372 let next_block_timestamp =
373 (next_height >= N::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default()).then_some(next_timestamp);
374 // Construct the finalize state.
375 let state = FinalizeGlobalState::new::<N>(
376 next_round,
377 next_height,
378 next_block_timestamp,
379 next_cumulative_weight,
380 next_cumulative_proof_target,
381 previous_block.hash(),
382 )?;
383 // Speculate over the ratifications, solutions, and transactions.
384 let (ratifications, transactions, aborted_transaction_ids, ratified_finalize_operations) = self.vm.speculate(
385 state,
386 next_timestamp.saturating_sub(previous_block.timestamp()),
387 Some(coinbase_reward),
388 candidate_ratifications,
389 &solutions,
390 candidate_transactions.iter(),
391 rng,
392 )?;
393
394 // Compute the ratifications root.
395 let ratifications_root = ratifications.to_ratifications_root()?;
396
397 // Construct the subdag root.
398 let subdag_root = match subdag {
399 Some(subdag) => subdag.to_subdag_root()?,
400 None => Field::zero(),
401 };
402
403 // Construct the metadata.
404 let metadata = Metadata::new(
405 N::ID,
406 next_round,
407 next_height,
408 next_cumulative_weight,
409 next_cumulative_proof_target,
410 next_coinbase_target,
411 next_proof_target,
412 next_last_coinbase_target,
413 next_last_coinbase_timestamp,
414 next_timestamp,
415 )?;
416
417 // Construct the header.
418 let header = Header::from(
419 latest_state_root,
420 transactions.to_transactions_root()?,
421 transactions.to_finalize_root(ratified_finalize_operations)?,
422 ratifications_root,
423 solutions_root,
424 subdag_root,
425 metadata,
426 )?;
427
428 // Return the block template.
429 Ok((header, ratifications, solutions, aborted_solution_ids, transactions, aborted_transaction_ids))
430 }
431}