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