Skip to main content

snarkvm_ledger_block/
verify.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
16#![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    /// Ensures the block is well-formed and consistent with the previous block.
30    ///
31    /// # Returns
32    /// - On success, the sets of transaction and solution IDs that existed in this subDAG but were already included in the previous block.
33    /// - On failure, the error that caused verification to fail, e.g., invalid block hash, invalid block authority, or invalid transmissions.
34    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        // Ensure the block hash is correct.
46        self.verify_hash(previous_block.height(), previous_block.hash())?;
47
48        // Ensure the block authority is correct.
49        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        // Ensure the block solutions are correct.
63        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        // Ensure the block ratifications are correct.
75        self.verify_ratifications(expected_block_reward, expected_puzzle_reward)?;
76
77        // Ensure the block transactions are correct.
78        self.verify_transactions()?;
79
80        // Set the expected previous state root.
81        let expected_previous_state_root = current_state_root;
82        // Compute the expected transactions root.
83        let expected_transactions_root = self.compute_transactions_root()?;
84        // Compute the expected finalize root.
85        let expected_finalize_root = self.compute_finalize_root(ratified_finalize_operations)?;
86        // Compute the expected ratifications root.
87        let expected_ratifications_root = self.compute_ratifications_root()?;
88        // Compute the expected solutions root.
89        let expected_solutions_root = self.compute_solutions_root()?;
90        // Compute the expected subdag root.
91        let expected_subdag_root = self.compute_subdag_root()?;
92
93        // Ensure the block header is correct.
94        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        // Return the expected existing solution IDs and transaction IDs.
114        Ok((expected_existing_solution_ids, expected_existing_transaction_ids))
115    }
116}
117
118impl<N: Network> Block<N> {
119    /// Ensures the block hash is correct.
120    fn verify_hash(&self, previous_height: u32, previous_hash: N::BlockHash) -> Result<(), Error> {
121        // Determine the expected height.
122        let expected_height = previous_height.saturating_add(1);
123
124        // Ensure the previous block hash matches.
125        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        // Compute the Merkle root of the block header.
133        let Ok(header_root) = self.header.to_root() else {
134            bail!("Failed to compute the Merkle root of the block header");
135        };
136        // Compute the block hash.
137        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 the block hash matches.
142        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        // Return success.
149        Ok(())
150    }
151
152    /// Ensures the block authority is correct.
153    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        // Note: Do not remove this. This ensures that all blocks after genesis are quorum blocks.
161        #[cfg(not(any(test, feature = "test")))]
162        ensure!(self.authority.is_quorum(), "The next block must be a quorum block");
163
164        // Determine the expected height.
165        let expected_height = previous_height.saturating_add(1);
166
167        // Determine the expected round.
168        let expected_round = match &self.authority {
169            // Beacon blocks increment the previous block round by 1.
170            Authority::Beacon(..) => previous_round.saturating_add(1),
171            // Quorum blocks use the subdag anchor round.
172            Authority::Quorum(subdag) => {
173                // Ensure the subdag anchor round is after the previous block round.
174                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                // Ensure that the rounds in the subdag are sequential.
182                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                // Output the subdag anchor round.
191                subdag.anchor_round()
192            }
193        };
194        // Ensure the block round minus the committee lookback range is at least the starting round of the committee lookback.
195        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        // Ensure the block authority is correct.
204        // Determine the solution IDs and transaction IDs that are expected to be in previous blocks.
205        let (expected_existing_solution_ids, expected_existing_transaction_ids) = match &self.authority {
206            Authority::Beacon(signature) => {
207                // Retrieve the signer.
208                let signer = signature.to_address();
209                // Ensure the block is signed by a committee member.
210                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 the signature is valid.
215                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                // Compute the expected leader.
224                let expected_leader = current_committee_lookback.get_leader(expected_round)?;
225                // Ensure the block is authored by the expected leader.
226                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                // Ensure the transmission IDs from the subdag correspond to the block.
232                // This is redundant if the block has been created via `Block::from()`;
233                // however, we need to obtain the solution and transaction IDs here,
234                // so may want to remove the redundant check in `Block::from()` and leave it here.
235                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        // Determine the expected timestamp.
246        let expected_timestamp = match &self.authority {
247            // Beacon blocks do not have a timestamp check.
248            Authority::Beacon(..) => self.timestamp(),
249            // Quorum blocks use the weighted median timestamp from the subdag.
250            Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
251        };
252
253        // Check that the committee IDs are correct.
254        if let Authority::Quorum(subdag) = &self.authority {
255            // Check that the committee ID of the leader certificate is correct.
256            ensure!(
257                subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
258                "Leader certificate has an incorrect committee ID"
259            );
260
261            // Check that all certificates on each round have the same committee ID.
262            cfg_iter!(subdag).try_for_each(|(round, certificates)| {
263                // Check that every certificate for a given round shares the same committee ID.
264                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        // Return success.
277        Ok((
278            expected_round,
279            expected_height,
280            expected_timestamp,
281            expected_existing_solution_ids,
282            expected_existing_transaction_ids,
283        ))
284    }
285
286    /// Ensures the block ratifications are correct.
287    fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
288        let height = self.height();
289
290        // Ensure there are sufficient ratifications.
291        ensure!(self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
292
293        // Initialize a ratifications iterator.
294        let mut ratifications_iter = self.ratifications.iter();
295
296        // Retrieve the block reward from the first block ratification.
297        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        // Retrieve the puzzle reward from the second block ratification.
302        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 the block reward is correct.
308        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 the puzzle reward is correct.
313        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    /// Ensures the block solutions are correct.
321    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 the number of solutions is within the allowed range.
331        // This check is redundant if the block has been created via `Block::from()`.
332        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 the number of aborted solution IDs is within the allowed range.
340        // This check is redundant if the block has been created via `Block::from()`.
341        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        // Ensure there are no duplicate solution IDs.
348        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        // Compute the combined proof target.
360        let combined_proof_target = match self.solutions.deref() {
361            Some(solutions) => current_puzzle.get_combined_proof_target(solutions)?,
362            None => 0u128,
363        };
364
365        // Verify the solutions.
366        if let Some(coinbase) = self.solutions.deref() {
367            // Ensure the puzzle proof is valid.
368            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            // Ensure that the block cumulative proof target is less than the previous block's coinbase target.
374            // Note: This is a sanity check, as the cumulative proof target resets to 0 if the
375            // coinbase target was reached in this block.
376            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        // Calculate the next coinbase targets and timestamps.
382        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        // Calculate the expected coinbase reward.
400        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        // Calculate the expected transaction fees.
414        let expected_transaction_fees =
415            self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
416
417        // Calculate the time since last block.
418        let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp());
419        // Compute the expected block reward.
420        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        // Compute the expected puzzle reward.
429        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    /// Ensures the block transactions are correct.
444    fn verify_transactions(&self) -> Result<()> {
445        let height = self.height();
446
447        // Ensure the number of transactions is within the allowed range.
448        // This check is redundant if the block has been created via `Block::from()`.
449        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        // Ensure the number of aborted transaction IDs is within the allowed range.
457        // This check is redundant if the block has been created via `Block::from()`.
458        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        // Ensure there are no duplicate transaction IDs.
466        if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
467            bail!("Found a duplicate transaction in block {height}");
468        }
469
470        // Ensure there are no duplicate transition IDs.
471        if has_duplicates(self.transition_ids()) {
472            bail!("Found a duplicate transition in block {height}");
473        }
474
475        // Ensure there are no duplicate program IDs.
476        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        /* Input */
483
484        // Ensure there are no duplicate input IDs.
485        if has_duplicates(self.input_ids()) {
486            bail!("Found a duplicate input ID in block {height}");
487        }
488        // Ensure there are no duplicate serial numbers.
489        if has_duplicates(self.serial_numbers()) {
490            bail!("Found a duplicate serial number in block {height}");
491        }
492        // Ensure there are no duplicate tags.
493        if has_duplicates(self.tags()) {
494            bail!("Found a duplicate tag in block {height}");
495        }
496
497        /* Output */
498
499        // Ensure there are no duplicate output IDs.
500        if has_duplicates(self.output_ids()) {
501            bail!("Found a duplicate output ID in block {height}");
502        }
503        // Ensure there are no duplicate commitments.
504        if has_duplicates(self.commitments()) {
505            bail!("Found a duplicate commitment in block {height}");
506        }
507        // Ensure there are no duplicate nonces.
508        if has_duplicates(self.nonces()) {
509            bail!("Found a duplicate nonce in block {height}");
510        }
511
512        /* Metadata */
513
514        // Ensure there are no duplicate transition public keys.
515        if has_duplicates(self.transition_public_keys()) {
516            bail!("Found a duplicate transition public key in block {height}");
517        }
518        // Ensure there are no duplicate transition commitments.
519        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    /// Computes the transactions root for the block.
527    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    /// Computes the finalize root for the block.
535    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    /// Computes the ratifications root for the block.
543    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    /// Computes the solutions root for the block.
551    fn compute_solutions_root(&self) -> Result<Field<N>> {
552        self.solutions.to_solutions_root()
553    }
554
555    /// Computes the subdag root for the block.
556    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    /// Checks that the transmission IDs in the given subdag matches the solutions and transactions in the block.
564    /// Returns the IDs of the transactions and solutions that should already exist in the ledger.
565    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        // Prepare an iterator over the solution IDs.
573        let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
574        // Prepare an iterator over the unconfirmed transactions.
575        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        // Initialize a set of already seen transaction and solution IDs.
581        let mut seen_transaction_ids = HashSet::new();
582        let mut seen_solution_ids = HashSet::new();
583
584        // Initialize a set of aborted or already-existing solution IDs.
585        let mut aborted_or_existing_solution_ids = HashSet::new();
586        // Initialize a set of aborted or already-existing transaction IDs.
587        let mut aborted_or_existing_transaction_ids = HashSet::new();
588
589        // Iterate over the transmission IDs.
590        for transmission_id in subdag.transmission_ids() {
591            // If the transaction or solution ID has already been seen, then continue.
592            // Note: This is done instead of checking `TransmissionID` directly, because we need to
593            // ensure that each transaction or solution ID is unique. The `TransmissionID` is guaranteed
594            // to be unique, however the transaction/solution ID may not be due to malleability concerns.
595            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            // Process the transmission ID.
610            match transmission_id {
611                TransmissionID::Ratification => {}
612                TransmissionID::Solution(solution_id, _checksum) => {
613                    match solutions.peek() {
614                        // Check the next solution matches the expected solution ID.
615                        // We don't check against the checksum, because check_solution_mut might mutate the solution.
616                        Some((_, solution)) if solution.id() == *solution_id => {
617                            // Increment the solution iterator.
618                            solutions.next();
619                        }
620                        // Otherwise, add the solution ID to the aborted or existing list.
621                        _ => {
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                        // Check the next transaction matches the expected transaction.
631                        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                            // Increment the unconfirmed transaction iterator.
638                            unconfirmed_transactions.next();
639                        }
640                        // Otherwise, add the transaction ID to the aborted or existing list.
641                        _ => {
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 there are no more solutions in the block.
652        ensure!(solutions.next().is_none(), "There exist more solutions than expected.");
653        // Ensure there are no more transactions in the block.
654        ensure!(unconfirmed_transactions.next().is_none(), "There exist more transactions than expected.");
655
656        // Ensure the aborted solution IDs match.
657        for aborted_solution_id in aborted_solution_ids {
658            // If the aborted transaction ID is not found, throw an error.
659            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        // Ensure the aborted transaction IDs match.
666        for aborted_transaction_id in aborted_transaction_ids {
667            // If the aborted transaction ID is not found, throw an error.
668            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        // Retrieve the solution IDs that should already exist in the ledger.
676        let existing_solution_ids: Vec<_> = aborted_or_existing_solution_ids
677            .difference(&aborted_solution_ids.iter().copied().collect())
678            .copied()
679            .collect();
680        // Retrieve the transaction IDs that should already exist in the ledger.
681        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}