Skip to main content

snarkvm_ledger_block/
verify.rs

1// Copyright (c) 2019-2026 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 certificates follow the canonical order for this consensus version.
174                subdag.check_certificate_order(expected_height)?;
175                // Ensure the subdag anchor round is after the previous block round.
176                ensure!(
177                    subdag.anchor_round() > previous_round,
178                    "Subdag anchor round is not after previous block round in block {} (found '{}', expected after '{}')",
179                    expected_height,
180                    subdag.anchor_round(),
181                    previous_round
182                );
183                // Ensure that the rounds in the subdag are sequential.
184                if previous_round != 0 {
185                    for round in previous_round..=subdag.anchor_round() {
186                        ensure!(
187                            subdag.contains_key(&round),
188                            "Subdag is missing round {round} in block {expected_height}",
189                        );
190                    }
191                }
192                // Output the subdag anchor round.
193                subdag.anchor_round()
194            }
195        };
196        // Ensure the block round minus the committee lookback range is at least the starting round of the committee lookback.
197        ensure!(
198            expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE)
199                >= current_committee_lookback.starting_round(),
200            "Block {expected_height} has an invalid round (found '{}', expected at least '{}')",
201            expected_round.saturating_sub(Committee::<N>::COMMITTEE_LOOKBACK_RANGE),
202            current_committee_lookback.starting_round()
203        );
204
205        // Ensure the block authority is correct.
206        // Determine the solution IDs and transaction IDs that are expected to be in previous blocks.
207        let (expected_existing_solution_ids, expected_existing_transaction_ids) = match &self.authority {
208            Authority::Beacon(signature) => {
209                // Retrieve the signer.
210                let signer = signature.to_address();
211                // Ensure the block is signed by a committee member.
212                ensure!(
213                    current_committee_lookback.members().contains_key(&signer),
214                    "Beacon block {expected_height} has a signer not in the committee (found '{signer}')",
215                );
216                // Ensure the signature is valid.
217                ensure!(
218                    signature.verify(&signer, &[*self.block_hash]),
219                    "Signature is invalid in block {expected_height}"
220                );
221
222                (vec![], vec![])
223            }
224            Authority::Quorum(subdag) => {
225                // Compute the expected leader.
226                let expected_leader = current_committee_lookback.get_leader(expected_round)?;
227                // Ensure the block is authored by the expected leader.
228                ensure!(
229                    subdag.leader_address() == expected_leader,
230                    "Quorum block {expected_height} is authored by an unexpected leader (found: {}, expected: {expected_leader})",
231                    subdag.leader_address()
232                );
233                // Ensure the transmission IDs from the subdag correspond to the block.
234                // This is redundant if the block has been created via `Block::from()`;
235                // however, we need to obtain the solution and transaction IDs here,
236                // so may want to remove the redundant check in `Block::from()` and leave it here.
237                Self::check_subdag_transmissions(
238                    subdag,
239                    &self.solutions,
240                    &self.aborted_solution_ids,
241                    &self.transactions,
242                    &self.aborted_transaction_ids,
243                )?
244            }
245        };
246
247        // Determine the expected timestamp.
248        let expected_timestamp = match &self.authority {
249            // Beacon blocks do not have a timestamp check.
250            Authority::Beacon(..) => self.timestamp(),
251            // Quorum blocks use the weighted median timestamp from the subdag.
252            Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
253        };
254
255        // Check that the committee IDs are correct.
256        if let Authority::Quorum(subdag) = &self.authority {
257            // Check that the committee ID of the leader certificate is correct.
258            ensure!(
259                subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
260                "Leader certificate has an incorrect committee ID"
261            );
262
263            // Check that all certificates on each round have the same committee ID.
264            cfg_iter!(subdag).try_for_each(|(round, certificates)| {
265                // Check that every certificate for a given round shares the same committee ID.
266                let expected_committee_id = certificates
267                    .first()
268                    .map(|certificate| certificate.committee_id())
269                    .ok_or(anyhow!("No certificates found for subdag round {round}"))?;
270                ensure!(
271                    certificates.iter().skip(1).all(|certificate| certificate.committee_id() == expected_committee_id),
272                    "Certificates on round {round} do not all have the same committee ID",
273                );
274                Ok(())
275            })?;
276        }
277
278        // Return success.
279        Ok((
280            expected_round,
281            expected_height,
282            expected_timestamp,
283            expected_existing_solution_ids,
284            expected_existing_transaction_ids,
285        ))
286    }
287
288    /// Ensures the block ratifications are correct.
289    fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
290        let height = self.height();
291
292        // Ensure there are sufficient ratifications.
293        ensure!(self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
294
295        // Initialize a ratifications iterator.
296        let mut ratifications_iter = self.ratifications.iter();
297
298        // Retrieve the block reward from the first block ratification.
299        let block_reward = match ratifications_iter.next() {
300            Some(Ratify::BlockReward(block_reward)) => *block_reward,
301            _ => bail!("Block {height} is invalid - the first ratification must be a block reward"),
302        };
303        // Retrieve the puzzle reward from the second block ratification.
304        let puzzle_reward = match ratifications_iter.next() {
305            Some(Ratify::PuzzleReward(puzzle_reward)) => *puzzle_reward,
306            _ => bail!("Block {height} is invalid - the second ratification must be a puzzle reward"),
307        };
308
309        // Ensure the block reward is correct.
310        ensure!(
311            block_reward == expected_block_reward,
312            "Block {height} has an invalid block reward (found '{block_reward}', expected '{expected_block_reward}')",
313        );
314        // Ensure the puzzle reward is correct.
315        ensure!(
316            puzzle_reward == expected_puzzle_reward,
317            "Block {height} has an invalid puzzle reward (found '{puzzle_reward}', expected '{expected_puzzle_reward}')",
318        );
319        Ok(())
320    }
321
322    /// Ensures the block solutions are correct.
323    fn verify_solutions(
324        &self,
325        previous_block: &Block<N>,
326        current_puzzle: &Puzzle<N>,
327        current_epoch_hash: N::BlockHash,
328    ) -> Result<(u128, u128, u64, u64, u64, i64, u64, u64)> {
329        let height = self.height();
330        let timestamp = self.timestamp();
331
332        // Ensure the number of solutions is within the allowed range.
333        // This check is redundant if the block has been created via `Block::from()`.
334        ensure!(
335            self.solutions.len() <= N::MAX_SOLUTIONS,
336            "Block {height} contains too many prover solutions (found '{}', expected '{}')",
337            self.solutions.len(),
338            N::MAX_SOLUTIONS
339        );
340
341        // Ensure the number of aborted solution IDs is within the allowed range.
342        // This check is redundant if the block has been created via `Block::from()`.
343        ensure!(
344            self.aborted_solution_ids.len() <= Solutions::<N>::max_aborted_solutions(),
345            "Block {height} contains too many aborted solution IDs (found '{}')",
346            self.aborted_solution_ids.len(),
347        );
348
349        // Ensure there are no duplicate solution IDs.
350        if has_duplicates(
351            self.solutions
352                .as_ref()
353                .map(PuzzleSolutions::solution_ids)
354                .into_iter()
355                .flatten()
356                .chain(self.aborted_solution_ids()),
357        ) {
358            bail!("Found a duplicate solution in block {height}");
359        }
360
361        // Compute the combined proof target.
362        let combined_proof_target = match self.solutions.deref() {
363            Some(solutions) => current_puzzle.get_combined_proof_target(solutions)?,
364            None => 0u128,
365        };
366
367        // Verify the solutions.
368        if let Some(coinbase) = self.solutions.deref() {
369            // Ensure the puzzle proof is valid.
370            if let Err(e) = current_puzzle.check_solutions(coinbase, current_epoch_hash, previous_block.proof_target())
371            {
372                bail!("Block {height} contains an invalid puzzle proof - {e}");
373            }
374
375            // Ensure that the block cumulative proof target is less than the previous block's coinbase target.
376            // Note: This is a sanity check, as the cumulative proof target resets to 0 if the
377            // coinbase target was reached in this block.
378            if self.cumulative_proof_target() >= previous_block.coinbase_target() as u128 {
379                bail!("The cumulative proof target in block {height} must be less than the previous coinbase target")
380            }
381        };
382
383        // Calculate the next coinbase targets and timestamps.
384        let (
385            expected_coinbase_target,
386            expected_proof_target,
387            expected_cumulative_proof_target,
388            expected_cumulative_weight,
389            expected_last_coinbase_target,
390            expected_last_coinbase_timestamp,
391        ) = to_next_targets::<N>(
392            N::CONSENSUS_VERSION(height)?,
393            previous_block.cumulative_proof_target(),
394            combined_proof_target,
395            previous_block.coinbase_target(),
396            previous_block.cumulative_weight(),
397            previous_block.last_coinbase_target(),
398            previous_block.last_coinbase_timestamp(),
399            timestamp,
400        )?;
401
402        // Calculate the expected coinbase reward.
403        let expected_coinbase_reward = coinbase_reward::<N>(
404            height,
405            timestamp,
406            N::GENESIS_TIMESTAMP,
407            N::STARTING_SUPPLY,
408            N::REWARD_ANCHOR_TIME,
409            N::ANCHOR_HEIGHT,
410            N::BLOCK_TIME,
411            combined_proof_target,
412            u64::try_from(previous_block.cumulative_proof_target())?,
413            previous_block.coinbase_target(),
414        )?;
415
416        // Calculate the expected transaction fees.
417        let expected_transaction_fees =
418            self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
419
420        // Calculate the time since last block.
421        let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp());
422        // Compute the expected block reward.
423        let expected_block_reward = block_reward::<N>(
424            height,
425            N::STARTING_SUPPLY,
426            N::BLOCK_TIME,
427            time_since_last_block,
428            expected_coinbase_reward,
429            expected_transaction_fees,
430        )?;
431        // Compute the expected puzzle reward.
432        let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward);
433
434        Ok((
435            expected_cumulative_weight,
436            expected_cumulative_proof_target,
437            expected_coinbase_target,
438            expected_proof_target,
439            expected_last_coinbase_target,
440            expected_last_coinbase_timestamp,
441            expected_block_reward,
442            expected_puzzle_reward,
443        ))
444    }
445
446    /// Ensures the block transactions are correct.
447    fn verify_transactions(&self) -> Result<()> {
448        let height = self.height();
449
450        // Ensure the number of transactions is within the allowed range.
451        // This check is redundant if the block has been created via `Block::from()`.
452        if self.transactions.len() > Transactions::<N>::MAX_TRANSACTIONS {
453            bail!(
454                "Cannot validate a block with more than {} confirmed transactions",
455                Transactions::<N>::MAX_TRANSACTIONS
456            );
457        }
458
459        // Ensure the number of aborted transaction IDs is within the allowed range.
460        // This check is redundant if the block has been created via `Block::from()`.
461        if self.aborted_transaction_ids.len() > Transactions::<N>::max_aborted_transactions() {
462            bail!(
463                "Cannot validate a block with more than {} aborted transaction IDs",
464                Transactions::<N>::max_aborted_transactions()
465            );
466        }
467
468        // Ensure there are no duplicate transaction IDs.
469        if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
470            bail!("Found a duplicate transaction in block {height}");
471        }
472
473        // Ensure there are no duplicate transition IDs.
474        if has_duplicates(self.transition_ids()) {
475            bail!("Found a duplicate transition in block {height}");
476        }
477
478        // Ensure there are no duplicate program IDs.
479        if has_duplicates(
480            self.transactions().iter().filter_map(|tx| tx.transaction().deployment().map(|d| d.program_id())),
481        ) {
482            bail!("Found a duplicate program ID in block {height}");
483        }
484
485        /* Input */
486
487        // Ensure there are no duplicate input IDs.
488        if has_duplicates(self.input_ids()) {
489            bail!("Found a duplicate input ID in block {height}");
490        }
491        // Ensure there are no duplicate serial numbers.
492        if has_duplicates(self.serial_numbers()) {
493            bail!("Found a duplicate serial number in block {height}");
494        }
495        // Ensure there are no duplicate tags.
496        if has_duplicates(self.tags()) {
497            bail!("Found a duplicate tag in block {height}");
498        }
499
500        /* Output */
501
502        // Ensure there are no duplicate output IDs.
503        if has_duplicates(self.output_ids()) {
504            bail!("Found a duplicate output ID in block {height}");
505        }
506        // Ensure there are no duplicate commitments.
507        if has_duplicates(self.commitments()) {
508            bail!("Found a duplicate commitment in block {height}");
509        }
510        // Ensure there are no duplicate nonces.
511        if has_duplicates(self.nonces()) {
512            bail!("Found a duplicate nonce in block {height}");
513        }
514
515        /* Metadata */
516
517        // Ensure there are no duplicate transition public keys.
518        if has_duplicates(self.transition_public_keys()) {
519            bail!("Found a duplicate transition public key in block {height}");
520        }
521        // Ensure there are no duplicate transition commitments.
522        if has_duplicates(self.transition_commitments()) {
523            bail!("Found a duplicate transition commitment in block {height}");
524        }
525        Ok(())
526    }
527}
528impl<N: Network> Block<N> {
529    /// Computes the transactions root for the block.
530    fn compute_transactions_root(&self) -> Result<Field<N>> {
531        match self.transactions.to_transactions_root() {
532            Ok(transactions_root) => Ok(transactions_root),
533            Err(error) => bail!("Failed to compute the transactions root for block {} - {error}", self.height()),
534        }
535    }
536
537    /// Computes the finalize root for the block.
538    fn compute_finalize_root(&self, ratified_finalize_operations: Vec<FinalizeOperation<N>>) -> Result<Field<N>> {
539        match self.transactions.to_finalize_root(ratified_finalize_operations) {
540            Ok(finalize_root) => Ok(finalize_root),
541            Err(error) => bail!("Failed to compute the finalize root for block {} - {error}", self.height()),
542        }
543    }
544
545    /// Computes the ratifications root for the block.
546    fn compute_ratifications_root(&self) -> Result<Field<N>> {
547        match self.ratifications.to_ratifications_root() {
548            Ok(ratifications_root) => Ok(ratifications_root),
549            Err(error) => bail!("Failed to compute the ratifications root for block {} - {error}", self.height()),
550        }
551    }
552
553    /// Computes the solutions root for the block.
554    fn compute_solutions_root(&self) -> Result<Field<N>> {
555        self.solutions.to_solutions_root()
556    }
557
558    /// Computes the subdag root for the block.
559    fn compute_subdag_root(&self) -> Result<Field<N>> {
560        match self.authority {
561            Authority::Quorum(ref subdag) => subdag.to_subdag_root(),
562            Authority::Beacon(_) => Ok(Field::zero()),
563        }
564    }
565
566    /// Checks that the transmission IDs in the given subdag matches the solutions and transactions in the block.
567    /// Returns the IDs of the transactions and solutions that should already exist in the ledger.
568    pub(super) fn check_subdag_transmissions(
569        subdag: &Subdag<N>,
570        solutions: &Option<PuzzleSolutions<N>>,
571        aborted_solution_ids: &[SolutionID<N>],
572        transactions: &Transactions<N>,
573        aborted_transaction_ids: &[N::TransactionID],
574    ) -> Result<(Vec<SolutionID<N>>, Vec<N::TransactionID>)> {
575        // Prepare an iterator over the solution IDs.
576        let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
577        // Prepare an iterator over the unconfirmed transactions.
578        let unconfirmed_transactions = cfg_iter!(transactions)
579            .map(|confirmed| confirmed.to_unconfirmed_transaction())
580            .collect::<Result<Vec<_>>>()?;
581        let mut unconfirmed_transactions = unconfirmed_transactions.iter().peekable();
582
583        // Initialize a set of already seen transaction and solution IDs.
584        let mut seen_transaction_ids = HashSet::new();
585        let mut seen_solution_ids = HashSet::new();
586
587        // Initialize a set of aborted or already-existing solution IDs.
588        let mut aborted_or_existing_solution_ids = HashSet::new();
589        // Initialize a set of aborted or already-existing transaction IDs.
590        let mut aborted_or_existing_transaction_ids = HashSet::new();
591
592        // Iterate over the transmission IDs.
593        for transmission_id in subdag.transmission_ids() {
594            // If the transaction or solution ID has already been seen, then continue.
595            // Note: This is done instead of checking `TransmissionID` directly, because we need to
596            // ensure that each transaction or solution ID is unique. The `TransmissionID` is guaranteed
597            // to be unique, however the transaction/solution ID may not be due to malleability concerns.
598            match transmission_id {
599                TransmissionID::Ratification => {}
600                TransmissionID::Solution(solution_id, _) => {
601                    if !seen_solution_ids.insert(solution_id) {
602                        continue;
603                    }
604                }
605                TransmissionID::Transaction(transaction_id, _) => {
606                    if !seen_transaction_ids.insert(transaction_id) {
607                        continue;
608                    }
609                }
610            }
611
612            // Process the transmission ID.
613            match transmission_id {
614                TransmissionID::Ratification => {}
615                TransmissionID::Solution(solution_id, _checksum) => {
616                    match solutions.peek() {
617                        // Check the next solution matches the expected solution ID.
618                        // We don't check against the checksum, because check_solution_mut might mutate the solution.
619                        Some((_, solution)) if solution.id() == *solution_id => {
620                            // Increment the solution iterator.
621                            solutions.next();
622                        }
623                        // Otherwise, add the solution ID to the aborted or existing list.
624                        _ => {
625                            if !aborted_or_existing_solution_ids.insert(*solution_id) {
626                                bail!("Block contains a duplicate aborted solution ID (found '{solution_id}')");
627                            }
628                        }
629                    }
630                }
631                TransmissionID::Transaction(transaction_id, checksum) => {
632                    match unconfirmed_transactions.peek() {
633                        // Check the next transaction matches the expected transaction.
634                        Some(transaction)
635                            if transaction.id() == *transaction_id
636                                && Data::<Transaction<N>>::Buffer(transaction.to_bytes_le()?.into())
637                                    .to_checksum::<N>()?
638                                    == *checksum =>
639                        {
640                            // Increment the unconfirmed transaction iterator.
641                            unconfirmed_transactions.next();
642                        }
643                        // Otherwise, add the transaction ID to the aborted or existing list.
644                        _ => {
645                            if !aborted_or_existing_transaction_ids.insert(*transaction_id) {
646                                bail!("Block contains a duplicate aborted transaction ID (found '{transaction_id}')");
647                            }
648                        }
649                    }
650                }
651            }
652        }
653
654        // Ensure there are no more solutions in the block.
655        ensure!(solutions.next().is_none(), "There exist more solutions than expected.");
656        // Ensure there are no more transactions in the block.
657        ensure!(unconfirmed_transactions.next().is_none(), "There exist more transactions than expected.");
658
659        // Ensure the aborted solution IDs match.
660        for aborted_solution_id in aborted_solution_ids {
661            // If the aborted transaction ID is not found, throw an error.
662            if !aborted_or_existing_solution_ids.contains(aborted_solution_id) {
663                bail!(
664                    "Block contains an aborted solution ID that is not found in the subdag (found '{aborted_solution_id}')"
665                );
666            }
667        }
668        // Ensure the aborted transaction IDs match.
669        for aborted_transaction_id in aborted_transaction_ids {
670            // If the aborted transaction ID is not found, throw an error.
671            if !aborted_or_existing_transaction_ids.contains(aborted_transaction_id) {
672                bail!(
673                    "Block contains an aborted transaction ID that is not found in the subdag (found '{aborted_transaction_id}')"
674                );
675            }
676        }
677
678        // Retrieve the solution IDs that should already exist in the ledger.
679        let existing_solution_ids: Vec<_> = aborted_or_existing_solution_ids
680            .difference(&aborted_solution_ids.iter().copied().collect())
681            .copied()
682            .collect();
683        // Retrieve the transaction IDs that should already exist in the ledger.
684        let existing_transaction_ids: Vec<_> = aborted_or_existing_transaction_ids
685            .difference(&aborted_transaction_ids.iter().copied().collect())
686            .copied()
687            .collect();
688
689        Ok((existing_solution_ids, existing_transaction_ids))
690    }
691}