1#![allow(clippy::too_many_arguments)]
17#![allow(clippy::type_complexity)]
18
19use super::*;
20use snarkvm_ledger_puzzle::Puzzle;
21use snarkvm_synthesizer_program::FinalizeOperation;
22
23use std::collections::HashSet;
24
25#[cfg(not(feature = "serial"))]
26use rayon::prelude::*;
27
28impl<N: Network> Block<N> {
29 pub fn verify(
31 &self,
32 previous_block: &Block<N>,
33 current_state_root: N::StateRoot,
34 previous_committee_lookback: &Committee<N>,
35 current_committee_lookback: &Committee<N>,
36 current_puzzle: &Puzzle<N>,
37 current_epoch_hash: N::BlockHash,
38 current_timestamp: i64,
39 ratified_finalize_operations: Vec<FinalizeOperation<N>>,
40 ) -> Result<(Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
41 self.verify_hash(previous_block.height(), previous_block.hash())?;
43
44 let (
46 expected_round,
47 expected_height,
48 expected_timestamp,
49 expected_existing_solution_ids,
50 expected_existing_transaction_ids,
51 ) = self.verify_authority(
52 previous_block.round(),
53 previous_block.height(),
54 previous_committee_lookback,
55 current_committee_lookback,
56 )?;
57
58 let (
60 expected_cumulative_weight,
61 expected_cumulative_proof_target,
62 expected_coinbase_target,
63 expected_proof_target,
64 expected_last_coinbase_target,
65 expected_last_coinbase_timestamp,
66 expected_block_reward,
67 expected_puzzle_reward,
68 ) = self.verify_solutions(previous_block, current_puzzle, current_epoch_hash)?;
69
70 self.verify_ratifications(expected_block_reward, expected_puzzle_reward)?;
72
73 self.verify_transactions()?;
75
76 let expected_previous_state_root = current_state_root;
78 let expected_transactions_root = self.compute_transactions_root()?;
80 let expected_finalize_root = self.compute_finalize_root(ratified_finalize_operations)?;
82 let expected_ratifications_root = self.compute_ratifications_root()?;
84 let expected_solutions_root = self.compute_solutions_root()?;
86 let expected_subdag_root = self.compute_subdag_root()?;
88
89 self.header.verify(
91 expected_previous_state_root,
92 expected_transactions_root,
93 expected_finalize_root,
94 expected_ratifications_root,
95 expected_solutions_root,
96 expected_subdag_root,
97 expected_round,
98 expected_height,
99 expected_cumulative_weight,
100 expected_cumulative_proof_target,
101 expected_coinbase_target,
102 expected_proof_target,
103 expected_last_coinbase_target,
104 expected_last_coinbase_timestamp,
105 expected_timestamp,
106 current_timestamp,
107 )?;
108
109 Ok((expected_existing_solution_ids, expected_existing_transaction_ids))
111 }
112}
113
114impl<N: Network> Block<N> {
115 fn verify_hash(&self, previous_height: u32, previous_hash: N::BlockHash) -> Result<(), Error> {
117 let expected_height = previous_height.saturating_add(1);
119
120 ensure!(
122 self.previous_hash == previous_hash,
123 "Previous block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
124 self.previous_hash,
125 previous_hash
126 );
127
128 let Ok(header_root) = self.header.to_root() else {
130 bail!("Failed to compute the Merkle root of the block header");
131 };
132 let candidate_hash = match N::hash_bhp1024(&to_bits_le![previous_hash, header_root]) {
134 Ok(candidate_hash) => candidate_hash,
135 Err(error) => bail!("Failed to compute the block hash for block {expected_height} - {error}"),
136 };
137 ensure!(
139 *self.block_hash == candidate_hash,
140 "Block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
141 self.block_hash,
142 Into::<N::BlockHash>::into(candidate_hash)
143 );
144 Ok(())
146 }
147
148 fn verify_authority(
150 &self,
151 previous_round: u64,
152 previous_height: u32,
153 previous_committee_lookback: &Committee<N>,
154 current_committee_lookback: &Committee<N>,
155 ) -> Result<(u64, u32, i64, Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
156 #[cfg(not(any(test, feature = "test")))]
158 ensure!(self.authority.is_quorum(), "The next block must be a quorum block");
159
160 let expected_height = previous_height.saturating_add(1);
162
163 let expected_round = match &self.authority {
165 Authority::Beacon(..) => previous_round.saturating_add(1),
167 Authority::Quorum(subdag) => {
169 ensure!(
171 subdag.anchor_round() > previous_round,
172 "Subdag anchor round is not after previous block round in block {} (found '{}', expected after '{}')",
173 expected_height,
174 subdag.anchor_round(),
175 previous_round
176 );
177 if previous_round != 0 {
179 for round in previous_round..=subdag.anchor_round() {
180 ensure!(
181 subdag.contains_key(&round),
182 "Subdag is missing round {round} in block {expected_height}",
183 );
184 }
185 }
186 subdag.anchor_round()
188 }
189 };
190 ensure!(
192 expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE)
193 >= current_committee_lookback.starting_round(),
194 "Block {expected_height} has an invalid round (found '{}', expected at least '{}')",
195 expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE),
196 current_committee_lookback.starting_round()
197 );
198
199 let (expected_existing_solution_ids, expected_existing_transaction_ids) = match &self.authority {
202 Authority::Beacon(signature) => {
203 let signer = signature.to_address();
205 ensure!(
207 current_committee_lookback.members().contains_key(&signer),
208 "Beacon block {expected_height} has a signer not in the committee (found '{signer}')",
209 );
210 ensure!(
212 signature.verify(&signer, &[*self.block_hash]),
213 "Signature is invalid in block {expected_height}"
214 );
215
216 (vec![], vec![])
217 }
218 Authority::Quorum(subdag) => {
219 let expected_leader = current_committee_lookback.get_leader(expected_round)?;
221 ensure!(
223 subdag.leader_address() == expected_leader,
224 "Quorum block {expected_height} is authored by an unexpected leader (found: {}, expected: {expected_leader})",
225 subdag.leader_address()
226 );
227 Self::check_subdag_transmissions(
232 subdag,
233 &self.solutions,
234 &self.aborted_solution_ids,
235 &self.transactions,
236 &self.aborted_transaction_ids,
237 )?
238 }
239 };
240
241 let expected_timestamp = match &self.authority {
243 Authority::Beacon(..) => self.timestamp(),
245 Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
247 };
248
249 if let Authority::Quorum(subdag) = &self.authority {
251 ensure!(
253 subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
254 "Leader certificate has an incorrect committee ID"
255 );
256
257 cfg_iter!(subdag).try_for_each(|(round, certificates)| {
259 let expected_committee_id = certificates
261 .first()
262 .map(|certificate| certificate.committee_id())
263 .ok_or(anyhow!("No certificates found for subdag round {round}"))?;
264 ensure!(
265 certificates.iter().skip(1).all(|certificate| certificate.committee_id() == expected_committee_id),
266 "Certificates on round {round} do not all have the same committee ID",
267 );
268 Ok(())
269 })?;
270 }
271
272 Ok((
274 expected_round,
275 expected_height,
276 expected_timestamp,
277 expected_existing_solution_ids,
278 expected_existing_transaction_ids,
279 ))
280 }
281
282 fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
284 let height = self.height();
285
286 ensure!(self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
288
289 let mut ratifications_iter = self.ratifications.iter();
291
292 let block_reward = match ratifications_iter.next() {
294 Some(Ratify::BlockReward(block_reward)) => *block_reward,
295 _ => bail!("Block {height} is invalid - the first ratification must be a block reward"),
296 };
297 let puzzle_reward = match ratifications_iter.next() {
299 Some(Ratify::PuzzleReward(puzzle_reward)) => *puzzle_reward,
300 _ => bail!("Block {height} is invalid - the second ratification must be a puzzle reward"),
301 };
302
303 ensure!(
305 block_reward == expected_block_reward,
306 "Block {height} has an invalid block reward (found '{block_reward}', expected '{expected_block_reward}')",
307 );
308 ensure!(
310 puzzle_reward == expected_puzzle_reward,
311 "Block {height} has an invalid puzzle reward (found '{puzzle_reward}', expected '{expected_puzzle_reward}')",
312 );
313 Ok(())
314 }
315
316 fn verify_solutions(
318 &self,
319 previous_block: &Block<N>,
320 current_puzzle: &Puzzle<N>,
321 current_epoch_hash: N::BlockHash,
322 ) -> Result<(u128, u128, u64, u64, u64, i64, u64, u64)> {
323 let height = self.height();
324 let timestamp = self.timestamp();
325
326 ensure!(
329 self.solutions.len() <= N::MAX_SOLUTIONS,
330 "Block {height} contains too many prover solutions (found '{}', expected '{}')",
331 self.solutions.len(),
332 N::MAX_SOLUTIONS
333 );
334
335 ensure!(
338 self.aborted_solution_ids.len() <= Solutions::<N>::max_aborted_solutions()?,
339 "Block {height} contains too many aborted solution IDs (found '{}')",
340 self.aborted_solution_ids.len(),
341 );
342
343 if has_duplicates(
345 self.solutions
346 .as_ref()
347 .map(PuzzleSolutions::solution_ids)
348 .into_iter()
349 .flatten()
350 .chain(self.aborted_solution_ids()),
351 ) {
352 bail!("Found a duplicate solution in block {height}");
353 }
354
355 let combined_proof_target = match self.solutions.deref() {
357 Some(solutions) => current_puzzle.get_combined_proof_target(solutions)?,
358 None => 0u128,
359 };
360
361 if let Some(coinbase) = self.solutions.deref() {
363 if let Err(e) = current_puzzle.check_solutions(coinbase, current_epoch_hash, previous_block.proof_target())
365 {
366 bail!("Block {height} contains an invalid puzzle proof - {e}");
367 }
368
369 if self.cumulative_proof_target() >= previous_block.coinbase_target() as u128 {
373 bail!("The cumulative proof target in block {height} must be less than the previous coinbase target")
374 }
375 };
376
377 let (
379 expected_coinbase_target,
380 expected_proof_target,
381 expected_cumulative_proof_target,
382 expected_cumulative_weight,
383 expected_last_coinbase_target,
384 expected_last_coinbase_timestamp,
385 ) = to_next_targets::<N>(
386 previous_block.cumulative_proof_target(),
387 combined_proof_target,
388 previous_block.coinbase_target(),
389 previous_block.cumulative_weight(),
390 previous_block.last_coinbase_target(),
391 previous_block.last_coinbase_timestamp(),
392 timestamp,
393 )?;
394
395 let expected_coinbase_reward = coinbase_reward::<N>(
397 height,
398 timestamp,
399 N::GENESIS_TIMESTAMP,
400 N::STARTING_SUPPLY,
401 N::ANCHOR_TIME,
402 N::ANCHOR_HEIGHT,
403 N::BLOCK_TIME,
404 combined_proof_target,
405 u64::try_from(previous_block.cumulative_proof_target())?,
406 previous_block.coinbase_target(),
407 )?;
408
409 let expected_transaction_fees =
411 self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
412
413 let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp());
415 let expected_block_reward = block_reward::<N>(
417 height,
418 N::STARTING_SUPPLY,
419 N::BLOCK_TIME,
420 time_since_last_block,
421 expected_coinbase_reward,
422 expected_transaction_fees,
423 )?;
424 let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward);
426
427 Ok((
428 expected_cumulative_weight,
429 expected_cumulative_proof_target,
430 expected_coinbase_target,
431 expected_proof_target,
432 expected_last_coinbase_target,
433 expected_last_coinbase_timestamp,
434 expected_block_reward,
435 expected_puzzle_reward,
436 ))
437 }
438
439 fn verify_transactions(&self) -> Result<()> {
441 let height = self.height();
442
443 if self.transactions.len() > Transactions::<N>::MAX_TRANSACTIONS {
446 bail!(
447 "Cannot validate a block with more than {} confirmed transactions",
448 Transactions::<N>::MAX_TRANSACTIONS
449 );
450 }
451
452 if self.aborted_transaction_ids.len() > Transactions::<N>::max_aborted_transactions()? {
455 bail!(
456 "Cannot validate a block with more than {} aborted transaction IDs",
457 Transactions::<N>::max_aborted_transactions()?
458 );
459 }
460
461 if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
463 bail!("Found a duplicate transaction in block {height}");
464 }
465
466 if has_duplicates(self.transition_ids()) {
468 bail!("Found a duplicate transition in block {height}");
469 }
470
471 if has_duplicates(
473 self.transactions().iter().filter_map(|tx| tx.transaction().deployment().map(|d| d.program_id())),
474 ) {
475 bail!("Found a duplicate program ID in block {height}");
476 }
477
478 if has_duplicates(self.input_ids()) {
482 bail!("Found a duplicate input ID in block {height}");
483 }
484 if has_duplicates(self.serial_numbers()) {
486 bail!("Found a duplicate serial number in block {height}");
487 }
488 if has_duplicates(self.tags()) {
490 bail!("Found a duplicate tag in block {height}");
491 }
492
493 if has_duplicates(self.output_ids()) {
497 bail!("Found a duplicate output ID in block {height}");
498 }
499 if has_duplicates(self.commitments()) {
501 bail!("Found a duplicate commitment in block {height}");
502 }
503 if has_duplicates(self.nonces()) {
505 bail!("Found a duplicate nonce in block {height}");
506 }
507
508 if has_duplicates(self.transition_public_keys()) {
512 bail!("Found a duplicate transition public key in block {height}");
513 }
514 if has_duplicates(self.transition_commitments()) {
516 bail!("Found a duplicate transition commitment in block {height}");
517 }
518 Ok(())
519 }
520}
521impl<N: Network> Block<N> {
522 fn compute_transactions_root(&self) -> Result<Field<N>> {
524 match self.transactions.to_transactions_root() {
525 Ok(transactions_root) => Ok(transactions_root),
526 Err(error) => bail!("Failed to compute the transactions root for block {} - {error}", self.height()),
527 }
528 }
529
530 fn compute_finalize_root(&self, ratified_finalize_operations: Vec<FinalizeOperation<N>>) -> Result<Field<N>> {
532 match self.transactions.to_finalize_root(ratified_finalize_operations) {
533 Ok(finalize_root) => Ok(finalize_root),
534 Err(error) => bail!("Failed to compute the finalize root for block {} - {error}", self.height()),
535 }
536 }
537
538 fn compute_ratifications_root(&self) -> Result<Field<N>> {
540 match self.ratifications.to_ratifications_root() {
541 Ok(ratifications_root) => Ok(ratifications_root),
542 Err(error) => bail!("Failed to compute the ratifications root for block {} - {error}", self.height()),
543 }
544 }
545
546 fn compute_solutions_root(&self) -> Result<Field<N>> {
548 self.solutions.to_solutions_root()
549 }
550
551 fn compute_subdag_root(&self) -> Result<Field<N>> {
553 match self.authority {
554 Authority::Quorum(ref subdag) => subdag.to_subdag_root(),
555 Authority::Beacon(_) => Ok(Field::zero()),
556 }
557 }
558
559 pub(super) fn check_subdag_transmissions(
562 subdag: &Subdag<N>,
563 solutions: &Option<PuzzleSolutions<N>>,
564 aborted_solution_ids: &[SolutionID<N>],
565 transactions: &Transactions<N>,
566 aborted_transaction_ids: &[N::TransactionID],
567 ) -> Result<(Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
568 let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
570 let unconfirmed_transactions = cfg_iter!(transactions)
572 .map(|confirmed| confirmed.to_unconfirmed_transaction())
573 .collect::<Result<Vec<_>>>()?;
574 let mut unconfirmed_transactions = unconfirmed_transactions.iter().peekable();
575
576 let mut seen_transaction_ids = HashSet::new();
578 let mut seen_solution_ids = HashSet::new();
579
580 let mut aborted_or_existing_solution_ids = HashSet::new();
582 let mut aborted_or_existing_transaction_ids = HashSet::new();
584
585 for transmission_id in subdag.transmission_ids() {
587 match transmission_id {
592 TransmissionID::Ratification => {}
593 TransmissionID::Solution(solution_id, _) => {
594 if !seen_solution_ids.insert(solution_id) {
595 continue;
596 }
597 }
598 TransmissionID::Transaction(transaction_id, _) => {
599 if !seen_transaction_ids.insert(transaction_id) {
600 continue;
601 }
602 }
603 }
604
605 match transmission_id {
607 TransmissionID::Ratification => {}
608 TransmissionID::Solution(solution_id, _checksum) => {
609 match solutions.peek() {
610 Some((_, solution)) if solution.id() == *solution_id => {
613 solutions.next();
615 }
616 _ => {
618 if !aborted_or_existing_solution_ids.insert(*solution_id) {
619 bail!("Block contains a duplicate aborted solution ID (found '{solution_id}')");
620 }
621 }
622 }
623 }
624 TransmissionID::Transaction(transaction_id, checksum) => {
625 match unconfirmed_transactions.peek() {
626 Some(transaction)
628 if transaction.id() == *transaction_id
629 && Data::<Transaction<N>>::Buffer(transaction.to_bytes_le()?.into())
630 .to_checksum::<N>()?
631 == *checksum =>
632 {
633 unconfirmed_transactions.next();
635 }
636 _ => {
638 if !aborted_or_existing_transaction_ids.insert(*transaction_id) {
639 bail!("Block contains a duplicate aborted transaction ID (found '{transaction_id}')");
640 }
641 }
642 }
643 }
644 }
645 }
646
647 ensure!(solutions.next().is_none(), "There exist more solutions than expected.");
649 ensure!(unconfirmed_transactions.next().is_none(), "There exist more transactions than expected.");
651
652 for aborted_solution_id in aborted_solution_ids {
654 if !aborted_or_existing_solution_ids.contains(aborted_solution_id) {
656 bail!(
657 "Block contains an aborted solution ID that is not found in the subdag (found '{aborted_solution_id}')"
658 );
659 }
660 }
661 for aborted_transaction_id in aborted_transaction_ids {
663 if !aborted_or_existing_transaction_ids.contains(aborted_transaction_id) {
665 bail!(
666 "Block contains an aborted transaction ID that is not found in the subdag (found '{aborted_transaction_id}')"
667 );
668 }
669 }
670
671 let existing_solution_ids: Vec<_> = aborted_or_existing_solution_ids
673 .difference(&aborted_solution_ids.iter().copied().collect())
674 .copied()
675 .collect();
676 let existing_transaction_ids: Vec<_> = aborted_or_existing_transaction_ids
678 .difference(&aborted_transaction_ids.iter().copied().collect())
679 .copied()
680 .collect();
681
682 Ok((existing_solution_ids, existing_transaction_ids))
683 }
684}