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;
19
20/// Wrapper for a block that has a valid subDAG, but where the block header,
21/// solutions, and transmissions have not been verified yet.
22///
23/// This type is created by `Ledger::check_block_subdag` and consumed by `Ledger::check_block_content`.
24#[derive(Clone, PartialEq, Eq)]
25pub struct PendingBlock<N: Network>(Block<N>);
26
27impl<N: Network> Deref for PendingBlock<N> {
28 type Target = Block<N>;
29
30 fn deref(&self) -> &Block<N> {
31 &self.0
32 }
33}
34
35impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
36 /// Checks that the subDAG in a given block is valid, but does not fully verify the block.
37 ///
38 /// # Arguments
39 /// * `block` - The block to check.
40 /// * `pending_block` - A sequence of blocks between the block to check and the current height of the ledger.
41 ///
42 /// # Notes
43 /// * This does *not* check that the header of the block is correct or execute/verify any of the transmissions contained within it.
44 ///
45 /// * In most cases, you want to use [`Self::check_next_block`] instead to perform a full verification.
46 ///
47 /// * This will reject any blocks with a height <= the current height and any blocks with a height >= the current height + GC.
48 /// For the former, a valid block already exists and,
49 /// for the latter, the comittte is still unknown.
50 pub fn check_block_subdag(&self, block: Block<N>, pending_blocks: &[PendingBlock<N>]) -> Result<PendingBlock<N>> {
51 self.check_block_subdag_inner(&block, pending_blocks)?;
52 Ok(PendingBlock(block))
53 }
54
55 fn check_block_subdag_inner(&self, block: &Block<N>, pending_blocks: &[PendingBlock<N>]) -> Result<()> {
56 let height = block.height();
57 let latest_block = self.latest_block();
58
59 // Ensure the block hash does not already exist.
60 if self.contains_block_hash(&block.hash())? {
61 bail!("Block hash '{}' already exists in the ledger", block.hash())
62 }
63
64 // First check that the heights of the pending block sequence and of the new block are correct.
65 {
66 let mut expected_height = latest_block.height() + 1;
67 for pending in pending_blocks {
68 if pending.height() != expected_height {
69 bail!(
70 "Pending block has invalid height. Expected {expected_height}, but got {actual}.",
71 actual = pending.height()
72 );
73 }
74 expected_height += 1;
75 }
76
77 if height != expected_height {
78 bail!("Block has invalid height. Expected {expected_height}, but got {height}.");
79 }
80 }
81 // Ensure the certificates in the block subdag have met quorum requirements.
82 self.check_block_subdag_quorum(block)?;
83
84 // Determine if the block subdag is correctly constructed and is not a combination of multiple subdags.
85 self.check_block_subdag_atomicity(block)?;
86
87 // Ensure that all leaves of the subdag point to valid batches in other subdags/blocks.
88 self.check_block_subdag_leaves(block, pending_blocks)?;
89
90 Ok(())
91 }
92
93 /// Checks the given block is a valid next block with regard to the current state/height of the Ledger.
94 pub fn check_next_block<R: CryptoRng + Rng>(&self, block: &Block<N>, rng: &mut R) -> Result<()> {
95 self.check_block_subdag_inner(block, &[])?;
96 self.check_block_content_inner(block, rng)?;
97
98 Ok(())
99 }
100
101 /// Takes a pending block and performs the remaining checks to full verify it.
102 ///
103 /// # Arguments
104 /// This takes a [`PendingBlock`] as input, which is the output of a successful call to [`Self::check_block_subdag`].
105 /// The latter already verified the block's DAG and certificate signatures.
106 ///
107 /// # Return Value
108 /// This returns a [`Block`] on success representing the fully verified block.
109 pub fn check_block_content<R: CryptoRng + Rng>(&self, block: PendingBlock<N>, rng: &mut R) -> Result<Block<N>> {
110 self.check_block_content_inner(&block.0, rng)?;
111 Ok(block.0)
112 }
113
114 fn check_block_content_inner<R: CryptoRng + Rng>(&self, block: &Block<N>, rng: &mut R) -> Result<()> {
115 let latest_block = self.latest_block();
116
117 // Ensure the solutions do not already exist.
118 for solution_id in block.solutions().solution_ids() {
119 if self.contains_solution_id(solution_id)? {
120 bail!("Solution ID {solution_id} already exists in the ledger");
121 }
122 }
123
124 // Retrieve the committee lookback.
125 let committee_lookback = self
126 .get_committee_lookback_for_round(block.round())?
127 .ok_or(anyhow!("Failed to fetch committee lookback for round {}", block.round()))?;
128
129 // Retrieve the previous committee lookback.
130 let previous_committee_lookback = {
131 // Calculate the penultimate round, which is the round before the anchor round.
132 let penultimate_round = block.round().saturating_sub(1);
133 // Output the committee lookback for the penultimate round.
134 self.get_committee_lookback_for_round(penultimate_round)?
135 .ok_or(anyhow!("Failed to fetch committee lookback for round {penultimate_round}"))?
136 };
137
138 // TODO (howardwu): Remove this after moving the total supply into credits.aleo.
139 {
140 // // Retrieve the latest total supply.
141 // let latest_total_supply = self.latest_total_supply_in_microcredits();
142 // // Retrieve the block reward from the first block ratification.
143 // let block_reward = match block.ratifications()[0] {
144 // Ratify::BlockReward(block_reward) => block_reward,
145 // _ => bail!("Block {height} is invalid - the first ratification must be a block reward"),
146 // };
147 // // Retrieve the puzzle reward from the second block ratification.
148 // let puzzle_reward = match block.ratifications()[1] {
149 // Ratify::PuzzleReward(puzzle_reward) => puzzle_reward,
150 // _ => bail!("Block {height} is invalid - the second ratification must be a puzzle reward"),
151 // };
152 // // Compute the next total supply in microcredits.
153 // let next_total_supply_in_microcredits =
154 // update_total_supply(latest_total_supply, block_reward, puzzle_reward, block.transactions())?;
155 // // Ensure the total supply in microcredits is correct.
156 // if next_total_supply_in_microcredits != block.total_supply_in_microcredits() {
157 // bail!("Invalid total supply in microcredits")
158 // }
159 }
160
161 // Construct the finalize state.
162 let state = FinalizeGlobalState::new::<N>(
163 block.round(),
164 block.height(),
165 block.cumulative_weight(),
166 block.cumulative_proof_target(),
167 block.previous_hash(),
168 )?;
169
170 // Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique.
171 let time_since_last_block = block.timestamp().saturating_sub(self.latest_timestamp());
172 let ratified_finalize_operations = self.vm.check_speculate(
173 state,
174 time_since_last_block,
175 block.ratifications(),
176 block.solutions(),
177 block.transactions(),
178 rng,
179 )?;
180
181 // Ensure the block is correct.
182 let (expected_existing_solution_ids, expected_existing_transaction_ids) = block.verify(
183 &latest_block,
184 self.latest_state_root(),
185 &previous_committee_lookback,
186 &committee_lookback,
187 self.puzzle(),
188 self.latest_epoch_hash()?,
189 OffsetDateTime::now_utc().unix_timestamp(),
190 ratified_finalize_operations,
191 )?;
192
193 // Ensure that the provers are within their stake bounds.
194 if let Some(solutions) = block.solutions().deref() {
195 let mut accepted_solutions: IndexMap<Address<N>, u64> = IndexMap::new();
196 for solution in solutions.values() {
197 let prover_address = solution.address();
198 let num_accepted_solutions = *accepted_solutions.get(&prover_address).unwrap_or(&0);
199 // Check if the prover has reached their solution limit.
200 if self.is_solution_limit_reached(&prover_address, num_accepted_solutions) {
201 bail!("Prover '{prover_address}' has reached their solution limit for the current epoch");
202 }
203 // Track the already accepted solutions.
204 *accepted_solutions.entry(prover_address).or_insert(0) += 1;
205 }
206 }
207
208 // Ensure that each existing solution ID from the block exists in the ledger.
209 for existing_solution_id in expected_existing_solution_ids {
210 if !self.contains_solution_id(&existing_solution_id)? {
211 bail!("Solution ID '{existing_solution_id}' does not exist in the ledger");
212 }
213 }
214
215 // Ensure that each existing transaction ID from the block exists in the ledger.
216 for existing_transaction_id in expected_existing_transaction_ids {
217 if !self.contains_transaction_id(&existing_transaction_id)? {
218 bail!("Transaction ID '{existing_transaction_id}' does not exist in the ledger");
219 }
220 }
221
222 Ok(())
223 }
224
225 /// Check that leaves in the subdag point to batches in other blocks that are valid.
226 ///
227 /// This does not verify that the batches are signed correctly or that the edges are valid
228 /// (only point to the previous round), as those checks already happened when the node received the batch.
229 fn check_block_subdag_leaves(&self, block: &Block<N>, previous_blocks: &[PendingBlock<N>]) -> Result<()> {
230 // Check if the block has a subdag.
231 let Authority::Quorum(subdag) = block.authority() else {
232 return Ok(());
233 };
234
235 let previous_certs: HashSet<_> = previous_blocks
236 .iter()
237 .filter_map(|block| match block.authority() {
238 Authority::Quorum(subdag) => Some(subdag.certificate_ids()),
239 Authority::Beacon(_) => None,
240 })
241 .flatten()
242 .collect();
243
244 // Store the IDs of all certificates in this subDAG.
245 // This allows determining which edges point to other subDAGs/blocks.
246 let subdag_certs: HashSet<_> = subdag.certificate_ids().collect();
247
248 // Generate a set of all external certificates this subDAG references.
249 // If multiple certificates reference the same external certificate, the id and round number will be
250 // identical and the set will contain only one entry for the external certificate.
251 let leaf_edges: HashSet<_> = subdag
252 .certificates()
253 .flat_map(|cert| cert.previous_certificate_ids().iter().map(|prev_id| (cert.round() - 1, prev_id)))
254 .filter(|(_, prev_id)| !subdag_certs.contains(prev_id))
255 .collect();
256
257 cfg_iter!(leaf_edges).try_for_each(|(prev_round, prev_id)| {
258 if prev_round + (BatchHeader::<N>::MAX_GC_ROUNDS as u64) - 1 <= block.round() {
259 // If the previous round is at the end of GC, we cannot (and do not need to) verify the next batch.
260 // For this leaf we are at the maximum length of the DAG, so any following batches are not allowed
261 // to be part of the block and, thus, a malicious actor cannot remove them.
262 return Ok::<(), Error>(());
263 }
264
265 // Ensure that the certificate is associated with a previous block.
266 if !previous_certs.contains(prev_id) && !self.vm.block_store().contains_block_for_certificate(prev_id)? {
267 bail!(
268 "Batch(es) in the block point(s) to a certificate {prev_id} in round {prev_round} that is not associated with a previous block"
269 )
270 }
271
272 Ok(())
273 })
274 }
275
276 /// Check that the certificates in the block subdag have met quorum requirements.
277 ///
278 /// Called by [`Self::check_block_subdag`]
279 fn check_block_subdag_quorum(&self, block: &Block<N>) -> Result<()> {
280 // Check if the block has a subdag.
281 let subdag = match block.authority() {
282 Authority::Quorum(subdag) => subdag,
283 _ => return Ok(()),
284 };
285
286 // Check that all certificates on each round have met quorum requirements.
287 cfg_iter!(subdag).try_for_each(|(round, certificates)| {
288 // Retrieve the committee lookback for the round.
289 let committee_lookback = self
290 .get_committee_lookback_for_round(*round)?
291 .ok_or_else(|| anyhow!("No committee lookback found for round {round}"))?;
292
293 // Check that each certificate for this round has met quorum requirements.
294 // Note that we do not need to check the quorum requirement for the previous certificates
295 // because that is done during construction in `BatchCertificate::new`.
296 cfg_iter!(certificates).try_for_each(|certificate| {
297 // Collect the certificate signers.
298 let mut signers: HashSet<_> =
299 certificate.signatures().map(|signature| signature.to_address()).collect();
300 // Append the certificate author.
301 signers.insert(certificate.author());
302
303 // Ensure that the signers of the certificate reach the quorum threshold.
304 ensure!(
305 committee_lookback.is_quorum_threshold_reached(&signers),
306 "Certificate '{}' for round {round} does not meet quorum requirements",
307 certificate.id()
308 );
309
310 Ok::<_, Error>(())
311 })?;
312
313 Ok::<_, Error>(())
314 })?;
315
316 Ok(())
317 }
318
319 /// Checks that the block subdag can not be split into multiple valid subdags.
320 ///
321 /// Called by [`Self::check_block_subdag`]
322 fn check_block_subdag_atomicity(&self, block: &Block<N>) -> Result<()> {
323 // Returns `true` if there is a path from the previous certificate to the current certificate.
324 fn is_linked<N: Network>(
325 subdag: &Subdag<N>,
326 previous_certificate: &BatchCertificate<N>,
327 current_certificate: &BatchCertificate<N>,
328 ) -> Result<bool> {
329 // Initialize the list containing the traversal.
330 let mut traversal = vec![current_certificate];
331 // Iterate over the rounds from the current certificate to the previous certificate.
332 for round in (previous_certificate.round()..current_certificate.round()).rev() {
333 // Retrieve all of the certificates for this past round.
334 let certificates = subdag.get(&round).ok_or(anyhow!("No certificates found for round {round}"))?;
335 // Filter the certificates to only include those that are in the traversal.
336 traversal = certificates
337 .into_iter()
338 .filter(|p| traversal.iter().any(|c| c.previous_certificate_ids().contains(&p.id())))
339 .collect();
340 }
341 Ok(traversal.contains(&previous_certificate))
342 }
343
344 // Check if the block has a subdag.
345 let subdag = match block.authority() {
346 Authority::Quorum(subdag) => subdag,
347 _ => return Ok(()),
348 };
349
350 // Iterate over the rounds to find possible leader certificates.
351 for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2)
352 {
353 // Retrieve the previous committee lookback.
354 let previous_committee_lookback = self
355 .get_committee_lookback_for_round(round)?
356 .ok_or_else(|| anyhow!("No committee lookback found for round {round}"))?;
357
358 // Compute the leader for the commit round.
359 let computed_leader = previous_committee_lookback
360 .get_leader(round)
361 .map_err(|e| anyhow!("Failed to compute leader for round {round}: {e}"))?;
362
363 // Retrieve the previous leader certificates.
364 let previous_certificate = match subdag.get(&round).and_then(|certificates| {
365 certificates.iter().find(|certificate| certificate.author() == computed_leader)
366 }) {
367 Some(cert) => cert,
368 None => continue,
369 };
370
371 // Determine if there is a path between the previous certificate and the subdag's leader certificate.
372 if is_linked(subdag, previous_certificate, subdag.leader_certificate())? {
373 bail!(
374 "The previous certificate should not be linked to the current certificate in block {}",
375 block.height()
376 );
377 }
378 }
379
380 Ok(())
381 }
382}