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}