Skip to main content

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}