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 correct.
30    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        // Ensure the block hash is correct.
42        self.verify_hash(previous_block.height(), previous_block.hash())?;
43
44        // Ensure the block authority is correct.
45        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        // Ensure the block solutions are correct.
59        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        // Ensure the block ratifications are correct.
71        self.verify_ratifications(expected_block_reward, expected_puzzle_reward)?;
72
73        // Ensure the block transactions are correct.
74        self.verify_transactions()?;
75
76        // Set the expected previous state root.
77        let expected_previous_state_root = current_state_root;
78        // Compute the expected transactions root.
79        let expected_transactions_root = self.compute_transactions_root()?;
80        // Compute the expected finalize root.
81        let expected_finalize_root = self.compute_finalize_root(ratified_finalize_operations)?;
82        // Compute the expected ratifications root.
83        let expected_ratifications_root = self.compute_ratifications_root()?;
84        // Compute the expected solutions root.
85        let expected_solutions_root = self.compute_solutions_root()?;
86        // Compute the expected subdag root.
87        let expected_subdag_root = self.compute_subdag_root()?;
88
89        // Ensure the block header is correct.
90        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        // Return the expected existing solution IDs and transaction IDs.
110        Ok((expected_existing_solution_ids, expected_existing_transaction_ids))
111    }
112}
113
114impl<N: Network> Block<N> {
115    /// Ensures the block hash is correct.
116    fn verify_hash(&self, previous_height: u32, previous_hash: N::BlockHash) -> Result<(), Error> {
117        // Determine the expected height.
118        let expected_height = previous_height.saturating_add(1);
119
120        // Ensure the previous block hash matches.
121        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        // Compute the Merkle root of the block header.
129        let Ok(header_root) = self.header.to_root() else {
130            bail!("Failed to compute the Merkle root of the block header");
131        };
132        // Compute the block hash.
133        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 the block hash matches.
138        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        // Return success.
145        Ok(())
146    }
147
148    /// Ensures the block authority is correct.
149    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        // Note: Do not remove this. This ensures that all blocks after genesis are quorum blocks.
157        #[cfg(not(any(test, feature = "test")))]
158        ensure!(self.authority.is_quorum(), "The next block must be a quorum block");
159
160        // Determine the expected height.
161        let expected_height = previous_height.saturating_add(1);
162
163        // Determine the expected round.
164        let expected_round = match &self.authority {
165            // Beacon blocks increment the previous block round by 1.
166            Authority::Beacon(..) => previous_round.saturating_add(1),
167            // Quorum blocks use the subdag anchor round.
168            Authority::Quorum(subdag) => {
169                // Ensure the subdag anchor round is after the previous block round.
170                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                // Ensure that the rounds in the subdag are sequential.
178                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                // Output the subdag anchor round.
187                subdag.anchor_round()
188            }
189        };
190        // Ensure the block round minus the committee lookback range is at least the starting round of the committee lookback.
191        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        // Ensure the block authority is correct.
200        // Determine the solution IDs and transaction IDs that are expected to be in previous blocks.
201        let (expected_existing_solution_ids, expected_existing_transaction_ids) = match &self.authority {
202            Authority::Beacon(signature) => {
203                // Retrieve the signer.
204                let signer = signature.to_address();
205                // Ensure the block is signed by a committee member.
206                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 the signature is valid.
211                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                // Compute the expected leader.
220                let expected_leader = current_committee_lookback.get_leader(expected_round)?;
221                // Ensure the block is authored by the expected leader.
222                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                // Ensure the transmission IDs from the subdag correspond to the block.
228                // This is redundant if the block has been created via `Block::from()`;
229                // however, we need to obtain the solution and transaction IDs here,
230                // so may want to remove the redundant check in `Block::from()` and leave it here.
231                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        // Determine the expected timestamp.
242        let expected_timestamp = match &self.authority {
243            // Beacon blocks do not have a timestamp check.
244            Authority::Beacon(..) => self.timestamp(),
245            // Quorum blocks use the weighted median timestamp from the subdag.
246            Authority::Quorum(subdag) => subdag.timestamp(previous_committee_lookback),
247        };
248
249        // Check that the committee IDs are correct.
250        if let Authority::Quorum(subdag) = &self.authority {
251            // Check that the committee ID of the leader certificate is correct.
252            ensure!(
253                subdag.leader_certificate().committee_id() == current_committee_lookback.id(),
254                "Leader certificate has an incorrect committee ID"
255            );
256
257            // Check that all certificates on each round have the same committee ID.
258            cfg_iter!(subdag).try_for_each(|(round, certificates)| {
259                // Check that every certificate for a given round shares the same committee ID.
260                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        // Return success.
273        Ok((
274            expected_round,
275            expected_height,
276            expected_timestamp,
277            expected_existing_solution_ids,
278            expected_existing_transaction_ids,
279        ))
280    }
281
282    /// Ensures the block ratifications are correct.
283    fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
284        let height = self.height();
285
286        // Ensure there are sufficient ratifications.
287        ensure!(self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
288
289        // Initialize a ratifications iterator.
290        let mut ratifications_iter = self.ratifications.iter();
291
292        // Retrieve the block reward from the first block ratification.
293        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        // Retrieve the puzzle reward from the second block ratification.
298        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 the block reward is correct.
304        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 the puzzle reward is correct.
309        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    /// Ensures the block solutions are correct.
317    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 the number of solutions is within the allowed range.
327        // This check is redundant if the block has been created via `Block::from()`.
328        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 the number of aborted solution IDs is within the allowed range.
336        // This check is redundant if the block has been created via `Block::from()`.
337        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        // Ensure there are no duplicate solution IDs.
344        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        // Compute the combined proof target.
356        let combined_proof_target = match self.solutions.deref() {
357            Some(solutions) => current_puzzle.get_combined_proof_target(solutions)?,
358            None => 0u128,
359        };
360
361        // Verify the solutions.
362        if let Some(coinbase) = self.solutions.deref() {
363            // Ensure the puzzle proof is valid.
364            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            // Ensure that the block cumulative proof target is less than the previous block's coinbase target.
370            // Note: This is a sanity check, as the cumulative proof target resets to 0 if the
371            // coinbase target was reached in this block.
372            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        // Calculate the next coinbase targets and timestamps.
378        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        // Calculate the expected coinbase reward.
396        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        // Calculate the expected transaction fees.
410        let expected_transaction_fees =
411            self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
412
413        // Calculate the time since last block.
414        let time_since_last_block = timestamp.saturating_sub(previous_block.timestamp());
415        // Compute the expected block reward.
416        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        // Compute the expected puzzle reward.
425        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    /// Ensures the block transactions are correct.
440    fn verify_transactions(&self) -> Result<()> {
441        let height = self.height();
442
443        // Ensure the number of transactions is within the allowed range.
444        // This check is redundant if the block has been created via `Block::from()`.
445        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        // Ensure the number of aborted transaction IDs is within the allowed range.
453        // This check is redundant if the block has been created via `Block::from()`.
454        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        // Ensure there are no duplicate transaction IDs.
462        if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
463            bail!("Found a duplicate transaction in block {height}");
464        }
465
466        // Ensure there are no duplicate transition IDs.
467        if has_duplicates(self.transition_ids()) {
468            bail!("Found a duplicate transition in block {height}");
469        }
470
471        // Ensure there are no duplicate program IDs.
472        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        /* Input */
479
480        // Ensure there are no duplicate input IDs.
481        if has_duplicates(self.input_ids()) {
482            bail!("Found a duplicate input ID in block {height}");
483        }
484        // Ensure there are no duplicate serial numbers.
485        if has_duplicates(self.serial_numbers()) {
486            bail!("Found a duplicate serial number in block {height}");
487        }
488        // Ensure there are no duplicate tags.
489        if has_duplicates(self.tags()) {
490            bail!("Found a duplicate tag in block {height}");
491        }
492
493        /* Output */
494
495        // Ensure there are no duplicate output IDs.
496        if has_duplicates(self.output_ids()) {
497            bail!("Found a duplicate output ID in block {height}");
498        }
499        // Ensure there are no duplicate commitments.
500        if has_duplicates(self.commitments()) {
501            bail!("Found a duplicate commitment in block {height}");
502        }
503        // Ensure there are no duplicate nonces.
504        if has_duplicates(self.nonces()) {
505            bail!("Found a duplicate nonce in block {height}");
506        }
507
508        /* Metadata */
509
510        // Ensure there are no duplicate transition public keys.
511        if has_duplicates(self.transition_public_keys()) {
512            bail!("Found a duplicate transition public key in block {height}");
513        }
514        // Ensure there are no duplicate transition commitments.
515        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    /// Computes the transactions root for the block.
523    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    /// Computes the finalize root for the block.
531    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    /// Computes the ratifications root for the block.
539    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    /// Computes the solutions root for the block.
547    fn compute_solutions_root(&self) -> Result<Field<N>> {
548        self.solutions.to_solutions_root()
549    }
550
551    /// Computes the subdag root for the block.
552    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    /// Checks that the transmission IDs in the given subdag matches the solutions and transactions in the block.
560    /// Returns the IDs of the transactions and solutions that should already exist in the ledger.
561    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        // Prepare an iterator over the solution IDs.
569        let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
570        // Prepare an iterator over the unconfirmed transactions.
571        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        // Initialize a set of already seen transaction and solution IDs.
577        let mut seen_transaction_ids = HashSet::new();
578        let mut seen_solution_ids = HashSet::new();
579
580        // Initialize a set of aborted or already-existing solution IDs.
581        let mut aborted_or_existing_solution_ids = HashSet::new();
582        // Initialize a set of aborted or already-existing transaction IDs.
583        let mut aborted_or_existing_transaction_ids = HashSet::new();
584
585        // Iterate over the transmission IDs.
586        for transmission_id in subdag.transmission_ids() {
587            // If the transaction or solution ID has already been seen, then continue.
588            // Note: This is done instead of checking `TransmissionID` directly, because we need to
589            // ensure that each transaction or solution ID is unique. The `TransmissionID` is guaranteed
590            // to be unique, however the transaction/solution ID may not be due to malleability concerns.
591            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            // Process the transmission ID.
606            match transmission_id {
607                TransmissionID::Ratification => {}
608                TransmissionID::Solution(solution_id, _checksum) => {
609                    match solutions.peek() {
610                        // Check the next solution matches the expected solution ID.
611                        // We don't check against the checksum, because check_solution_mut might mutate the solution.
612                        Some((_, solution)) if solution.id() == *solution_id => {
613                            // Increment the solution iterator.
614                            solutions.next();
615                        }
616                        // Otherwise, add the solution ID to the aborted or existing list.
617                        _ => {
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                        // Check the next transaction matches the expected transaction.
627                        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                            // Increment the unconfirmed transaction iterator.
634                            unconfirmed_transactions.next();
635                        }
636                        // Otherwise, add the transaction ID to the aborted or existing list.
637                        _ => {
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 there are no more solutions in the block.
648        ensure!(solutions.next().is_none(), "There exist more solutions than expected.");
649        // Ensure there are no more transactions in the block.
650        ensure!(unconfirmed_transactions.next().is_none(), "There exist more transactions than expected.");
651
652        // Ensure the aborted solution IDs match.
653        for aborted_solution_id in aborted_solution_ids {
654            // If the aborted transaction ID is not found, throw an error.
655            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        // Ensure the aborted transaction IDs match.
662        for aborted_transaction_id in aborted_transaction_ids {
663            // If the aborted transaction ID is not found, throw an error.
664            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        // Retrieve the solution IDs that should already exist in the ledger.
672        let existing_solution_ids: Vec<_> = aborted_or_existing_solution_ids
673            .difference(&aborted_solution_ids.iter().copied().collect())
674            .copied()
675            .collect();
676        // Retrieve the transaction IDs that should already exist in the ledger.
677        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}