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
20use anyhow::{Context, bail};
21
22impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
23    /// Checks the given block is valid next block.
24    pub fn check_next_block<R: CryptoRng + Rng>(&self, block: &Block<N>, rng: &mut R) -> Result<()> {
25        let height = block.height();
26        let latest_block = self.latest_block();
27
28        // Check that this is actually the next block.
29        if height != latest_block.height() + 1 {
30            bail!("Block height is {height}, but expected {}", latest_block.height() + 1);
31        }
32
33        // Ensure the block hash does not already exist.
34        if self.contains_block_hash(&block.hash())? {
35            bail!("Block hash '{}' already exists in the ledger", block.hash())
36        }
37
38        // Ensure the solutions do not already exist.
39        for solution_id in block.solutions().solution_ids() {
40            if self.contains_solution_id(solution_id)? {
41                bail!("Solution ID {solution_id} already exists in the ledger");
42            }
43        }
44
45        // Determine if the block timestamp should be included.
46        let block_timestamp = (block.height() >= N::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default())
47            .then_some(block.timestamp());
48        // Construct the finalize state.
49        let state = FinalizeGlobalState::new::<N>(
50            block.round(),
51            block.height(),
52            block_timestamp,
53            block.cumulative_weight(),
54            block.cumulative_proof_target(),
55            block.previous_hash(),
56        )?;
57
58        // Ensure speculation over the unconfirmed transactions is correct and ensure each transaction is well-formed and unique.
59        let time_since_last_block = block.timestamp().saturating_sub(self.latest_timestamp());
60        let ratified_finalize_operations = self
61            .vm
62            .check_speculate(
63                state,
64                time_since_last_block,
65                block.ratifications(),
66                block.solutions(),
67                block.transactions(),
68                rng,
69            )
70            .with_context(|| "Failed to speculate over unconfirmed transactions")?;
71
72        // Retrieve the committee lookback.
73        let committee_lookback = self
74            .get_committee_lookback_for_round(block.round())?
75            .ok_or(anyhow!("Failed to fetch committee lookback for round {}", block.round()))?;
76
77        // Retrieve the previous committee lookback.
78        let previous_committee_lookback = {
79            // Calculate the penultimate round, which is the round before the anchor round.
80            let penultimate_round = block.round().saturating_sub(1);
81            // Output the committee lookback for the penultimate round.
82            self.get_committee_lookback_for_round(penultimate_round)?
83                .ok_or(anyhow!("Failed to fetch committee lookback for round {penultimate_round}"))?
84        };
85
86        // Ensure the block is correct.
87        let (expected_existing_solution_ids, expected_existing_transaction_ids) = block
88            .verify(
89                &latest_block,
90                self.latest_state_root(),
91                &previous_committee_lookback,
92                &committee_lookback,
93                self.puzzle(),
94                self.latest_epoch_hash()?,
95                OffsetDateTime::now_utc().unix_timestamp(),
96                ratified_finalize_operations,
97            )
98            .with_context(|| "Failed to verify block")?;
99
100        // Ensure that the provers are within their stake bounds.
101        if let Some(solutions) = block.solutions().deref() {
102            let mut accepted_solutions: IndexMap<Address<N>, u64> = IndexMap::new();
103            for solution in solutions.values() {
104                let prover_address = solution.address();
105                let num_accepted_solutions = *accepted_solutions.get(&prover_address).unwrap_or(&0);
106                // Check if the prover has reached their solution limit.
107                if self.is_solution_limit_reached(&prover_address, num_accepted_solutions) {
108                    bail!("Prover '{prover_address}' has reached their solution limit for the current epoch");
109                }
110                // Track the already accepted solutions.
111                *accepted_solutions.entry(prover_address).or_insert(0) += 1;
112            }
113        }
114
115        // Ensure the certificates in the block subdag have met quorum requirements.
116        self.check_block_subdag_quorum(block)?;
117
118        // Determine if the block subdag is correctly constructed and is not a combination of multiple subdags.
119        self.check_block_subdag_atomicity(block)?;
120
121        // Ensure that all leaves of the subdag point to valid batches in other subdags/blocks.
122        self.check_block_subdag_leaves(block)?;
123
124        // Ensure that each existing solution ID from the block exists in the ledger.
125        for existing_solution_id in expected_existing_solution_ids {
126            if !self.contains_solution_id(&existing_solution_id)? {
127                bail!("Solution ID '{existing_solution_id}' does not exist in the ledger");
128            }
129        }
130
131        // Ensure that each existing transaction ID from the block exists in the ledger.
132        for existing_transaction_id in expected_existing_transaction_ids {
133            if !self.contains_transaction_id(&existing_transaction_id)? {
134                bail!("Transaction ID '{existing_transaction_id}' does not exist in the ledger");
135            }
136        }
137
138        Ok(())
139    }
140
141    /// Check that leaves in the subdag point to batches in other blocks that are valid.
142    ///
143    /// This does not verify that the batches are signed correctly or that the edges are valid
144    /// (only point to the previous round), as those checks already happened when the node received the batch.
145    fn check_block_subdag_leaves(&self, block: &Block<N>) -> Result<()> {
146        // Check if the block has a subdag.
147        let Authority::Quorum(subdag) = block.authority() else {
148            return Ok(());
149        };
150
151        // Store the IDs of all certificates in this subDAG.
152        // This allows determining which edges point to other subDAGs/blocks.
153        let subdag_certs: HashSet<_> = subdag.certificate_ids().collect();
154
155        // Generate a set of all external certificates this subDAG references.
156        // If multiple certificates reference the same external certificate, the id and round number will be
157        // identical and the set will contain only one entry for the external certificate.
158        let leaf_edges: HashSet<_> = subdag
159            .certificates()
160            .flat_map(|cert| cert.previous_certificate_ids().iter().map(|prev_id| (cert.round() - 1, prev_id)))
161            .filter(|(_, prev_id)| !subdag_certs.contains(prev_id))
162            .collect();
163
164        cfg_iter!(leaf_edges).try_for_each(|(prev_round, prev_id)| {
165            if prev_round + (BatchHeader::<N>::MAX_GC_ROUNDS as u64) - 1 <= block.round() {
166                // If the previous round is at the end of GC, we cannot (and do not need to) verify the next batch.
167                // For this leaf we are at the maximum length of the DAG, so any following batches are not allowed
168                // to be part of the block and, thus, a malicious actor cannot remove them.
169                return Ok::<(), Error>(());
170            }
171
172            // Ensure that the certificate is associated with a previous block.
173            if !self.vm.block_store().contains_block_for_certificate(prev_id)? {
174                bail!(
175                    "Batch(es) in the block point(s) to a certificate {prev_id} in round {prev_round} that is not associated with a previous block"
176                )
177            }
178
179            Ok(())
180        })
181    }
182
183    /// Check that the certificates in the block subdag have met quorum requirements.
184    fn check_block_subdag_quorum(&self, block: &Block<N>) -> Result<()> {
185        // Check if the block has a subdag.
186        let subdag = match block.authority() {
187            Authority::Quorum(subdag) => subdag,
188            _ => return Ok(()),
189        };
190
191        // Check that all certificates on each round have met quorum requirements.
192        cfg_iter!(subdag).try_for_each(|(round, certificates)| {
193            // Retrieve the committee lookback for the round.
194            let committee_lookback = self
195                .get_committee_lookback_for_round(*round)
196                .with_context(|| format!("Failed to get committee lookback for round {round}"))?
197                .ok_or_else(|| anyhow!("No committee lookback for round {round}"))?;
198
199            // Check that each certificate for this round has met quorum requirements.
200            // Note that we do not need to check the quorum requirement for the previous certificates
201            // because that is done during construction in `BatchCertificate::new`.
202            cfg_iter!(certificates).try_for_each(|certificate| {
203                // Collect the certificate signers.
204                let mut signers: HashSet<_> =
205                    certificate.signatures().map(|signature| signature.to_address()).collect();
206                // Append the certificate author.
207                signers.insert(certificate.author());
208
209                // Ensure that the signers of the certificate reach the quorum threshold.
210                ensure!(
211                    committee_lookback.is_quorum_threshold_reached(&signers),
212                    "Certificate '{}' for round {round} does not meet quorum requirements",
213                    certificate.id()
214                );
215
216                Ok::<_, Error>(())
217            })?;
218
219            Ok::<_, Error>(())
220        })?;
221
222        Ok(())
223    }
224
225    /// Checks that the block subdag can not be split into multiple valid subdags.
226    fn check_block_subdag_atomicity(&self, block: &Block<N>) -> Result<()> {
227        // Returns `true` if there is a path from the previous certificate to the current certificate.
228        fn is_linked<N: Network>(
229            subdag: &Subdag<N>,
230            previous_certificate: &BatchCertificate<N>,
231            current_certificate: &BatchCertificate<N>,
232        ) -> Result<bool> {
233            // Initialize the list containing the traversal.
234            let mut traversal = vec![current_certificate];
235            // Iterate over the rounds from the current certificate to the previous certificate.
236            for round in (previous_certificate.round()..current_certificate.round()).rev() {
237                // Retrieve all of the certificates for this past round.
238                let certificates = subdag.get(&round).ok_or(anyhow!("No certificates found for round {round}"))?;
239                // Filter the certificates to only include those that are in the traversal.
240                traversal = certificates
241                    .into_iter()
242                    .filter(|p| traversal.iter().any(|c| c.previous_certificate_ids().contains(&p.id())))
243                    .collect();
244            }
245            Ok(traversal.contains(&previous_certificate))
246        }
247
248        // Check if the block has a subdag.
249        let subdag = match block.authority() {
250            Authority::Quorum(subdag) => subdag,
251            _ => return Ok(()),
252        };
253
254        // Iterate over the rounds to find possible leader certificates.
255        for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2)
256        {
257            // Retrieve the previous committee lookback.
258            let previous_committee_lookback = self
259                .get_committee_lookback_for_round(round)?
260                .ok_or_else(|| anyhow!("No committee lookback found for round {round}"))?;
261
262            // Compute the leader for the commit round.
263            let computed_leader = previous_committee_lookback
264                .get_leader(round)
265                .with_context(|| format!("Failed to compute leader for round {round}"))?;
266
267            // Retrieve the previous leader certificates.
268            let previous_certificate = match subdag.get(&round).and_then(|certificates| {
269                certificates.iter().find(|certificate| certificate.author() == computed_leader)
270            }) {
271                Some(cert) => cert,
272                None => continue,
273            };
274
275            // Determine if there is a path between the previous certificate and the subdag's leader certificate.
276            if is_linked(subdag, previous_certificate, subdag.leader_certificate())? {
277                bail!(
278                    "The previous certificate should not be linked to the current certificate in block {}",
279                    block.height()
280                );
281            }
282        }
283
284        Ok(())
285    }
286}