snarkvm_ledger/
check_next_block.rs1use super::*;
17
18use crate::narwhal::BatchHeader;
19
20use anyhow::{Context, bail};
21
22impl<N: Network, C: ConsensusStorage<N>> Ledger<N, C> {
23 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 if height != latest_block.height() + 1 {
30 bail!("Block height is {height}, but expected {}", latest_block.height() + 1);
31 }
32
33 if self.contains_block_hash(&block.hash())? {
35 bail!("Block hash '{}' already exists in the ledger", block.hash())
36 }
37
38 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 let block_timestamp = (block.height() >= N::CONSENSUS_HEIGHT(ConsensusVersion::V12).unwrap_or_default())
47 .then_some(block.timestamp());
48 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 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 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 let previous_committee_lookback = {
79 let penultimate_round = block.round().saturating_sub(1);
81 self.get_committee_lookback_for_round(penultimate_round)?
83 .ok_or(anyhow!("Failed to fetch committee lookback for round {penultimate_round}"))?
84 };
85
86 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 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 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 *accepted_solutions.entry(prover_address).or_insert(0) += 1;
112 }
113 }
114
115 self.check_block_subdag_quorum(block)?;
117
118 self.check_block_subdag_atomicity(block)?;
120
121 self.check_block_subdag_leaves(block)?;
123
124 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 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 fn check_block_subdag_leaves(&self, block: &Block<N>) -> Result<()> {
146 let Authority::Quorum(subdag) = block.authority() else {
148 return Ok(());
149 };
150
151 let subdag_certs: HashSet<_> = subdag.certificate_ids().collect();
154
155 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 return Ok::<(), Error>(());
170 }
171
172 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 fn check_block_subdag_quorum(&self, block: &Block<N>) -> Result<()> {
185 let subdag = match block.authority() {
187 Authority::Quorum(subdag) => subdag,
188 _ => return Ok(()),
189 };
190
191 cfg_iter!(subdag).try_for_each(|(round, certificates)| {
193 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 cfg_iter!(certificates).try_for_each(|certificate| {
203 let mut signers: HashSet<_> =
205 certificate.signatures().map(|signature| signature.to_address()).collect();
206 signers.insert(certificate.author());
208
209 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 fn check_block_subdag_atomicity(&self, block: &Block<N>) -> Result<()> {
227 fn is_linked<N: Network>(
229 subdag: &Subdag<N>,
230 previous_certificate: &BatchCertificate<N>,
231 current_certificate: &BatchCertificate<N>,
232 ) -> Result<bool> {
233 let mut traversal = vec![current_certificate];
235 for round in (previous_certificate.round()..current_certificate.round()).rev() {
237 let certificates = subdag.get(&round).ok_or(anyhow!("No certificates found for round {round}"))?;
239 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 let subdag = match block.authority() {
250 Authority::Quorum(subdag) => subdag,
251 _ => return Ok(()),
252 };
253
254 for round in (self.latest_round().saturating_add(2)..=subdag.anchor_round().saturating_sub(2)).rev().step_by(2)
256 {
257 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 let computed_leader = previous_committee_lookback
264 .get_leader(round)
265 .with_context(|| format!("Failed to compute leader for round {round}"))?;
266
267 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 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}