Skip to main content

snarkvm_ledger/
check_next_block.rs

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