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