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(
35 &self,
36 previous_block: &Block<N>,
37 current_state_root: N::StateRoot,
38 previous_committee_lookback: &Committee<N>,
39 current_committee_lookback: &Committee<N>,
40 current_puzzle: &Puzzle<N>,
41 current_epoch_hash: N::BlockHash,
42 current_timestamp: i64,
43 ratified_finalize_operations: Vec<FinalizeOperation<N>>,
44 ) -> Result<(Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
45 self.verify_hash(previous_block.height(), previous_block.hash())?;
47
48 let (
50 expected_round,
51 expected_height,
52 expected_timestamp,
53 expected_existing_solution_ids,
54 expected_existing_transaction_ids,
55 ) = self.verify_authority(
56 previous_block.round(),
57 previous_block.height(),
58 previous_committee_lookback,
59 current_committee_lookback,
60 )?;
61
62 let (
64 expected_cumulative_weight,
65 expected_cumulative_proof_target,
66 expected_coinbase_target,
67 expected_proof_target,
68 expected_last_coinbase_target,
69 expected_last_coinbase_timestamp,
70 expected_block_reward,
71 expected_puzzle_reward,
72 ) = self.verify_solutions(previous_block, current_puzzle, current_epoch_hash)?;
73
74 self.verify_ratifications(expected_block_reward, expected_puzzle_reward)?;
76
77 self.verify_transactions()?;
79
80 let expected_previous_state_root = current_state_root;
82 let expected_transactions_root = self.compute_transactions_root()?;
84 let expected_finalize_root = self.compute_finalize_root(ratified_finalize_operations)?;
86 let expected_ratifications_root = self.compute_ratifications_root()?;
88 let expected_solutions_root = self.compute_solutions_root()?;
90 let expected_subdag_root = self.compute_subdag_root()?;
92
93 self.header.verify(
95 expected_previous_state_root,
96 expected_transactions_root,
97 expected_finalize_root,
98 expected_ratifications_root,
99 expected_solutions_root,
100 expected_subdag_root,
101 expected_round,
102 expected_height,
103 expected_cumulative_weight,
104 expected_cumulative_proof_target,
105 expected_coinbase_target,
106 expected_proof_target,
107 expected_last_coinbase_target,
108 expected_last_coinbase_timestamp,
109 expected_timestamp,
110 current_timestamp,
111 )?;
112
113 Ok((expected_existing_solution_ids, expected_existing_transaction_ids))
115 }
116}
117
118impl<N: Network> Block<N> {
119 fn verify_hash(&self, previous_height: u32, previous_hash: N::BlockHash) -> Result<(), Error> {
121 let expected_height = previous_height.saturating_add(1);
123
124 ensure!(
126 self.previous_hash == previous_hash,
127 "Previous block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
128 self.previous_hash,
129 previous_hash
130 );
131
132 let Ok(header_root) = self.header.to_root() else {
134 bail!("Failed to compute the Merkle root of the block header");
135 };
136 let candidate_hash = match N::hash_bhp1024(&to_bits_le![previous_hash, header_root]) {
138 Ok(candidate_hash) => candidate_hash,
139 Err(error) => bail!("Failed to compute the block hash for block {expected_height} - {error}"),
140 };
141 ensure!(
143 *self.block_hash == candidate_hash,
144 "Block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
145 self.block_hash,
146 Into::<N::BlockHash>::into(candidate_hash)
147 );
148 Ok(())
150 }
151
152 fn verify_authority(
154 &self,
155 previous_round: u64,
156 previous_height: u32,
157 previous_committee_lookback: &Committee<N>,
158 current_committee_lookback: &Committee<N>,
159 ) -> Result<(u64, u32, i64, Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
160 #[cfg(not(any(test, feature = "test")))]
162 ensure!(self.authority.is_quorum(), "The next block must be a quorum block");
163
164 let expected_height = previous_height.saturating_add(1);
166
167 let expected_round = match &self.authority {
169 Authority::Beacon(..) => previous_round.saturating_add(1),
171 Authority::Quorum(subdag) => {
173 ensure!(
175 subdag.anchor_round() > previous_round,
176 "Subdag anchor round is not after previous block round in block {} (found '{}', expected after '{}')",
177 expected_height,
178 subdag.anchor_round(),
179 previous_round
180 );
181 if previous_round != 0 {
183 for round in previous_round..=subdag.anchor_round() {
184 ensure!(
185 subdag.contains_key(&round),
186 "Subdag is missing round {round} in block {expected_height}",
187 );
188 }
189 }
190 subdag.anchor_round()
192 }
193 };
194 ensure!(
196 expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE)
197 >= current_committee_lookback.starting_round(),
198 "Block {expected_height} has an invalid round (found '{}', expected at least '{}')",
199 expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE),
200 current_committee_lookback.starting_round()
201 );
202
203 let (expected_existing_solution_ids, expected_existing_transaction_ids) = match &self.authority {
206 Authority::Beacon(signature) => {
207 let signer = signature.to_address();
209 ensure!(
211 current_committee_lookback.members().contains_key(&signer),
212 "Beacon block {expected_height} has a signer not in the committee (found '{signer}')",
213 );
214 ensure!(
216 signature.verify(&signer, &[*self.block_hash]),
217 "Signature is invalid in block {expected_height}"
218 );
219
220 (vec![], vec![])
221 }
222 Authority::Quorum(subdag) => {
223 let expected_leader = current_committee_lookback.get_leader(expected_round)?;
225 ensure!(
227 subdag.leader_address() == expected_leader,
228 "Quorum block {expected_height} is authored by an unexpected leader (found: {}, expected: {expected_leader})",
229 subdag.leader_address()
230 );
231 Self::check_subdag_transmissions(
236 subdag,
237 &self.solutions,
238 &self.aborted_solution_ids,
239 &self.transactions,
240 &self.aborted_transaction_ids,
241 )?
242 }
243 };
244
245 let expected_timestamp = match &self.authority {
247 Authority::Beacon(..) => self.timestamp(),
249 Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
251 };
252
253 if let Authority::Quorum(subdag) = &self.authority {
255 ensure!(
257 subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
258 "Leader certificate has an incorrect committee ID"
259 );
260
261 cfg_iter!(subdag).try_for_each(|(round, certificates)| {
263 let expected_committee_id = certificates
265 .first()
266 .map(|certificate| certificate.committee_id())
267 .ok_or(anyhow!("No certificates found for subdag round {round}"))?;
268 ensure!(
269 certificates.iter().skip(1).all(|certificate| certificate.committee_id() == expected_committee_id),
270 "Certificates on round {round} do not all have the same committee ID",
271 );
272 Ok(())
273 })?;
274 }
275
276 Ok((
278 expected_round,
279 expected_height,
280 expected_timestamp,
281 expected_existing_solution_ids,
282 expected_existing_transaction_ids,
283 ))
284 }
285
286 fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
288 let height = self.height();
289
290 ensure!(self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
292
293 let mut ratifications_iter = self.ratifications.iter();
295
296 let block_reward = match ratifications_iter.next() {
298 Some(Ratify::BlockReward(block_reward)) => *block_reward,
299 _ => bail!("Block {height} is invalid - the first ratification must be a block reward"),
300 };
301 let puzzle_reward = match ratifications_iter.next() {
303 Some(Ratify::PuzzleReward(puzzle_reward)) => *puzzle_reward,
304 _ => bail!("Block {height} is invalid - the second ratification must be a puzzle reward"),
305 };
306
307 ensure!(
309 block_reward == expected_block_reward,
310 "Block {height} has an invalid block reward (found '{block_reward}', expected '{expected_block_reward}')",
311 );
312 ensure!(
314 puzzle_reward == expected_puzzle_reward,
315 "Block {height} has an invalid puzzle reward (found '{puzzle_reward}', expected '{expected_puzzle_reward}')",
316 );
317 Ok(())
318 }
319
320 fn verify_solutions(
322 &self,
323 previous_block: &Block<N>,
324 current_puzzle: &Puzzle<N>,
325 current_epoch_hash: N::BlockHash,
326 ) -> Result<(u128, u128, u64, u64, u64, i64, u64, u64)> {
327 let height = self.height();
328 let timestamp = self.timestamp();
329
330 ensure!(
333 self.solutions.len() <= N::MAX_SOLUTIONS,
334 "Block {height} contains too many prover solutions (found '{}', expected '{}')",
335 self.solutions.len(),
336 N::MAX_SOLUTIONS
337 );
338
339 ensure!(
342 self.aborted_solution_ids.len() <= Solutions::<N>::max_aborted_solutions()?,
343 "Block {height} contains too many aborted solution IDs (found '{}')",
344 self.aborted_solution_ids.len(),
345 );
346
347 if has_duplicates(
349 self.solutions
350 .as_ref()
351 .map(PuzzleSolutions::solution_ids)
352 .into_iter()
353 .flatten()
354 .chain(self.aborted_solution_ids()),
355 ) {
356 bail!("Found a duplicate solution in block {height}");
357 }
358
359 let combined_proof_target = match self.solutions.deref() {
361 Some(solutions) => current_puzzle.get_combined_proof_target(solutions)?,
362 None => 0u128,
363 };
364
365 if let Some(coinbase) = self.solutions.deref() {
367 if let Err(e) = current_puzzle.check_solutions(coinbase, current_epoch_hash, previous_block.proof_target())
369 {
370 bail!("Block {height} contains an invalid puzzle proof - {e}");
371 }
372
373 if self.cumulative_proof_target() >= previous_block.coinbase_target() as u128 {
377 bail!("The cumulative proof target in block {height} must be less than the previous coinbase target")
378 }
379 };
380
381 let (
383 expected_coinbase_target,
384 expected_proof_target,
385 expected_cumulative_proof_target,
386 expected_cumulative_weight,
387 expected_last_coinbase_target,
388 expected_last_coinbase_timestamp,
389 ) = to_next_targets::<N>(
390 previous_block.cumulative_proof_target(),
391 combined_proof_target,
392 previous_block.coinbase_target(),
393 previous_block.cumulative_weight(),
394 previous_block.last_coinbase_target(),
395 previous_block.last_coinbase_timestamp(),
396 timestamp,
397 )?;
398
399 let expected_coinbase_reward = coinbase_reward::<N>(
401 height,
402 timestamp,
403 N::GENESIS_TIMESTAMP,
404 N::STARTING_SUPPLY,
405 N::ANCHOR_TIME,
406 N::ANCHOR_HEIGHT,
407 N::BLOCK_TIME,
408 combined_proof_target,
409 u64::try_from(previous_block.cumulative_proof_target())?,
410 previous_block.coinbase_target(),
411 )?;
412
413 let expected_transaction_fees =
415 self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
416
417 let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp());
419 let expected_block_reward = block_reward::<N>(
421 height,
422 N::STARTING_SUPPLY,
423 N::BLOCK_TIME,
424 time_since_last_block,
425 expected_coinbase_reward,
426 expected_transaction_fees,
427 )?;
428 let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward);
430
431 Ok((
432 expected_cumulative_weight,
433 expected_cumulative_proof_target,
434 expected_coinbase_target,
435 expected_proof_target,
436 expected_last_coinbase_target,
437 expected_last_coinbase_timestamp,
438 expected_block_reward,
439 expected_puzzle_reward,
440 ))
441 }
442
443 fn verify_transactions(&self) -> Result<()> {
445 let height = self.height();
446
447 if self.transactions.len() > Transactions::<N>::MAX_TRANSACTIONS {
450 bail!(
451 "Cannot validate a block with more than {} confirmed transactions",
452 Transactions::<N>::MAX_TRANSACTIONS
453 );
454 }
455
456 if self.aborted_transaction_ids.len() > Transactions::<N>::max_aborted_transactions()? {
459 bail!(
460 "Cannot validate a block with more than {} aborted transaction IDs",
461 Transactions::<N>::max_aborted_transactions()?
462 );
463 }
464
465 if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
467 bail!("Found a duplicate transaction in block {height}");
468 }
469
470 if has_duplicates(self.transition_ids()) {
472 bail!("Found a duplicate transition in block {height}");
473 }
474
475 if has_duplicates(
477 self.transactions().iter().filter_map(|tx| tx.transaction().deployment().map(|d| d.program_id())),
478 ) {
479 bail!("Found a duplicate program ID in block {height}");
480 }
481
482 if has_duplicates(self.input_ids()) {
486 bail!("Found a duplicate input ID in block {height}");
487 }
488 if has_duplicates(self.serial_numbers()) {
490 bail!("Found a duplicate serial number in block {height}");
491 }
492 if has_duplicates(self.tags()) {
494 bail!("Found a duplicate tag in block {height}");
495 }
496
497 if has_duplicates(self.output_ids()) {
501 bail!("Found a duplicate output ID in block {height}");
502 }
503 if has_duplicates(self.commitments()) {
505 bail!("Found a duplicate commitment in block {height}");
506 }
507 if has_duplicates(self.nonces()) {
509 bail!("Found a duplicate nonce in block {height}");
510 }
511
512 if has_duplicates(self.transition_public_keys()) {
516 bail!("Found a duplicate transition public key in block {height}");
517 }
518 if has_duplicates(self.transition_commitments()) {
520 bail!("Found a duplicate transition commitment in block {height}");
521 }
522 Ok(())
523 }
524}
525impl<N: Network> Block<N> {
526 fn compute_transactions_root(&self) -> Result<Field<N>> {
528 match self.transactions.to_transactions_root() {
529 Ok(transactions_root) => Ok(transactions_root),
530 Err(error) => bail!("Failed to compute the transactions root for block {} - {error}", self.height()),
531 }
532 }
533
534 fn compute_finalize_root(&self, ratified_finalize_operations: Vec<FinalizeOperation<N>>) -> Result<Field<N>> {
536 match self.transactions.to_finalize_root(ratified_finalize_operations) {
537 Ok(finalize_root) => Ok(finalize_root),
538 Err(error) => bail!("Failed to compute the finalize root for block {} - {error}", self.height()),
539 }
540 }
541
542 fn compute_ratifications_root(&self) -> Result<Field<N>> {
544 match self.ratifications.to_ratifications_root() {
545 Ok(ratifications_root) => Ok(ratifications_root),
546 Err(error) => bail!("Failed to compute the ratifications root for block {} - {error}", self.height()),
547 }
548 }
549
550 fn compute_solutions_root(&self) -> Result<Field<N>> {
552 self.solutions.to_solutions_root()
553 }
554
555 fn compute_subdag_root(&self) -> Result<Field<N>> {
557 match self.authority {
558 Authority::Quorum(ref subdag) => subdag.to_subdag_root(),
559 Authority::Beacon(_) => Ok(Field::zero()),
560 }
561 }
562
563 pub(super) fn check_subdag_transmissions(
566 subdag: &Subdag<N>,
567 solutions: &Option<PuzzleSolutions<N>>,
568 aborted_solution_ids: &[SolutionID<N>],
569 transactions: &Transactions<N>,
570 aborted_transaction_ids: &[N::TransactionID],
571 ) -> Result<(Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
572 let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
574 let unconfirmed_transactions = cfg_iter!(transactions)
576 .map(|confirmed| confirmed.to_unconfirmed_transaction())
577 .collect::<Result<Vec<_>>>()?;
578 let mut unconfirmed_transactions = unconfirmed_transactions.iter().peekable();
579
580 let mut seen_transaction_ids = HashSet::new();
582 let mut seen_solution_ids = HashSet::new();
583
584 let mut aborted_or_existing_solution_ids = HashSet::new();
586 let mut aborted_or_existing_transaction_ids = HashSet::new();
588
589 for transmission_id in subdag.transmission_ids() {
591 match transmission_id {
596 TransmissionID::Ratification => {}
597 TransmissionID::Solution(solution_id, _) => {
598 if !seen_solution_ids.insert(solution_id) {
599 continue;
600 }
601 }
602 TransmissionID::Transaction(transaction_id, _) => {
603 if !seen_transaction_ids.insert(transaction_id) {
604 continue;
605 }
606 }
607 }
608
609 match transmission_id {
611 TransmissionID::Ratification => {}
612 TransmissionID::Solution(solution_id, _checksum) => {
613 match solutions.peek() {
614 Some((_, solution)) if solution.id() == *solution_id => {
617 solutions.next();
619 }
620 _ => {
622 if !aborted_or_existing_solution_ids.insert(*solution_id) {
623 bail!("Block contains a duplicate aborted solution ID (found '{solution_id}')");
624 }
625 }
626 }
627 }
628 TransmissionID::Transaction(transaction_id, checksum) => {
629 match unconfirmed_transactions.peek() {
630 Some(transaction)
632 if transaction.id() == *transaction_id
633 && Data::<Transaction<N>>::Buffer(transaction.to_bytes_le()?.into())
634 .to_checksum::<N>()?
635 == *checksum =>
636 {
637 unconfirmed_transactions.next();
639 }
640 _ => {
642 if !aborted_or_existing_transaction_ids.insert(*transaction_id) {
643 bail!("Block contains a duplicate aborted transaction ID (found '{transaction_id}')");
644 }
645 }
646 }
647 }
648 }
649 }
650
651 ensure!(solutions.next().is_none(), "There exist more solutions than expected.");
653 ensure!(unconfirmed_transactions.next().is_none(), "There exist more transactions than expected.");
655
656 for aborted_solution_id in aborted_solution_ids {
658 if !aborted_or_existing_solution_ids.contains(aborted_solution_id) {
660 bail!(
661 "Block contains an aborted solution ID that is not found in the subdag (found '{aborted_solution_id}')"
662 );
663 }
664 }
665 for aborted_transaction_id in aborted_transaction_ids {
667 if !aborted_or_existing_transaction_ids.contains(aborted_transaction_id) {
669 bail!(
670 "Block contains an aborted transaction ID that is not found in the subdag (found '{aborted_transaction_id}')"
671 );
672 }
673 }
674
675 let existing_solution_ids: Vec<_> = aborted_or_existing_solution_ids
677 .difference(&aborted_solution_ids.iter().copied().collect())
678 .copied()
679 .collect();
680 let existing_transaction_ids: Vec<_> = aborted_or_existing_transaction_ids
682 .difference(&aborted_transaction_ids.iter().copied().collect())
683 .copied()
684 .collect();
685
686 Ok((existing_solution_ids, existing_transaction_ids))
687 }
688}