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}