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