Skip to main content

snarkvm_ledger/
check_next_block.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 crate::{narwhal::BatchHeader, puzzle::SolutionID};
19
20use anyhow::{Context, bail};
21
22/// Wrapper for a block that has a valid subDAG, but where the block header,
23/// solutions, and transmissions have not been verified yet.
24///
25/// This type is created by `Ledger::check_block_subdag` and consumed by `Ledger::check_block_content`.
26#[derive(Clone, PartialEq, Eq)]
27pub struct PendingBlock<N: Network>(Block<N>);
28
29impl<N: Network> Deref for PendingBlock<N> {
30    type Target = Block<N>;
31
32    fn deref(&self) -> &Block<N> {
33        &self.0
34    }
35}
36
37impl<N: Network> Debug for PendingBlock<N> {
38    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
39        write!(f, "PendingBlock {{ height: {}, hash: {} }}", self.height(), self.hash())
40    }
41}
42
43/// Error returned by [`Self::check_block_subdag`] and [`Self::check_block_subdag_inner`].
44///
45/// This allows parsing for begning errors, such as the block already existing in the ledger.
46#[derive(thiserror::Error, Debug)]
47pub enum CheckBlockError<N: Network> {
48    #[error("Block with hash {hash} already exists in the ledger")]
49    BlockAlreadyExists { hash: N::BlockHash },
50    #[error("Block has invalid height. Expected {expected}, but got {actual}")]
51    InvalidHeight { expected: u32, actual: u32 },
52    #[error("Block has invalid hash")]
53    InvalidHash,
54    /// An error related to the given prefix of pending blocks.
55    #[error("The prefix as an error at index {index} - {error:?}")]
56    InvalidPrefix { index: usize, error: Box<CheckBlockError<N>> },
57    #[error("The block contains solution '{solution_id}', but it already exists in the ledger")]
58    SolutionAlreadyExists { solution_id: SolutionID<N> },
59    #[error("Failed to speculate over unconfirmed transactions - {inner}")]
60    SpeculationFailed { inner: anyhow::Error },
61    #[error("Failed to verify block - {inner}")]
62    VerificationFailed { inner: anyhow::Error },
63    #[error("Prover '{prover_address}' has reached their solution limit for the current epoch")]
64    SolutionLimitReached { prover_address: Address<N> },
65    #[error("The previous block should contain solution '{solution_id}', but it does not exist in the ledger")]
66    PreviousSolutionNotFound { solution_id: SolutionID<N> },
67    #[error("The previous block should contain solution '{transaction_id}', but it does not exist in the ledger")]
68    PreviousTransactionNotFound { transaction_id: N::TransactionID },
69    #[error(transparent)]
70    Other(#[from] anyhow::Error),
71}
72
73impl<N: Network> CheckBlockError<N> {
74    pub fn into_anyhow(self) -> anyhow::Error {
75        match self {
76            Self::Other(err) => err,
77            _ => anyhow::anyhow!("{self:?}"),
78        }
79    }
80}
81
82impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
83    /// Checks that the subDAG in a given block is valid, but does not fully verify the block.
84    ///
85    /// # Arguments
86    /// * `block` - The block to check.
87    /// * `prefix` - A sequence of blocks between the block to check and the current height of the ledger.
88    ///
89    /// # Returns
90    /// * On success, a [`PendingBlock`] representing the block that was checked. Once the prefix of this block has been fully added to the ledger,
91    ///   the [`PendingBlock`] can then be passed to [`Self::check_block_content`] to fully verify it.
92    /// * On failure, a [`CheckBlockError`] describing the reason the block was rejected.
93    ///
94    /// # Notes
95    /// * This does *not* check that the header of the block is correct or execute/verify any of the transmissions contained within it.
96    /// * In most cases, you want to use [`Self::check_next_block`] instead to perform a full verification.
97    /// * This will reject any blocks with a height <= the current height and any blocks with a height >= the current height + GC.
98    ///   For the former, a valid block already exists and,for the latter, the comittee is still unknown.
99    /// * This function executes atomically, in that there is guaranteed to be no concurrent updates to the ledger during its execution.
100    ///   However there are no ordering guarantees *between* multiple invocations of this function, [`Self::check_block_content`] and [`Self::advance_to_next_block`].
101    pub fn check_block_subdag(
102        &self,
103        block: Block<N>,
104        prefix: &[PendingBlock<N>],
105    ) -> Result<PendingBlock<N>, CheckBlockError<N>> {
106        self.check_block_subdag_inner(&block, prefix)?;
107        Ok(PendingBlock(block))
108    }
109
110    fn check_block_subdag_inner(&self, block: &Block<N>, prefix: &[PendingBlock<N>]) -> Result<(), CheckBlockError<N>> {
111        // Grab a lock to the latest_block in the ledger, to prevent concurrent writes to the ledger,
112        // and to ensure that this check is atomic.
113        let latest_block = self.current_block.read();
114
115        // First check that the heights and hashes of the pending block sequence and of the new block are correct.
116        // The hash checks should be redundant, but we perform them out of extra caution.
117        let mut expected_height = latest_block.height() + 1;
118        for (index, prefix_block) in prefix.iter().enumerate() {
119            if prefix_block.height() != expected_height {
120                return Err(CheckBlockError::InvalidPrefix {
121                    index,
122                    error: Box::new(CheckBlockError::InvalidHeight {
123                        expected: expected_height,
124                        actual: prefix_block.height(),
125                    }),
126                });
127            }
128
129            if self.contains_block_hash(&prefix_block.hash())? {
130                return Err(CheckBlockError::InvalidPrefix {
131                    index,
132                    error: Box::new(CheckBlockError::BlockAlreadyExists { hash: prefix_block.hash() }),
133                });
134            }
135
136            expected_height += 1;
137        }
138
139        if self.contains_block_hash(&block.hash())? {
140            return Err(CheckBlockError::BlockAlreadyExists { hash: block.hash() });
141        }
142
143        if block.height() != expected_height {
144            return Err(CheckBlockError::InvalidHeight { expected: expected_height, actual: block.height() });
145        }
146
147        // Ensure the certificates in the block subdag have met quorum requirements.
148        self.check_block_subdag_quorum(block)?;
149
150        // Determine if the block subdag is correctly constructed and is not a combination of multiple subdags.
151        self.check_block_subdag_atomicity(block)?;
152
153        // Ensure that all leaves of the subdag point to valid batches in other subdags/blocks.
154        self.check_block_subdag_leaves(block, prefix)?;
155
156        Ok(())
157    }
158
159    /// Checks the given block is a valid next block with regard to the current state/height of the Ledger.
160    ///
161    /// # Panics
162    /// This function panics if called from an async context.
163    pub fn check_next_block<R: CryptoRng + Rng>(&self, block: &Block<N>, rng: &mut R) -> Result<()> {
164        self.check_block_subdag_inner(block, &[]).map_err(|err| err.into_anyhow())?;
165        self.check_block_content_inner(block, rng).map_err(|err| err.into_anyhow())?;
166
167        Ok(())
168    }
169
170    /// Takes a pending block and performs the remaining checks to full verify it.
171    ///
172    /// # Arguments
173    /// This takes a [`PendingBlock`] as input, which is the output of a successful call to [`Self::check_block_subdag`].
174    /// The latter already verified the block's DAG and certificate signatures.
175    ///
176    /// # Return Value
177    /// This returns a [`Block`] on success representing the fully verified block.
178    ///
179    /// # Notes
180    /// - This check can only succeed for pending blocks that are a direct successor of the latest block in the ledger.
181    /// - Execution of this function is atomic, and there is guaranteed to be no concurrent update to the ledger during its execution.
182    /// - Even though this function may return `Ok(block)`, advancing the ledger to this block may still fail, if there was an update to the ledger
183    ///   *between* calling `check_block_content` and `advance_to_next_block`.
184    ///   If your implementation requires atomicity across these two steps, you need to implement your own locking mechanism.
185    ///
186    /// # Panics
187    /// This function panics if called from an async context.
188    pub fn check_block_content<R: CryptoRng + Rng>(
189        &self,
190        block: PendingBlock<N>,
191        rng: &mut R,
192    ) -> Result<Block<N>, CheckBlockError<N>> {
193        self.check_block_content_inner(&block.0, rng)?;
194        Ok(block.0)
195    }
196
197    /// # Panics
198    /// This function panics if called from an async context.
199    fn check_block_content_inner<R: CryptoRng + Rng>(
200        &self,
201        block: &Block<N>,
202        rng: &mut R,
203    ) -> Result<(), CheckBlockError<N>> {
204        let latest_block = self.current_block.read();
205        let latest_block_timestamp = latest_block.timestamp();
206
207        // Ensure, again, that the ledger has not advanced yet. This prevents cryptic errors form appearing during the block check.
208        if block.height() != latest_block.height() + 1 {
209            return Err(CheckBlockError::InvalidHeight { expected: latest_block.height() + 1, actual: block.height() });
210        }
211
212        // Ensure the solutions do not already exist.
213        for solution_id in block.solutions().solution_ids() {
214            if self.contains_solution_id(solution_id)? {
215                return Err(CheckBlockError::SolutionAlreadyExists { solution_id: *solution_id });
216            }
217        }
218
219        // Determine if the block timestamp should be included.
220        let block_timestamp = (block.height() >= N::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default())
221            .then_some(block.timestamp());
222        // Construct the finalize state.
223        let state = FinalizeGlobalState::new::<N>(
224            block.round(),
225            block.height(),
226            block_timestamp,
227            block.cumulative_weight(),
228            block.cumulative_proof_target(),
229            block.previous_hash(),
230        )?;
231
232        // Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique.
233        let time_since_last_block = block.timestamp().saturating_sub(latest_block_timestamp);
234        let ratified_finalize_operations = self.vm.check_speculate(
235            state,
236            time_since_last_block,
237            block.ratifications(),
238            block.solutions(),
239            block.transactions(),
240            rng,
241        )?;
242
243        // Retrieve the committee lookback.
244        let committee_lookback = self
245            .get_committee_lookback_for_round(block.round())?
246            .ok_or(anyhow!("Failed to fetch committee lookback for round {}", block.round()))?;
247
248        // Retrieve the previous committee lookback.
249        let previous_committee_lookback = {
250            // Calculate the penultimate round, which is the round before the anchor round.
251            let penultimate_round = block.round().saturating_sub(1);
252            // Output the committee lookback for the penultimate round.
253            self.get_committee_lookback_for_round(penultimate_round)?
254                .ok_or(anyhow!("Failed to fetch committee lookback for round {penultimate_round}"))?
255        };
256
257        // Ensure the block is correct.
258        let (expected_existing_solution_ids, expected_existing_transaction_ids) = block
259            .verify(
260                &latest_block,
261                self.latest_state_root(),
262                &previous_committee_lookback,
263                &committee_lookback,
264                self.puzzle(),
265                self.latest_epoch_hash()?,
266                OffsetDateTime::now_utc().unix_timestamp(),
267                ratified_finalize_operations,
268            )
269            .map_err(|err| CheckBlockError::VerificationFailed { inner: err })?;
270
271        // Ensure that the provers are within their stake bounds.
272        if let Some(solutions) = block.solutions().deref() {
273            let mut accepted_solutions: IndexMap<Address<N>, u64> = IndexMap::new();
274            for solution in solutions.values() {
275                let prover_address = solution.address();
276                let num_accepted_solutions = *accepted_solutions.get(&prover_address).unwrap_or(&0);
277                // Check if the prover has reached their solution limit.
278                if self.is_solution_limit_reached_at_timestamp(
279                    &prover_address,
280                    num_accepted_solutions,
281                    latest_block_timestamp,
282                ) {
283                    return Err(CheckBlockError::SolutionLimitReached { prover_address });
284                }
285                // Track the already accepted solutions.
286                *accepted_solutions.entry(prover_address).or_insert(0) += 1;
287            }
288        }
289
290        // Ensure that each existing solution ID from the block exists in the ledger.
291        for existing_solution_id in expected_existing_solution_ids {
292            if !self.contains_solution_id(&existing_solution_id)? {
293                return Err(CheckBlockError::PreviousSolutionNotFound { solution_id: existing_solution_id });
294            }
295        }
296
297        // Ensure that each existing transaction ID from the block exists in the ledger.
298        for existing_transaction_id in expected_existing_transaction_ids {
299            if !self.contains_transaction_id(&existing_transaction_id)? {
300                return Err(CheckBlockError::PreviousTransactionNotFound { transaction_id: existing_transaction_id });
301            }
302        }
303
304        Ok(())
305    }
306
307    /// Check that leaves in the subdag point to batches in other blocks that are valid.
308    ///
309    //
310    /// # Arguments
311    /// * `block` - The block to check.
312    /// * `prefix` - A sequence of [`PendingBlock`]s between the block to check and the current height of the ledger.
313    ///
314    /// # Notes
315    /// This only checks that leaves point to valid batch in the previous round, and *not* hat the batches are signed correctly
316    /// or that the edges are valid, as those checks already happened when the node received the batch.
317    fn check_block_subdag_leaves(&self, block: &Block<N>, prefix: &[PendingBlock<N>]) -> Result<()> {
318        // Check if the block has a subdag.
319        let Authority::Quorum(subdag) = block.authority() else {
320            return Ok(());
321        };
322
323        let previous_certs: HashSet<_> = prefix
324            .iter()
325            .filter_map(|block| match block.authority() {
326                Authority::Quorum(subdag) => Some(subdag.certificate_ids()),
327                Authority::Beacon(_) => None,
328            })
329            .flatten()
330            .collect();
331
332        // Store the IDs of all certificates in this subDAG.
333        // This allows determining which edges point to other subDAGs/blocks.
334        let subdag_certs: HashSet<_> = subdag.certificate_ids().collect();
335
336        // Generate a set of all external certificates this subDAG references.
337        // If multiple certificates reference the same external certificate, the id and round number will be
338        // identical and the set will contain only one entry for the external certificate.
339        let leaf_edges: HashSet<_> = subdag
340            .certificates()
341            .flat_map(|cert| cert.previous_certificate_ids().iter().map(|prev_id| (cert.round() - 1, prev_id)))
342            .filter(|(_, prev_id)| !subdag_certs.contains(prev_id))
343            .collect();
344
345        cfg_iter!(leaf_edges).try_for_each(|(prev_round, prev_id)| {
346            if prev_round + (BatchHeader::<N>::MAX_GC_ROUNDS as u64) - 1 <= block.round() {
347                // If the previous round is at the end of GC, we cannot (and do not need to) verify the next batch.
348                // For this leaf we are at the maximum length of the DAG, so any following batches are not allowed
349                // to be part of the block and, thus, a malicious actor cannot remove them.
350                return Ok::<(), Error>(());
351            }
352
353            // Ensure that the certificate is associated with a previous block.
354            if !previous_certs.contains(prev_id) && !self.vm.block_store().contains_block_for_certificate(prev_id)? {
355                bail!(
356                    "Batch(es) in the block point(s) to a certificate {prev_id} in round {prev_round} that is not associated with a previous block"
357                )
358            }
359
360            Ok(())
361        })
362    }
363
364    /// Check that the certificates in the block subdag have met quorum requirements.
365    ///
366    /// Called by [`Self::check_block_subdag`]
367    fn check_block_subdag_quorum(&self, block: &Block<N>) -> Result<()> {
368        // Check if the block has a subdag.
369        let subdag = match block.authority() {
370            Authority::Quorum(subdag) => subdag,
371            _ => return Ok(()),
372        };
373
374        // Check that all certificates on each round have met quorum requirements.
375        cfg_iter!(subdag).try_for_each(|(round, certificates)| {
376            // Retrieve the committee lookback for the round.
377            let committee_lookback = self
378                .get_committee_lookback_for_round(*round)
379                .with_context(|| format!("Failed to get committee lookback for round {round}"))?
380                .ok_or_else(|| anyhow!("No committee lookback for round {round}"))?;
381
382            // Check that each certificate for this round has met quorum requirements.
383            // Note that we do not need to check the quorum requirement for the previous certificates
384            // because that is done during construction in `BatchCertificate::new`.
385            cfg_iter!(certificates).try_for_each(|certificate| {
386                // Collect the certificate signers.
387                let mut signers: HashSet<_> =
388                    certificate.signatures().map(|signature| signature.to_address()).collect();
389                // Append the certificate author.
390                signers.insert(certificate.author());
391
392                // Ensure that the signers of the certificate reach the quorum threshold.
393                ensure!(
394                    committee_lookback.is_quorum_threshold_reached(&signers),
395                    "Certificate '{}' for round {round} does not meet quorum requirements",
396                    certificate.id()
397                );
398
399                Ok::<_, Error>(())
400            })?;
401
402            Ok::<_, Error>(())
403        })?;
404
405        Ok(())
406    }
407
408    /// Checks that the block subdag can not be split into multiple valid subdags.
409    ///
410    /// Called by [`Self::check_block_subdag`]
411    fn check_block_subdag_atomicity(&self, block: &Block<N>) -> Result<()> {
412        // Returns `true` if there is a path from the previous certificate to the current certificate.
413        fn is_linked<N: Network>(
414            subdag: &Subdag<N>,
415            previous_certificate: &BatchCertificate<N>,
416            current_certificate: &BatchCertificate<N>,
417        ) -> Result<bool> {
418            // Initialize the list containing the traversal.
419            let mut traversal = vec![current_certificate];
420            // Iterate over the rounds from the current certificate to the previous certificate.
421            for round in (previous_certificate.round()..current_certificate.round()).rev() {
422                // Retrieve all of the certificates for this past round.
423                let certificates = subdag.get(&round).ok_or(anyhow!("No certificates found for round {round}"))?;
424                // Filter the certificates to only include those that are in the traversal.
425                traversal = certificates
426                    .into_iter()
427                    .filter(|p| traversal.iter().any(|c| c.previous_certificate_ids().contains(&p.id())))
428                    .collect();
429            }
430            Ok(traversal.contains(&previous_certificate))
431        }
432
433        // Check if the block has a subdag.
434        let subdag = match block.authority() {
435            Authority::Quorum(subdag) => subdag,
436            _ => return Ok(()),
437        };
438
439        // Iterate over the rounds to find possible leader certificates.
440        for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2)
441        {
442            // Retrieve the previous committee lookback.
443            let previous_committee_lookback = self
444                .get_committee_lookback_for_round(round)?
445                .ok_or_else(|| anyhow!("No committee lookback found for round {round}"))?;
446
447            // Compute the leader for the commit round.
448            let computed_leader = previous_committee_lookback
449                .get_leader(round)
450                .with_context(|| format!("Failed to compute leader for round {round}"))?;
451
452            // Retrieve the previous leader certificates.
453            let previous_certificate = match subdag.get(&round).and_then(|certificates| {
454                certificates.iter().find(|certificate| certificate.author() == computed_leader)
455            }) {
456                Some(cert) => cert,
457                None => continue,
458            };
459
460            // Determine if there is a path between the previous certificate and the subdag's leader certificate.
461            if is_linked(subdag, previous_certificate, subdag.leader_certificate())? {
462                bail!(
463                    "The previous certificate should not be linked to the current certificate in block {}",
464                    block.height()
465                );
466            }
467        }
468
469        Ok(())
470    }
471}