Skip to main content

solana_runtime/
block_component_processor.rs

1use {
2    crate::{
3        bank::Bank,
4        block_component_processor::vote_reward::{
5            CalcVoteRewardUpdateVoteStatesError, calc_vote_rewards_update_vote_states,
6        },
7        leader_schedule_utils::leader_slot_index,
8        validated_block_finalization::{
9            BlockFinalizationCertError, ValidatedBlockFinalizationCert,
10        },
11        validated_reward_certificate::{Error as ValidatedRewardCertError, ValidatedRewardCert},
12    },
13    agave_transaction_view::transaction_data::TransactionData,
14    agave_votor_messages::{
15        certificate::{CertSignature, Certificate, CertificateType, GenesisCert},
16        consensus_message::Block,
17        migration::MigrationStatus,
18        unverified_vote_message::UnverifiedCertificate,
19    },
20    crossbeam_channel::{Sender, TrySendError},
21    log::*,
22    smallvec::{SmallVec, smallvec},
23    solana_clock::Slot,
24    solana_entry::{
25        block_component::{
26            BlockFooterV1, BlockMarkerV1, GenesisCertBlockMarker, VersionedBlockFooter,
27            VersionedBlockHeader, VersionedBlockMarker, VersionedUpdateParent,
28        },
29        entry::EntryView,
30    },
31    solana_hash::Hash,
32    solana_pubkey::Pubkey,
33    std::{collections::HashSet, sync::Arc},
34    thiserror::Error,
35};
36
37pub(crate) mod vote_reward;
38
39#[derive(Debug, Error)]
40pub enum BankFooterError {
41    #[error("calc vote rewards updating vote states failed with \"{0}\"")]
42    CalcVoteRewardUpdateVoteStates(#[from] CalcVoteRewardUpdateVoteStatesError),
43}
44
45#[derive(Debug, Error)]
46pub enum BlockComponentProcessorError {
47    #[error("BlockComponent detected pre-migration")]
48    BlockComponentPreMigration,
49    #[error("GenesisCertificate marker detected when GenesisCertificate is already populated")]
50    GenesisCertificateAlreadyPopulated,
51    #[error("GenesisCertificate marker detected when the cluster has Alpenglow enabled at slot 0")]
52    GenesisCertificateInAlpenglowCluster,
53    #[error("GenesisCertificate marker detected on a block which is not a child of genesis")]
54    GenesisCertificateOnNonChild,
55    #[error("GenesisCertificate was invalid and failed to verify")]
56    GenesisCertificateFailedVerification,
57    #[error("Alpenglow migration became ready; aborting the TowerBFT bank")]
58    AlpenglowMigrationTransition,
59    #[error("GenesisCertificate marker must immediately follow the block header")]
60    GenesisCertificateOutOfOrder,
61    #[error("FinalizationCertificate was invalid or failed to verify {0}")]
62    InvalidFinalizationCertificate(#[from] BlockFinalizationCertError),
63    #[error("Missing block footer")]
64    MissingBlockFooter,
65    #[error("Missing genesis certificate marker")]
66    MissingGenesisCertificateMarker,
67    #[error("Missing parent marker (neither a header nor an update parent was present)")]
68    MissingParentMarker,
69    #[error("Entry batch detected after block footer")]
70    EntryBatchAfterBlockFooter,
71    #[error("Alpentick must be the final block component and appear after block footer")]
72    InvalidAlpentickPosition,
73    #[error("Multiple block footers detected")]
74    MultipleBlockFooters,
75    #[error("Multiple block headers detected")]
76    MultipleBlockHeaders,
77    #[error(
78        "Block header parent slot mismatch: header={header_parent_slot}, bank={bank_parent_slot}"
79    )]
80    HeaderParentSlotMismatch {
81        header_parent_slot: Slot,
82        bank_parent_slot: Slot,
83    },
84    #[error("Multiple update parents detected")]
85    MultipleUpdateParents,
86    #[error("Nanosecond clock out of bounds")]
87    NanosecondClockOutOfBounds,
88    #[error("Spurious update parent")]
89    SpuriousUpdateParent,
90    #[error("UpdateParent marker is only valid in the first slot of a leader window: slot {0}")]
91    UpdateParentNotFirstInLeaderWindow(Slot),
92    #[error(
93        "UpdateParent cannot be the initial parent marker unless replay starts at UpdateParent"
94    )]
95    UnexpectedInitialUpdateParent,
96    #[error("Abandoned bank")]
97    AbandonedBank(VersionedUpdateParent),
98    #[error("invalid reward certs {0}")]
99    InvalidRewardCerts(#[from] ValidatedRewardCertError),
100    #[error("updating bank footer failed with \"{0}\"")]
101    UpdateBankFooter(#[from] BankFooterError),
102}
103
104impl BlockComponentProcessorError {
105    /// Returns whether this error can come from an optimistic-parent prefix
106    /// that a later usable `UpdateParent` makes obsolete.
107    ///
108    /// This only determines soft-dead eligibility. Replay also verifies that
109    /// the failure occurred before the `UpdateParent`.
110    pub fn is_update_parent_recoverable_replay_error(&self) -> bool {
111        match self {
112            BlockComponentProcessorError::MissingParentMarker
113            | BlockComponentProcessorError::EntryBatchAfterBlockFooter
114            | BlockComponentProcessorError::InvalidAlpentickPosition
115            | BlockComponentProcessorError::MultipleBlockFooters
116            | BlockComponentProcessorError::MultipleBlockHeaders
117            | BlockComponentProcessorError::HeaderParentSlotMismatch { .. }
118            | BlockComponentProcessorError::NanosecondClockOutOfBounds
119            | BlockComponentProcessorError::UnexpectedInitialUpdateParent
120            | BlockComponentProcessorError::GenesisCertificateOutOfOrder
121            | BlockComponentProcessorError::GenesisCertificateAlreadyPopulated
122            | BlockComponentProcessorError::GenesisCertificateInAlpenglowCluster
123            | BlockComponentProcessorError::GenesisCertificateOnNonChild
124            | BlockComponentProcessorError::GenesisCertificateFailedVerification
125            | BlockComponentProcessorError::SpuriousUpdateParent
126            | BlockComponentProcessorError::AbandonedBank(_)
127            | BlockComponentProcessorError::InvalidRewardCerts(_)
128            | BlockComponentProcessorError::UpdateBankFooter(_)
129            | BlockComponentProcessorError::InvalidFinalizationCertificate(_) => true,
130            BlockComponentProcessorError::BlockComponentPreMigration
131            | BlockComponentProcessorError::MissingBlockFooter
132            | BlockComponentProcessorError::MissingGenesisCertificateMarker
133            | BlockComponentProcessorError::MultipleUpdateParents
134            | BlockComponentProcessorError::AlpenglowMigrationTransition
135            | BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(_) => false,
136        }
137    }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141/// The parent marker that established the current entry section.
142enum EntryParentMarker {
143    BlockHeader,
144    UpdateParent,
145}
146
147#[derive(Default, Debug, Clone, PartialEq, Eq)]
148/// The stage within the block we are currently in
149///
150/// All blocks MUST follow this exact shape
151///
152/// Header
153/// Optional Genesis marker - this is the only valid position for a genesis marker
154/// 0 or more Entries
155/// Optional UpdateParent
156/// 0 or more Entries
157/// Footer
158/// Alpentick
159///
160/// Block component processing can start either from the header
161/// or from the UpdateParent.
162enum BlockComponentStage {
163    #[default]
164    /// Beginning of the block, can only accept a parent marker
165    PreParentMarker,
166    /// Immediately after the header, can accept genesis marker, entries or footer
167    AcceptingGenesisOrEntries,
168    /// During the entries section, can accept entries or the footer
169    /// If the parent marker was a block header, can also accept an UpdateParent marker
170    AcceptingEntriesOrFooter { parent_marker: EntryParentMarker },
171    /// After the footer, can only accept the alpentick
172    AcceptingAlpentick,
173    /// After the alpentick, nothing more is accepted
174    Done,
175}
176
177impl BlockComponentStage {
178    /// If current stage is `PreParentMarker`, transition to `AcceptingGenesisOrEntries`
179    fn on_header(&mut self) -> Result<(), BlockComponentProcessorError> {
180        match self {
181            Self::PreParentMarker => {
182                *self = Self::AcceptingGenesisOrEntries;
183                Ok(())
184            }
185            Self::AcceptingGenesisOrEntries
186            | Self::AcceptingEntriesOrFooter {
187                parent_marker: EntryParentMarker::BlockHeader,
188            }
189            | Self::AcceptingAlpentick
190            | Self::Done => Err(BlockComponentProcessorError::MultipleBlockHeaders),
191            Self::AcceptingEntriesOrFooter {
192                parent_marker: EntryParentMarker::UpdateParent,
193            } => Err(BlockComponentProcessorError::SpuriousUpdateParent),
194        }
195    }
196
197    /// If current stage is `AcceptingGenesisOrEntries`, transition to `AcceptingEntriesOrFooter`
198    fn on_genesis_certificate(&mut self) -> Result<(), BlockComponentProcessorError> {
199        match self {
200            Self::PreParentMarker => Err(BlockComponentProcessorError::MissingParentMarker),
201            Self::AcceptingGenesisOrEntries => {
202                *self = Self::AcceptingEntriesOrFooter {
203                    parent_marker: EntryParentMarker::BlockHeader,
204                };
205                Ok(())
206            }
207            Self::AcceptingEntriesOrFooter { .. } | Self::AcceptingAlpentick | Self::Done => {
208                Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder)
209            }
210        }
211    }
212
213    /// If current stage is `AcceptingGenesisOrEntries` or `AcceptingEntriesOrFooter`, transition to
214    /// `AcceptingEntriesOrFooter`
215    fn on_entry_batch(&mut self) -> Result<(), BlockComponentProcessorError> {
216        match self {
217            Self::PreParentMarker => Err(BlockComponentProcessorError::MissingParentMarker),
218            Self::AcceptingGenesisOrEntries => {
219                *self = Self::AcceptingEntriesOrFooter {
220                    parent_marker: EntryParentMarker::BlockHeader,
221                };
222                Ok(())
223            }
224            Self::AcceptingEntriesOrFooter { .. } => Ok(()),
225            Self::AcceptingAlpentick | Self::Done => {
226                Err(BlockComponentProcessorError::EntryBatchAfterBlockFooter)
227            }
228        }
229    }
230
231    /// If current stage is `AcceptingGenesisOrEntries` or `AcceptingEntriesOrFooter`, return `AbandonedBank`
232    /// If current stage is `PreParentMarker` and `allow_initial_update_parent` is specified,
233    /// transition to `AcceptingEntriesOrFooter` with `EntryParentMarker::UpdateParent`
234    fn on_update_parent(
235        &mut self,
236        update_parent: &VersionedUpdateParent,
237        allow_initial_update_parent: bool,
238    ) -> Result<(), BlockComponentProcessorError> {
239        match self {
240            Self::PreParentMarker => {
241                if !allow_initial_update_parent {
242                    return Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent);
243                }
244                *self = Self::AcceptingEntriesOrFooter {
245                    parent_marker: EntryParentMarker::UpdateParent,
246                };
247                Ok(())
248            }
249            Self::AcceptingGenesisOrEntries
250            | Self::AcceptingEntriesOrFooter {
251                parent_marker: EntryParentMarker::BlockHeader,
252            } => {
253                // Only an error in the sense that replay execution of this block
254                // prefix is now over. Replay execution can continue after resetting
255                // bank.
256                Err(BlockComponentProcessorError::AbandonedBank(
257                    update_parent.clone(),
258                ))
259            }
260            Self::AcceptingEntriesOrFooter {
261                parent_marker: EntryParentMarker::UpdateParent,
262            } => Err(BlockComponentProcessorError::MultipleUpdateParents),
263            Self::AcceptingAlpentick | BlockComponentStage::Done => {
264                Err(BlockComponentProcessorError::SpuriousUpdateParent)
265            }
266        }
267    }
268
269    /// If the current stage is `AcceptingGenesisOrEntries`, `AcceptingEntriesOrFooter`
270    /// transition to `AcceptingAlpentick`
271    fn on_footer(&mut self) -> Result<(), BlockComponentProcessorError> {
272        match self {
273            Self::PreParentMarker => Err(BlockComponentProcessorError::MissingParentMarker),
274            Self::AcceptingGenesisOrEntries | Self::AcceptingEntriesOrFooter { .. } => {
275                *self = Self::AcceptingAlpentick;
276                Ok(())
277            }
278            Self::AcceptingAlpentick | Self::Done => {
279                Err(BlockComponentProcessorError::MultipleBlockFooters)
280            }
281        }
282    }
283
284    /// If stage is `AcceptingAlpentick`, transition to `Done`
285    fn on_alpentick(&mut self) -> Result<(), BlockComponentProcessorError> {
286        match self {
287            Self::PreParentMarker => Err(BlockComponentProcessorError::MissingParentMarker),
288            Self::AcceptingGenesisOrEntries => {
289                Err(BlockComponentProcessorError::InvalidAlpentickPosition)
290            }
291            Self::AcceptingEntriesOrFooter { .. } => {
292                Err(BlockComponentProcessorError::InvalidAlpentickPosition)
293            }
294            Self::AcceptingAlpentick => {
295                *self = Self::Done;
296                Ok(())
297            }
298            Self::Done => Err(BlockComponentProcessorError::InvalidAlpentickPosition),
299        }
300    }
301
302    /// Return `Ok(())` only if the stage is `Done`
303    fn on_final(&self) -> Result<(), BlockComponentProcessorError> {
304        match self {
305            Self::Done => Ok(()),
306            Self::AcceptingAlpentick => Err(BlockComponentProcessorError::InvalidAlpentickPosition),
307            Self::PreParentMarker
308            | Self::AcceptingGenesisOrEntries
309            | Self::AcceptingEntriesOrFooter { .. } => {
310                Err(BlockComponentProcessorError::MissingBlockFooter)
311            }
312        }
313    }
314}
315
316#[derive(Default)]
317pub struct BlockComponentProcessor {
318    stage: BlockComponentStage,
319    has_genesis_certificate_marker: bool,
320}
321
322impl BlockComponentProcessor {
323    pub fn on_final(
324        &self,
325        migration_status: &MigrationStatus,
326        slot: Slot,
327        parent_slot: Slot,
328    ) -> Result<(), BlockComponentProcessorError> {
329        // Only allow block markers for slots where they should be present.
330        // TowerBFT blocks must not include block headers.
331        if !migration_status.should_allow_block_markers(slot) {
332            if self.stage == BlockComponentStage::PreParentMarker {
333                return Ok(());
334            } else {
335                return Err(BlockComponentProcessorError::BlockComponentPreMigration);
336            };
337        }
338
339        if Self::requires_genesis_certificate_marker(migration_status, parent_slot)
340            && !self.has_genesis_certificate_marker
341        {
342            return Err(BlockComponentProcessorError::MissingGenesisCertificateMarker);
343        }
344
345        self.stage.on_final()
346    }
347
348    /// Check if `parent_slot` is the alpenglow genesis block for use in enforcing
349    /// that the block has a genesis block marker
350    ///
351    /// Note: We have an exemption for Dev clusters that have alpenglow active at slot 0,
352    /// as these clusters do not need a genesis block marker
353    fn requires_genesis_certificate_marker(
354        migration_status: &MigrationStatus,
355        parent_slot: Slot,
356    ) -> bool {
357        migration_status
358            .genesis_block()
359            .is_some_and(|genesis_block| {
360                genesis_block.slot != 0 && parent_slot == genesis_block.slot
361            })
362    }
363
364    /// Process an entry batch.
365    ///
366    /// Validates that a parent marker (header or update parent) has been processed
367    /// before any entry batches. The terminal Alpenglow tick is the only entry
368    /// batch allowed after the block footer.
369    pub fn on_entry_batch<D: TransactionData>(
370        &mut self,
371        migration_status: &MigrationStatus,
372        slot: Slot,
373        entries: &[EntryView<D>],
374        is_final_component: bool,
375    ) -> Result<(), BlockComponentProcessorError> {
376        if !migration_status.should_allow_block_markers(slot) {
377            return Ok(());
378        }
379
380        // The alpentick must be the final block component.
381        // It is fine for other ticks to be present in the block, they will be rejected
382        // for `TooManyTicks` in `verify_ticks()`
383        let is_alpentick = is_final_component
384            && matches!(entries, [entry] if entry.is_tick() && entry.num_hashes == 1);
385
386        if is_alpentick {
387            self.stage.on_alpentick()
388        } else {
389            self.stage.on_entry_batch()
390        }
391    }
392
393    /// Process a block marker:
394    /// - Pre migration, no block markers are allowed
395    /// - During the migration only header and genesis certificate are allowed:
396    ///     - This is in case our node was slow in observing the completion of the migration
397    ///     - By seeing the first alpenglow block, we can advance the migration phase
398    /// - Once the migration is complete all markers are allowed
399    pub fn on_marker(
400        &mut self,
401        bank: Arc<Bank>,
402        parent_bank: Arc<Bank>,
403        shred_version: u16,
404        marker: VersionedBlockMarker,
405        allow_initial_update_parent: bool,
406        finalization_cert_sender: Option<&Sender<SmallVec<[Certificate; 2]>>>,
407        migration_status: &MigrationStatus,
408    ) -> Result<(), BlockComponentProcessorError> {
409        let slot = bank.slot();
410        let VersionedBlockMarker::V1(marker) = marker;
411
412        let markers_fully_enabled = migration_status.should_allow_block_markers(slot);
413        let in_migration = migration_status.is_in_migration();
414        let fast_leader_handover_active =
415            bank.feature_set.snapshot().alpenglow_fast_leader_handover;
416
417        match marker {
418            // Header and genesis cert can be processed either:
419            // - once migration is fully enabled, or
420            // - while we're still in the migration phase (to let us advance it)
421            BlockMarkerV1::BlockHeader(header) if markers_fully_enabled || in_migration => {
422                self.on_header(header.inner(), bank.parent_slot())
423            }
424            BlockMarkerV1::GenesisCertificate(genesis_cert_block_marker)
425                if markers_fully_enabled || in_migration =>
426            {
427                self.on_genesis_cert_block_marker(
428                    bank,
429                    shred_version,
430                    genesis_cert_block_marker.into_inner(),
431                    migration_status,
432                )
433            }
434
435            // Everything else is only valid once migration is complete
436            BlockMarkerV1::BlockFooter(footer) if markers_fully_enabled => self.on_footer(
437                &migration_status.my_pubkey(),
438                bank,
439                parent_bank,
440                shred_version,
441                footer.into_inner(),
442                finalization_cert_sender,
443            ),
444
445            BlockMarkerV1::UpdateParent(update_parent) if markers_fully_enabled => {
446                if fast_leader_handover_active {
447                    self.on_update_parent(slot, update_parent.inner(), allow_initial_update_parent)
448                } else {
449                    Err(BlockComponentProcessorError::SpuriousUpdateParent)
450                }
451            }
452
453            // Any other combination means we saw a marker too early
454            _ => Err(BlockComponentProcessorError::BlockComponentPreMigration),
455        }
456    }
457
458    /// Processes the genesis block marker with full verification
459    pub fn on_genesis_cert_block_marker(
460        &mut self,
461        bank: Arc<Bank>,
462        shred_version: u16,
463        genesis_block_marker: GenesisCertBlockMarker,
464        migration_status: &MigrationStatus,
465    ) -> Result<(), BlockComponentProcessorError> {
466        self.stage.on_genesis_certificate()?;
467        self.process_unvalidated_genesis_cert_block_marker(
468            bank,
469            genesis_block_marker,
470            migration_status,
471            Some(shred_version),
472        )?;
473        Ok(())
474    }
475
476    /// Processes a locally produced genesis certificate marker without verification
477    pub fn on_genesis_cert_block_marker_leader(
478        &mut self,
479        bank: Arc<Bank>,
480        genesis_block_marker: GenesisCertBlockMarker,
481        migration_status: &MigrationStatus,
482    ) -> Result<(), BlockComponentProcessorError> {
483        self.process_unvalidated_genesis_cert_block_marker(
484            bank,
485            genesis_block_marker,
486            migration_status,
487            None,
488        )?;
489        Ok(())
490    }
491
492    /// Performs verification if `shred_version` is specified
493    fn process_unvalidated_genesis_cert_block_marker(
494        &mut self,
495        bank: Arc<Bank>,
496        genesis_block_marker: GenesisCertBlockMarker,
497        migration_status: &MigrationStatus,
498        shred_version: Option<u16>,
499    ) -> Result<(), BlockComponentProcessorError> {
500        // Genesis Certificate is only allowed for direct child of genesis
501        if bank.parent_slot() == 0 {
502            return Err(BlockComponentProcessorError::GenesisCertificateInAlpenglowCluster);
503        }
504
505        let parent_block_id = bank
506            .parent_block_id()
507            .expect("Block id is populated for all slots > 0");
508        if (bank.parent_slot(), parent_block_id)
509            != (
510                genesis_block_marker.slot,
511                genesis_block_marker.block_id.to_hash(),
512            )
513        {
514            return Err(BlockComponentProcessorError::GenesisCertificateOnNonChild);
515        }
516
517        if bank.get_alpenglow_genesis_certificate().is_some() {
518            return Err(BlockComponentProcessorError::GenesisCertificateAlreadyPopulated);
519        }
520
521        let genesis_cert = GenesisCert {
522            block: Block {
523                slot: genesis_block_marker.slot,
524                block_id: genesis_block_marker.block_id,
525            },
526            signature: CertSignature {
527                signature: genesis_block_marker.bls_signature,
528                bitmap: genesis_block_marker.bitmap,
529            },
530        };
531        if let Some(shred_version) = shred_version {
532            Self::verify_genesis_certificate(&bank, &genesis_cert, shred_version)?;
533        }
534
535        bank.set_alpenglow_genesis_certificate(&genesis_cert);
536        self.has_genesis_certificate_marker = true;
537
538        if migration_status.is_alpenglow_enabled() {
539            // We participated in the migration, nothing to do
540            bank.set_hashes_per_tick(None);
541            return Ok(());
542        }
543
544        // We missed the migration however we ingested the first alpenglow block.
545        // This is either a result of startup replay, or in some weird cases steady state replay after a network partition.
546        // Either way we ingest the genesis block details moving us to `ReadyToEnable`.
547        // Since this is a direct child of genesis, and we are replaying, we know we have frozen the genesis block.
548        // Then `load_frozen_forks` or `replay_stage` will take care of the rest.
549        warn!(
550            "{}: Alpenglow genesis marker processed during replay of {}. Transitioning Alpenglow \
551             to ReadyToEnable",
552            migration_status.my_pubkey(),
553            bank.slot()
554        );
555        migration_status.set_genesis_block(genesis_cert.block);
556        migration_status.set_genesis_certificate(Arc::new(genesis_cert));
557        assert!(migration_status.is_ready_to_enable());
558
559        // This bank was created with TowerBFT tick configuration. Stop processing it immediately;
560        // replay will discard it, enable Alpenglow, and rebuild it with Alpenglow tick rules.
561        Err(BlockComponentProcessorError::AlpenglowMigrationTransition)
562    }
563
564    fn verify_genesis_certificate(
565        bank: &Bank,
566        cert: &GenesisCert,
567        shred_version: u16,
568    ) -> Result<(), BlockComponentProcessorError> {
569        let cert_slot = cert.block.slot;
570        let unverified_cert = UnverifiedCertificate {
571            cert_type: CertificateType::Genesis(cert.block),
572            signature: cert.signature.signature,
573            bitmap: cert.signature.bitmap.clone(),
574            shred_version,
575        };
576        bank.verify_certificate(unverified_cert).map_err(|_| {
577            warn!(
578                "Failed to verify genesis certificate for slot {cert_slot} in bank slot {}",
579                bank.slot()
580            );
581            BlockComponentProcessorError::GenesisCertificateFailedVerification
582        })?;
583
584        Ok(())
585    }
586
587    fn on_footer(
588        &mut self,
589        my_pubkey: &Pubkey,
590        bank: Arc<Bank>,
591        parent_bank: Arc<Bank>,
592        shred_version: u16,
593        footer: VersionedBlockFooter,
594        finalization_cert_sender: Option<&Sender<SmallVec<[Certificate; 2]>>>,
595    ) -> Result<(), BlockComponentProcessorError> {
596        self.stage.on_footer()?;
597
598        let VersionedBlockFooter::V1(footer) = footer;
599
600        Self::enforce_nanosecond_clock_bounds(&bank, &parent_bank, &footer)?;
601
602        let BlockFooterV1 {
603            bank_hash,
604            block_producer_time_nanos,
605            block_user_agent: _,
606            block_final_cert,
607            skip_reward_cert,
608            notar_reward_cert,
609        } = footer;
610
611        let reward_cert = ValidatedRewardCert::try_new(
612            &bank,
613            shred_version,
614            &skip_reward_cert,
615            &notar_reward_cert,
616        )?;
617        let block_producer_time_nanos =
618            Self::block_producer_time_nanos_as_i64(block_producer_time_nanos)?;
619        let final_cert = block_final_cert
620            .map(|final_cert| {
621                ValidatedBlockFinalizationCert::try_from_footer(final_cert, &bank, shred_version)
622                    .map_err(BlockComponentProcessorError::InvalidFinalizationCertificate)
623            })
624            .transpose()?;
625
626        let (footer_input, pool_input) = match final_cert {
627            None => (None, None),
628            Some(cert) => {
629                let (signers, finalize_cert, notarize_cert) = cert.into_parts();
630                let final_slot = finalize_cert.cert_type.slot();
631                (
632                    Some((signers, final_slot)),
633                    Some((finalize_cert, notarize_cert)),
634                )
635            }
636        };
637
638        Self::update_bank_with_footer_fields(
639            &bank,
640            block_producer_time_nanos,
641            Some(bank_hash),
642            reward_cert,
643            footer_input
644                .as_ref()
645                .map(|(validators, slot)| (validators, *slot)),
646        )?;
647
648        // Send finalization cert(s) to consensus pool
649        if let Some((finalize_cert, notarize_cert)) = pool_input
650            && let Some(sender) = finalization_cert_sender
651        {
652            let channel_name = "finalization_cert_sender";
653            let certs = match notarize_cert {
654                None => smallvec![finalize_cert],
655                Some(c) => smallvec![finalize_cert, c],
656            };
657            match sender.try_send(certs) {
658                Ok(()) => (),
659                Err(TrySendError::Full(_)) => {
660                    warn!("{my_pubkey}: channel \"{channel_name}\" is full, dropping msg")
661                }
662                Err(TrySendError::Disconnected(_)) => {
663                    warn!("{my_pubkey}: channel \"{channel_name}\" disconnected")
664                }
665            }
666        }
667
668        Ok(())
669    }
670
671    fn on_header(
672        &mut self,
673        header: &VersionedBlockHeader,
674        bank_parent_slot: Slot,
675    ) -> Result<(), BlockComponentProcessorError> {
676        self.stage.on_header()?;
677
678        let VersionedBlockHeader::V1(header) = header;
679        if header.parent_slot != bank_parent_slot {
680            return Err(BlockComponentProcessorError::HeaderParentSlotMismatch {
681                header_parent_slot: header.parent_slot,
682                bank_parent_slot,
683            });
684        }
685        Ok(())
686    }
687
688    fn on_update_parent(
689        &mut self,
690        slot: Slot,
691        update_parent: &VersionedUpdateParent,
692        allow_initial_update_parent: bool,
693    ) -> Result<(), BlockComponentProcessorError> {
694        if leader_slot_index(slot) != 0 {
695            return Err(BlockComponentProcessorError::UpdateParentNotFirstInLeaderWindow(slot));
696        }
697
698        self.stage
699            .on_update_parent(update_parent, allow_initial_update_parent)
700    }
701
702    fn enforce_nanosecond_clock_bounds(
703        bank: &Bank,
704        parent_bank: &Bank,
705        footer: &BlockFooterV1,
706    ) -> Result<(), BlockComponentProcessorError> {
707        // Get parent time from the nanosecond clock account, or from the Tower-based
708        // clock for the first Alpenglow block.
709        let parent_time_nanos = parent_bank
710            .get_nanosecond_clock()
711            .unwrap_or_else(|| bank.clock().unix_timestamp.saturating_mul(1_000_000_000));
712
713        let parent_slot = parent_bank.slot();
714        let current_time_nanos =
715            Self::block_producer_time_nanos_as_i64(footer.block_producer_time_nanos)?;
716        let current_slot = bank.slot();
717        let elapsed_slot_duration_nanos =
718            bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot);
719
720        let (lower_bound_nanos, upper_bound_nanos) =
721            Self::nanosecond_time_bounds(parent_time_nanos, elapsed_slot_duration_nanos);
722
723        let is_valid =
724            lower_bound_nanos <= current_time_nanos && current_time_nanos <= upper_bound_nanos;
725
726        match is_valid {
727            true => Ok(()),
728            false => Err(BlockComponentProcessorError::NanosecondClockOutOfBounds),
729        }
730    }
731
732    /// Converts a footer timestamp to the signed nanosecond representation used
733    /// by bank clock state.
734    ///
735    /// The `block_producer_time_nanos` parameter comes from wire-format footer
736    /// data and is rejected if it cannot be represented as `i64`; wrapping it
737    /// would make an extreme future timestamp look negative.
738    fn block_producer_time_nanos_as_i64(
739        block_producer_time_nanos: u64,
740    ) -> Result<i64, BlockComponentProcessorError> {
741        i64::try_from(block_producer_time_nanos)
742            .map_err(|_| BlockComponentProcessorError::NanosecondClockOutOfBounds)
743    }
744
745    /// Given a parent time and elapsed slot duration, calculates inclusive
746    /// block producer timestamp bounds.
747    ///
748    /// `parent_time_nanos` describes the parent bank's nanosecond clock.
749    /// `elapsed_slot_duration_nanos` is the summed duration for all skipped
750    /// and working slots after the parent. The returned `(lower_bound,
751    /// upper_bound)` accepts timestamps where
752    /// `lower_bound <= working_bank_time <= upper_bound`.
753    ///
754    /// Refer to
755    /// https://github.com/solana-foundation/solana-improvement-documents/pull/363
756    /// for details on the bounds calculation.
757    pub fn nanosecond_time_bounds(
758        parent_time_nanos: i64,
759        elapsed_slot_duration_nanos: u128,
760    ) -> (i64, i64) {
761        let min_working_bank_time = parent_time_nanos.saturating_add(1);
762        let max_working_bank_time_offset = elapsed_slot_duration_nanos
763            .saturating_mul(2)
764            .min(i64::MAX as u128) as i64;
765        let max_working_bank_time = parent_time_nanos.saturating_add(max_working_bank_time_offset);
766
767        (min_working_bank_time, max_working_bank_time)
768    }
769
770    pub fn update_bank_with_footer_fields(
771        bank: &Bank,
772        block_producer_time_nanos: i64,
773        bank_hash: Option<Hash>,
774        reward_cert: Option<ValidatedRewardCert>,
775        final_cert_input: Option<(&HashSet<Pubkey>, Slot)>,
776    ) -> Result<(), BankFooterError> {
777        bank.update_clock_from_footer(block_producer_time_nanos);
778        calc_vote_rewards_update_vote_states(
779            bank,
780            reward_cert,
781            final_cert_input,
782            block_producer_time_nanos,
783        )?;
784
785        if let Some(hash) = bank_hash {
786            // Record expected bank hash from footer for later verification when the bank is frozen.
787            bank.set_expected_bank_hash(hash);
788        }
789        Ok(())
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use {
796        super::*,
797        crate::{
798            bank::{Bank, SlotLeader},
799            bank_forks::BankForks,
800            genesis_utils::{create_genesis_config, create_genesis_config_with_tower_leader},
801        },
802        agave_votor_messages::consensus_message::BlockId,
803        bytes::Bytes,
804        rand::Rng,
805        solana_bls_signatures::{BLS_SIGNATURE_AFFINE_SIZE, Signature as BLSSignature},
806        solana_clock::DEFAULT_MS_PER_SLOT,
807        solana_entry::block_component::{
808            BlockFooterV1, BlockHeaderV1, UpdateParentV1, VersionedUpdateParent,
809        },
810        solana_hash::Hash,
811        solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS,
812        std::{
813            assert_matches,
814            sync::{Arc, RwLock},
815        },
816        test_case::test_case,
817    };
818
819    const DEFAULT_NS_PER_SLOT: u64 = DEFAULT_MS_PER_SLOT * 1_000_000;
820
821    fn create_test_bank() -> (Arc<Bank>, Arc<RwLock<BankForks>>) {
822        let genesis_config_info = create_genesis_config(10_000);
823        Bank::new_with_bank_forks_for_tests(&genesis_config_info.genesis_config)
824    }
825
826    fn create_test_bank_tower() -> (Arc<Bank>, Arc<RwLock<BankForks>>) {
827        let genesis_config_info =
828            create_genesis_config_with_tower_leader(10_000, &Pubkey::new_unique(), 0);
829        Bank::new_with_bank_forks_for_tests(&genesis_config_info.genesis_config)
830    }
831
832    fn create_child_bank(
833        bank_forks: &RwLock<BankForks>,
834        parent: &Arc<Bank>,
835        slot: u64,
836    ) -> Arc<Bank> {
837        Bank::new_from_parent_with_bank_forks(
838            bank_forks,
839            parent.clone(),
840            SlotLeader::new_unique(),
841            slot,
842        )
843    }
844
845    fn test_genesis_cert_marker() -> GenesisCertBlockMarker {
846        GenesisCertBlockMarker {
847            slot: 0,
848            block_id: BlockId::default(),
849            bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
850            bitmap: vec![],
851        }
852    }
853
854    fn post_migration_status_with_genesis_slot(genesis_slot: Slot) -> MigrationStatus {
855        let migration_status = MigrationStatus::default();
856        let migration_slot = migration_status.record_feature_activation(0);
857        assert!(genesis_slot < migration_slot);
858
859        let genesis_block = Block::new_unique(genesis_slot);
860        migration_status.set_genesis_block(genesis_block);
861        let cert = Arc::new(GenesisCert {
862            block: genesis_block,
863            signature: CertSignature {
864                signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
865                bitmap: vec![],
866            },
867        });
868        migration_status.set_genesis_certificate(cert);
869        migration_status.enable_alpenglow_during_startup();
870
871        migration_status
872    }
873
874    fn processor_after_header() -> BlockComponentProcessor {
875        BlockComponentProcessor {
876            stage: BlockComponentStage::AcceptingGenesisOrEntries,
877            ..BlockComponentProcessor::default()
878        }
879    }
880
881    fn processor_after_footer() -> BlockComponentProcessor {
882        BlockComponentProcessor {
883            stage: BlockComponentStage::AcceptingAlpentick,
884            ..BlockComponentProcessor::default()
885        }
886    }
887
888    #[test]
889    fn test_first_alpenglow_block_requires_genesis_certificate_marker() {
890        let migration_status = post_migration_status_with_genesis_slot(1);
891        let processor = processor_after_footer();
892
893        let result = processor.on_final(&migration_status, 2, 1);
894        assert!(matches!(
895            result,
896            Err(BlockComponentProcessorError::MissingGenesisCertificateMarker)
897        ));
898    }
899
900    #[test]
901    fn test_first_alpenglow_block_with_genesis_certificate_marker_succeeds() {
902        let migration_status = post_migration_status_with_genesis_slot(1);
903        let (genesis_bank, bank_forks) = create_test_bank_tower();
904        let parent = create_child_bank(&bank_forks, &genesis_bank, 1);
905        let parent_block_id = BlockId::new_unique();
906        parent.set_block_id(Some(parent_block_id.to_hash()));
907        let bank = create_child_bank(&bank_forks, &parent, 2);
908        let genesis_marker = GenesisCertBlockMarker {
909            slot: parent.slot(),
910            block_id: parent_block_id,
911            bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
912            bitmap: vec![],
913        };
914        let mut processor = processor_after_header();
915        bank.set_hashes_per_tick(Some(42));
916        assert!(bank.hashes_per_tick().is_some());
917
918        processor
919            .on_genesis_cert_block_marker_leader(bank.clone(), genesis_marker, &migration_status)
920            .unwrap();
921        assert!(bank.hashes_per_tick().is_none());
922        processor.stage = BlockComponentStage::Done;
923        assert!(processor.on_final(&migration_status, 2, 1).is_ok());
924    }
925
926    #[test]
927    fn test_genesis_certificate_marker_aborts_tower_bank_during_migration() {
928        let migration_status = MigrationStatus::default();
929        migration_status.record_feature_activation(0);
930        let (genesis_bank, bank_forks) = create_test_bank_tower();
931        let parent = create_child_bank(&bank_forks, &genesis_bank, 1);
932        let parent_block_id = BlockId::new_unique();
933        parent.set_block_id(Some(parent_block_id.to_hash()));
934        let bank = create_child_bank(&bank_forks, &parent, 2);
935        let genesis_marker = GenesisCertBlockMarker {
936            slot: parent.slot(),
937            block_id: parent_block_id,
938            bls_signature: BLSSignature([0; BLS_SIGNATURE_AFFINE_SIZE]),
939            bitmap: vec![],
940        };
941        let mut processor = processor_after_header();
942        bank.set_hashes_per_tick(Some(42));
943        let tower_hashes_per_tick = bank.hashes_per_tick();
944        assert!(tower_hashes_per_tick.is_some());
945
946        assert_matches!(
947            processor.on_genesis_cert_block_marker_leader(
948                bank.clone(),
949                genesis_marker,
950                &migration_status,
951            ),
952            Err(BlockComponentProcessorError::AlpenglowMigrationTransition)
953        );
954
955        assert!(migration_status.is_ready_to_enable());
956        assert_eq!(bank.hashes_per_tick(), tower_hashes_per_tick);
957        assert!(bank.get_alpenglow_genesis_certificate().is_some());
958    }
959
960    #[test]
961    fn test_on_footer_sets_timestamp() {
962        let my_pubkey = Pubkey::new_unique();
963        let mut processor = processor_after_header();
964
965        let (parent, bank_forks) = create_test_bank();
966        let bank = create_child_bank(&bank_forks, &parent, 1);
967        let shred_version = rand::rng().random();
968
969        // Calculate valid timestamp based on parent's time
970        let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000);
971        let footer_time_nanos = parent_time_nanos + 200_000_000; // parent + 200ms
972        let expected_time_secs = footer_time_nanos / 1_000_000_000;
973
974        let footer = VersionedBlockFooter::V1(BlockFooterV1 {
975            bank_hash: Hash::new_unique(),
976            block_producer_time_nanos: footer_time_nanos as u64,
977            block_user_agent: vec![],
978            block_final_cert: None,
979            skip_reward_cert: None,
980            notar_reward_cert: None,
981        });
982
983        processor
984            .on_footer(
985                &my_pubkey,
986                bank.clone(),
987                parent,
988                shred_version,
989                footer,
990                None,
991            )
992            .unwrap();
993
994        assert_eq!(processor.stage, BlockComponentStage::AcceptingAlpentick);
995
996        // Verify clock sysvar was updated with correct timestamp (nanos converted to seconds)
997        assert_eq!(bank.clock().unix_timestamp, expected_time_secs);
998    }
999
1000    #[test]
1001    fn test_footer_sets_epoch_start_timestamp_on_epoch_change() {
1002        let my_pubkey = Pubkey::new_unique();
1003        let mut processor = processor_after_header();
1004        let shred_version = rand::rng().random();
1005
1006        // Create genesis bank
1007        let genesis_config_info = create_genesis_config(10_000);
1008        let (genesis_bank, bank_forks) =
1009            Bank::new_with_bank_forks_for_tests(&genesis_config_info.genesis_config);
1010
1011        // Get epoch schedule to find first slot of next epoch
1012        let epoch_schedule = genesis_bank.epoch_schedule();
1013        let first_slot_in_epoch_1 = epoch_schedule.get_first_slot_in_epoch(1);
1014
1015        // Create parent bank at last slot of epoch 0
1016        let mut parent = genesis_bank.clone();
1017        for slot in 1..first_slot_in_epoch_1 {
1018            parent = create_child_bank(&bank_forks, &parent, slot);
1019        }
1020
1021        // Create bank at first slot of epoch 1
1022        let bank = create_child_bank(&bank_forks, &parent, first_slot_in_epoch_1);
1023
1024        // Verify we're in epoch 1
1025        assert_eq!(bank.epoch(), 1);
1026
1027        // Calculate valid timestamp based on parent's time
1028        let parent_slot = parent.slot();
1029        let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000);
1030        let current_slot = bank.slot();
1031        let elapsed_slot_duration_nanos =
1032            bank.slot_range_duration_nanos(parent_slot.saturating_add(1), current_slot);
1033
1034        // Use a timestamp in the middle of the valid range
1035        let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds(
1036            parent_time_nanos,
1037            elapsed_slot_duration_nanos,
1038        );
1039        let footer_time_nanos = (lower_bound + upper_bound) / 2;
1040        let expected_time_secs = footer_time_nanos / 1_000_000_000;
1041
1042        let footer = VersionedBlockFooter::V1(BlockFooterV1 {
1043            bank_hash: Hash::new_unique(),
1044            block_producer_time_nanos: footer_time_nanos as u64,
1045            block_user_agent: vec![],
1046            block_final_cert: None,
1047            skip_reward_cert: None,
1048            notar_reward_cert: None,
1049        });
1050
1051        processor
1052            .on_footer(
1053                &my_pubkey,
1054                bank.clone(),
1055                parent,
1056                shred_version,
1057                footer,
1058                None,
1059            )
1060            .unwrap();
1061
1062        // Verify clock sysvar was updated
1063        assert_eq!(bank.clock().unix_timestamp, expected_time_secs);
1064
1065        // Verify epoch_start_timestamp was set correctly for the new epoch
1066        assert_eq!(bank.clock().epoch_start_timestamp, expected_time_secs);
1067    }
1068
1069    // Test clock bounds enforcement
1070    #[test_case(1, |_, lower, _| lower, true; "at_minimum")]
1071    #[test_case(1, |_, _, upper| upper, true; "at_maximum")]
1072    #[test_case(1, |_, lower, _| lower - 1, false; "below_minimum")]
1073    #[test_case(1, |_, _, upper| upper + 1, false; "above_maximum")]
1074    // For 5 slots: upper_bound = parent_time + 2 * 5 * 400ms = parent_time + 4000ms
1075    // Use 2 seconds which is within bounds
1076    #[test_case(5, |_, lower, _| lower + 2_000_000_000, true; "multi_slot_gap")]
1077    // Exceed by 1 second beyond the upper bound
1078    #[test_case(5, |_, _, upper| upper + 1_000_000_000, false; "multi_slot_gap_exceeds")]
1079    // Timestamp equal to parent time (should fail, must be strictly greater)
1080    #[test_case(1, |parent_time, _, _| parent_time, false; "timestamp_equals_parent")]
1081    fn test_clock_bounds(
1082        slot_gap: u64,
1083        timestamp_fn: impl FnOnce(i64, i64, i64) -> i64,
1084        should_pass: bool,
1085    ) {
1086        let my_pubkey = Pubkey::new_unique();
1087        let mut processor = processor_after_header();
1088        let shred_version = rand::rng().random();
1089
1090        let (parent, bank_forks) = create_test_bank();
1091        let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000);
1092
1093        // Set up clock on parent so validation doesn't skip bounds checking
1094        parent.update_clock_from_footer(parent_time_nanos);
1095
1096        let bank: Arc<Bank> = create_child_bank(&bank_forks, &parent, slot_gap);
1097        let elapsed_slot_duration_nanos = bank.slot_range_duration_nanos(1, slot_gap);
1098
1099        let (lower_bound, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds(
1100            parent_time_nanos,
1101            elapsed_slot_duration_nanos,
1102        );
1103
1104        let footer_time_nanos = timestamp_fn(parent_time_nanos, lower_bound, upper_bound);
1105
1106        let footer = VersionedBlockFooter::V1(BlockFooterV1 {
1107            bank_hash: Hash::new_unique(),
1108            block_producer_time_nanos: footer_time_nanos as u64,
1109            block_user_agent: vec![],
1110            block_final_cert: None,
1111            skip_reward_cert: None,
1112            notar_reward_cert: None,
1113        });
1114
1115        let result = processor.on_footer(&my_pubkey, bank, parent, shred_version, footer, None);
1116        if should_pass {
1117            result.unwrap();
1118        } else {
1119            assert!(matches!(
1120                result.unwrap_err(),
1121                BlockComponentProcessorError::NanosecondClockOutOfBounds
1122            ));
1123        }
1124    }
1125
1126    #[test]
1127    fn test_clock_bounds_without_parent_nanosecond_clock_rejects_out_of_bounds() {
1128        let my_pubkey = Pubkey::new_unique();
1129        let mut processor = processor_after_header();
1130        let shred_version = rand::rng().random();
1131
1132        let (parent, bank_forks) = create_test_bank();
1133        assert_eq!(parent.get_nanosecond_clock(), None);
1134
1135        let bank = create_child_bank(&bank_forks, &parent, 1);
1136        let parent_time_nanos = bank.clock().unix_timestamp.saturating_mul(1_000_000_000);
1137        let elapsed_slot_duration_nanos =
1138            bank.slot_range_duration_nanos(parent.slot().saturating_add(1), bank.slot());
1139        let (_, upper_bound) = BlockComponentProcessor::nanosecond_time_bounds(
1140            parent_time_nanos,
1141            elapsed_slot_duration_nanos,
1142        );
1143
1144        let footer = VersionedBlockFooter::V1(BlockFooterV1 {
1145            bank_hash: Hash::new_unique(),
1146            block_producer_time_nanos: u64::try_from(upper_bound.saturating_add(1)).unwrap(),
1147            block_user_agent: vec![],
1148            block_final_cert: None,
1149            skip_reward_cert: None,
1150            notar_reward_cert: None,
1151        });
1152
1153        assert!(matches!(
1154            processor
1155                .on_footer(&my_pubkey, bank, parent, shred_version, footer, None)
1156                .unwrap_err(),
1157            BlockComponentProcessorError::NanosecondClockOutOfBounds
1158        ));
1159    }
1160
1161    #[test]
1162    fn test_clock_bounds_rejects_timestamp_above_i64() {
1163        let my_pubkey = Pubkey::new_unique();
1164        let mut processor = processor_after_header();
1165        let shred_version = rand::rng().random();
1166
1167        let (parent, bank_forks) = create_test_bank();
1168        let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000);
1169        parent.update_clock_from_footer(parent_time_nanos);
1170        let bank = create_child_bank(&bank_forks, &parent, 1);
1171
1172        let footer = VersionedBlockFooter::V1(BlockFooterV1 {
1173            bank_hash: Hash::new_unique(),
1174            block_producer_time_nanos: u64::MAX,
1175            block_user_agent: vec![],
1176            block_final_cert: None,
1177            skip_reward_cert: None,
1178            notar_reward_cert: None,
1179        });
1180
1181        assert!(matches!(
1182            processor
1183                .on_footer(&my_pubkey, bank, parent, shred_version, footer, None)
1184                .unwrap_err(),
1185            BlockComponentProcessorError::NanosecondClockOutOfBounds
1186        ));
1187    }
1188
1189    // Helper function to test nanosecond_time_bounds calculation
1190    fn test_nanosecond_time_bounds_helper(
1191        parent_time_nanos: i64,
1192        elapsed_slot_duration_nanos: u128,
1193        expected_lower: i64,
1194        expected_upper: i64,
1195    ) {
1196        let (lower, upper) = BlockComponentProcessor::nanosecond_time_bounds(
1197            parent_time_nanos,
1198            elapsed_slot_duration_nanos,
1199        );
1200
1201        assert_eq!(lower, expected_lower);
1202        assert_eq!(upper, expected_upper);
1203    }
1204
1205    #[test]
1206    fn test_nanosecond_time_bounds_calculation() {
1207        // Test the nanosecond_time_bounds function directly
1208        // diff_slots = 15 - 10 = 5
1209        // lower = parent_time + 1
1210        // upper = parent_time + 2 * 5 * 400_000_000 = parent_time + 4_000_000_000
1211        let parent_slot = 10;
1212        let parent_time = 1_000_000_000_000; // 1000 seconds in nanos
1213        let working_slot = 15;
1214        let slot_delta = working_slot - parent_slot;
1215        test_nanosecond_time_bounds_helper(
1216            parent_time,
1217            u128::from(slot_delta).saturating_mul(u128::from(DEFAULT_NS_PER_SLOT)),
1218            parent_time + 1,
1219            parent_time + (2 * DEFAULT_NS_PER_SLOT * slot_delta) as i64,
1220        );
1221    }
1222
1223    #[test]
1224    fn test_nanosecond_time_bounds_same_slot() {
1225        // Test with same slot (diff = 0)
1226        // diff_slots = 0
1227        // lower = parent_time + 1
1228        // upper = parent_time + 2 * 0 * 400_000_000 = parent_time
1229        // Note: In this case, lower > upper, so no timestamp would be valid
1230        // This is expected since we shouldn't have the same slot for parent and working bank
1231        let parent_time = 1_000_000_000_000;
1232        test_nanosecond_time_bounds_helper(parent_time, 0, parent_time + 1, parent_time);
1233    }
1234
1235    #[test]
1236    fn test_nanosecond_time_bounds_saturates_upper_bound() {
1237        let parent_time = i64::MAX - 5;
1238        let (lower, upper) =
1239            BlockComponentProcessor::nanosecond_time_bounds(parent_time, u128::MAX);
1240
1241        assert_eq!(lower, parent_time + 1);
1242        assert_eq!(upper, i64::MAX);
1243    }
1244
1245    /// Each case runs a component sequence against a fresh processor — one
1246    /// `on_marker` / `on_entry_batch` call per component — then `on_final`,
1247    /// as on a full slot.
1248    #[test]
1249    fn test_processor_component_sequences() {
1250        use BlockComponentProcessorError as E;
1251
1252        type Step = Box<
1253            dyn FnOnce(
1254                &mut BlockComponentProcessor,
1255                &MigrationStatus,
1256            ) -> Result<(), BlockComponentProcessorError>,
1257        >;
1258
1259        let post_migration = MigrationStatus::post_migration_status();
1260        let pre_migration = MigrationStatus::default();
1261        // Feature activated but migration not yet complete
1262        let in_migration = MigrationStatus::default();
1263        in_migration.record_feature_activation(0);
1264
1265        let (parent, bank_forks) = create_test_bank();
1266        // First slot of a leader window so UpdateParent passes the window check
1267        let slot = NUM_CONSECUTIVE_LEADER_SLOTS.get() as Slot;
1268        let bank = create_child_bank(&bank_forks, &parent, slot);
1269        assert_eq!(leader_slot_index(slot), 0);
1270        // A bank whose slot is not the first in its leader window, for the
1271        // UpdateParent window check
1272        let bank_not_window_start = create_child_bank(&bank_forks, &parent, slot + 1);
1273        assert_ne!(leader_slot_index(slot + 1), 0);
1274        let shred_version: u16 = rand::rng().random();
1275
1276        // A timestamp inside the footer clock bounds for every case below
1277        let parent_time_nanos = parent.clock().unix_timestamp.saturating_mul(1_000_000_000);
1278        let footer_time_nanos = u64::try_from(parent_time_nanos + 400_000_000).unwrap();
1279
1280        // One step = one direct `on_marker` call
1281        let marker_step = {
1282            let (bank, parent) = (bank.clone(), parent.clone());
1283            move |marker: VersionedBlockMarker, allow_initial_update_parent: bool| -> Step {
1284                let (bank, parent) = (bank.clone(), parent.clone());
1285                Box::new(move |processor, migration_status| {
1286                    processor.on_marker(
1287                        bank,
1288                        parent,
1289                        shred_version,
1290                        marker,
1291                        allow_initial_update_parent,
1292                        None,
1293                        migration_status,
1294                    )
1295                })
1296            }
1297        };
1298        // One step = one direct `on_entry_batch` call
1299        let batch_step = move |entries: Vec<EntryView<Bytes>>, is_final: bool| -> Step {
1300            Box::new(move |processor, migration_status| {
1301                processor.on_entry_batch(migration_status, slot, &entries, is_final)
1302            })
1303        };
1304
1305        // Step builders: real wire types, fresh per case
1306        let header_with_parent_slot = |parent_slot: Slot| {
1307            marker_step(
1308                VersionedBlockMarker::from_block_header(BlockHeaderV1 {
1309                    parent_slot,
1310                    parent_block_id: Hash::default(),
1311                }),
1312                false,
1313            )
1314        };
1315        let header = || header_with_parent_slot(0);
1316        let genesis_cert = || {
1317            marker_step(
1318                VersionedBlockMarker::from_genesis_cert_block_marker(test_genesis_cert_marker()),
1319                false,
1320            )
1321        };
1322        let update_parent = |allow_initial_update_parent: bool| {
1323            marker_step(
1324                VersionedBlockMarker::from_update_parent(UpdateParentV1 {
1325                    new_parent_slot: 0,
1326                    new_parent_block_id: BlockId::default(),
1327                }),
1328                allow_initial_update_parent,
1329            )
1330        };
1331        // Same UpdateParent marker, sent to a bank whose slot is not the
1332        // first in its leader window
1333        let update_parent_not_window_start = {
1334            let (bank, parent) = (bank_not_window_start.clone(), parent.clone());
1335            move |allow_initial_update_parent: bool| -> Step {
1336                let (bank, parent) = (bank.clone(), parent.clone());
1337                Box::new(move |processor, migration_status| {
1338                    processor.on_marker(
1339                        bank,
1340                        parent,
1341                        shred_version,
1342                        VersionedBlockMarker::from_update_parent(UpdateParentV1 {
1343                            new_parent_slot: 0,
1344                            new_parent_block_id: BlockId::default(),
1345                        }),
1346                        allow_initial_update_parent,
1347                        None,
1348                        migration_status,
1349                    )
1350                })
1351            }
1352        };
1353        let footer = || {
1354            marker_step(
1355                VersionedBlockMarker::from_block_footer(BlockFooterV1 {
1356                    bank_hash: Hash::new_unique(),
1357                    block_producer_time_nanos: footer_time_nanos,
1358                    block_user_agent: vec![],
1359                    block_final_cert: None,
1360                    skip_reward_cert: None,
1361                    notar_reward_cert: None,
1362                }),
1363                false,
1364            )
1365        };
1366        let entries = || {
1367            batch_step(
1368                vec![EntryView {
1369                    num_hashes: 2,
1370                    hash: Hash::default(),
1371                    transactions: vec![],
1372                }],
1373                false,
1374            )
1375        };
1376        // Final batch with num_hashes != 1: never classified as the alpentick,
1377        // even as the last component of a full slot
1378        let final_entries = || {
1379            batch_step(
1380                vec![EntryView {
1381                    num_hashes: 2,
1382                    hash: Hash::default(),
1383                    transactions: vec![],
1384                }],
1385                true,
1386            )
1387        };
1388        // Single tick with num_hashes == 1: classified as the alpentick only
1389        // when it is the final component of a full slot
1390        let tick = |is_final: bool| {
1391            batch_step(
1392                vec![EntryView {
1393                    num_hashes: 1,
1394                    hash: Hash::default(),
1395                    transactions: vec![],
1396                }],
1397                is_final,
1398            )
1399        };
1400
1401        let abandoned = || {
1402            E::AbandonedBank(VersionedUpdateParent::V1(UpdateParentV1 {
1403                new_parent_slot: 0,
1404                new_parent_block_id: BlockId::default(),
1405            }))
1406        };
1407
1408        #[rustfmt::skip]
1409        let cases: Vec<(&MigrationStatus, Vec<Step>, Result<(), E>)> = vec![
1410            // - Pre migration
1411            (&pre_migration, vec![entries(), tick(true)], Ok(())),
1412            (&pre_migration, vec![entries(), tick(false)], Ok(())),
1413            (&pre_migration, vec![tick(false)], Ok(())),
1414            (&pre_migration, vec![tick(true), entries()], Ok(())),
1415            (&pre_migration, vec![entries(), entries(), tick(false), tick(true)], Ok(())),
1416            (&pre_migration, vec![header()], Err(E::BlockComponentPreMigration)),
1417            (&pre_migration, vec![genesis_cert()], Err(E::BlockComponentPreMigration)),
1418            (&pre_migration, vec![footer()], Err(E::BlockComponentPreMigration)),
1419            (&pre_migration, vec![update_parent(false)], Err(E::BlockComponentPreMigration)),
1420            (&pre_migration, vec![update_parent(true)], Err(E::BlockComponentPreMigration)),
1421
1422            // - In migration
1423            // header and genesis cert are processed so a slow node can catch up,
1424            // other markers are still rejected
1425            (&in_migration, vec![entries(), tick(true)], Ok(())),
1426            (&in_migration, vec![header()], Err(E::BlockComponentPreMigration)),
1427            // genesis cert reaches the stage check instead of the migration gate
1428            (&in_migration, vec![genesis_cert()], Err(E::MissingParentMarker)),
1429            // header passes on_marker: the genesis cert reaches deep validation
1430            (&in_migration, vec![header(), genesis_cert()], Err(E::GenesisCertificateInAlpenglowCluster)),
1431            (&in_migration, vec![footer()], Err(E::BlockComponentPreMigration)),
1432            (&in_migration, vec![update_parent(false)], Err(E::BlockComponentPreMigration)),
1433            (&in_migration, vec![update_parent(true)], Err(E::BlockComponentPreMigration)),
1434
1435            // - Post migration
1436
1437            // Valid block
1438            // a valid genesis cert block needs a parent slot != 0, covered by
1439            // test_first_alpenglow_block_with_genesis_certificate_marker_succeeds
1440            (&post_migration, vec![header(), footer(), tick(true)], Ok(())),
1441            (&post_migration, vec![header(), entries(), entries(), footer(), tick(true)], Ok(())),
1442            (&post_migration, vec![update_parent(true), entries(), footer(), tick(true)], Ok(())),
1443            (&post_migration, vec![update_parent(true), footer(), tick(true)], Ok(())),
1444            // Alpentick-shaped batch mid-block is a plain entry batch, not the alpentick
1445            (&post_migration, vec![header(), tick(false), footer(), tick(true)], Ok(())),
1446            (&post_migration, vec![update_parent(true), tick(false), footer(), tick(true)], Ok(())),
1447
1448            // Empty block
1449            (&post_migration, vec![], Err(E::MissingBlockFooter)),
1450
1451            // MissingParentMarker
1452            (&post_migration, vec![entries()], Err(E::MissingParentMarker)),
1453            (&post_migration, vec![genesis_cert()], Err(E::MissingParentMarker)),
1454            (&post_migration, vec![footer()], Err(E::MissingParentMarker)),
1455            (&post_migration, vec![tick(true)], Err(E::MissingParentMarker)),
1456            (&post_migration, vec![tick(false)], Err(E::MissingParentMarker)),
1457
1458            // From-shred-zero replay must not accept an initial UpdateParent
1459            (&post_migration, vec![update_parent(false)], Err(E::UnexpectedInitialUpdateParent)),
1460
1461            // Genesis cert position
1462            (&post_migration, vec![header(), entries(), genesis_cert()], Err(E::GenesisCertificateOutOfOrder)),
1463            (&post_migration, vec![header(), footer(), genesis_cert()], Err(E::GenesisCertificateOutOfOrder)),
1464            (&post_migration, vec![update_parent(true), genesis_cert()], Err(E::GenesisCertificateOutOfOrder)),
1465            // correct position, but this cluster runs Alpenglow from slot 0
1466            (&post_migration, vec![header(), genesis_cert()], Err(E::GenesisCertificateInAlpenglowCluster)),
1467
1468            // Duplicate / misplaced parent markers
1469            (&post_migration, vec![header(), header()], Err(E::MultipleBlockHeaders)),
1470            (&post_migration, vec![header(), entries(), header()], Err(E::MultipleBlockHeaders)),
1471            (&post_migration, vec![header(), footer(), header()], Err(E::MultipleBlockHeaders)),
1472            (&post_migration, vec![update_parent(true), header()], Err(E::SpuriousUpdateParent)),
1473            (&post_migration, vec![update_parent(true), update_parent(true)], Err(E::MultipleUpdateParents)),
1474            (&post_migration, vec![update_parent(true), update_parent(false)], Err(E::MultipleUpdateParents)),
1475
1476            // Header parent slot must match the bank parent slot
1477            (&post_migration, vec![header_with_parent_slot(3)], Err(E::HeaderParentSlotMismatch { header_parent_slot: 3, bank_parent_slot: 0 })),
1478
1479            // UpdateParent is only valid in the first slot of a leader window,
1480            // whatever the flag
1481            (&post_migration, vec![update_parent_not_window_start(true)], Err(E::UpdateParentNotFirstInLeaderWindow(5))),
1482            (&post_migration, vec![update_parent_not_window_start(false)], Err(E::UpdateParentNotFirstInLeaderWindow(5))),
1483
1484            // Mid-block UpdateParent: controlled abort (fast leader handover)
1485            // the flag only matters as the first component
1486            (&post_migration, vec![header(), update_parent(false)], Err(abandoned())),
1487            (&post_migration, vec![header(), update_parent(true)], Err(abandoned())),
1488            (&post_migration, vec![header(), entries(), update_parent(false)], Err(abandoned())),
1489            (&post_migration, vec![header(), footer(), update_parent(false)], Err(E::SpuriousUpdateParent)),
1490            (&post_migration, vec![header(), footer(), update_parent(true)], Err(E::SpuriousUpdateParent)),
1491
1492            // Alpentick (tick with is_final) only directly after the footer
1493            (&post_migration, vec![header(), tick(true)], Err(E::InvalidAlpentickPosition)),
1494            (&post_migration, vec![header(), entries(), tick(true)], Err(E::InvalidAlpentickPosition)),
1495            (&post_migration, vec![update_parent(true), tick(true)], Err(E::InvalidAlpentickPosition)),
1496
1497            // Footer is terminal
1498            (&post_migration, vec![header(), footer(), entries(), tick(true)], Err(E::EntryBatchAfterBlockFooter)),
1499            (&post_migration, vec![header(), footer(), footer()], Err(E::MultipleBlockFooters)),
1500            // Alpentick-shaped but non-final after the footer: plain entry batch
1501            (&post_migration, vec![header(), footer(), tick(false), tick(true)], Err(E::EntryBatchAfterBlockFooter)),
1502            // Final but not alpentick-shaped after the footer: plain entry batch
1503            (&post_migration, vec![header(), footer(), final_entries()], Err(E::EntryBatchAfterBlockFooter)),
1504
1505            // Nothing after the alpentick
1506            (&post_migration, vec![header(), footer(), tick(true), header()], Err(E::MultipleBlockHeaders)),
1507            (&post_migration, vec![header(), footer(), tick(true), genesis_cert()], Err(E::GenesisCertificateOutOfOrder)),
1508            (&post_migration, vec![header(), footer(), tick(true), entries()], Err(E::EntryBatchAfterBlockFooter)),
1509            (&post_migration, vec![header(), footer(), tick(true), tick(false)], Err(E::EntryBatchAfterBlockFooter)),
1510            // A second alpentick after the real one
1511            (&post_migration, vec![header(), footer(), tick(true), tick(true)], Err(E::InvalidAlpentickPosition)),
1512            (&post_migration, vec![header(), footer(), tick(true), footer()], Err(E::MultipleBlockFooters)),
1513            (&post_migration, vec![header(), footer(), tick(true), update_parent(false)], Err(E::SpuriousUpdateParent)),
1514
1515            // Missing tail on a full slot
1516            (&post_migration, vec![header()], Err(E::MissingBlockFooter)),
1517            (&post_migration, vec![header(), entries()], Err(E::MissingBlockFooter)),
1518            (&post_migration, vec![update_parent(true), entries()], Err(E::MissingBlockFooter)),
1519            // Footer but no alpentick
1520            (&post_migration, vec![header(), footer()], Err(E::InvalidAlpentickPosition)),
1521        ];
1522
1523        for (case_index, (migration_status, steps, expected)) in cases.into_iter().enumerate() {
1524            let mut processor = BlockComponentProcessor::default();
1525            let result = steps
1526                .into_iter()
1527                .try_for_each(|step| step(&mut processor, migration_status))
1528                .and_then(|()| processor.on_final(migration_status, slot, bank.parent_slot()));
1529
1530            match (result, expected) {
1531                (Ok(()), Ok(())) => (),
1532                (Err(err), Err(expected_err)) => {
1533                    assert_eq!(
1534                        std::mem::discriminant(&err),
1535                        std::mem::discriminant(&expected_err),
1536                        "case {case_index}: got {err:?}, expected {expected_err:?}"
1537                    );
1538                }
1539                (result, expected) => {
1540                    panic!("case {case_index}: got {result:?}, expected {expected:?}");
1541                }
1542            }
1543        }
1544    }
1545
1546    /// Every stage x event cell: `Ok(next)` asserts the transition, `Err(e)`
1547    /// asserts the error and that the stage is unchanged.
1548    #[test]
1549    fn test_stage_transitions() {
1550        type Event =
1551            Box<dyn Fn(&mut BlockComponentStage) -> Result<(), BlockComponentProcessorError>>;
1552
1553        let update_parent_marker = VersionedUpdateParent::V1(UpdateParentV1 {
1554            new_parent_slot: 0,
1555            new_parent_block_id: BlockId::default(),
1556        });
1557        let abandoned =
1558            || BlockComponentProcessorError::AbandonedBank(update_parent_marker.clone());
1559
1560        // Events: one direct stage method call each
1561        let on_header = || -> Event { Box::new(|stage| stage.on_header()) };
1562        let on_genesis_certificate =
1563            || -> Event { Box::new(|stage| stage.on_genesis_certificate()) };
1564        let on_entry_batch = || -> Event { Box::new(|stage| stage.on_entry_batch()) };
1565        let on_update_parent = |allow_initial_update_parent: bool| -> Event {
1566            let marker = update_parent_marker.clone();
1567            Box::new(move |stage| stage.on_update_parent(&marker, allow_initial_update_parent))
1568        };
1569        let on_footer = || -> Event { Box::new(|stage| stage.on_footer()) };
1570        let on_alpentick = || -> Event { Box::new(|stage| stage.on_alpentick()) };
1571        let on_final = || -> Event { Box::new(|stage| stage.on_final()) };
1572
1573        let pre_parent_marker = BlockComponentStage::PreParentMarker;
1574        let genesis_or_entries = BlockComponentStage::AcceptingGenesisOrEntries;
1575        let entries_after_header = BlockComponentStage::AcceptingEntriesOrFooter {
1576            parent_marker: EntryParentMarker::BlockHeader,
1577        };
1578        let entries_after_update_parent = BlockComponentStage::AcceptingEntriesOrFooter {
1579            parent_marker: EntryParentMarker::UpdateParent,
1580        };
1581        let accepting_alpentick = BlockComponentStage::AcceptingAlpentick;
1582        let done = BlockComponentStage::Done;
1583
1584        #[rustfmt::skip]
1585        let cases: Vec<(&BlockComponentStage, Event, Result<BlockComponentStage, BlockComponentProcessorError>)> = vec![
1586            // - PreParentMarker: only a parent marker
1587            (&pre_parent_marker, on_header(), Ok(genesis_or_entries.clone())),
1588            (&pre_parent_marker, on_genesis_certificate(), Err(BlockComponentProcessorError::MissingParentMarker)),
1589            (&pre_parent_marker, on_entry_batch(), Err(BlockComponentProcessorError::MissingParentMarker)),
1590            (&pre_parent_marker, on_update_parent(false), Err(BlockComponentProcessorError::UnexpectedInitialUpdateParent)),
1591            (&pre_parent_marker, on_update_parent(true), Ok(entries_after_update_parent.clone())),
1592            (&pre_parent_marker, on_footer(), Err(BlockComponentProcessorError::MissingParentMarker)),
1593            (&pre_parent_marker, on_alpentick(), Err(BlockComponentProcessorError::MissingParentMarker)),
1594
1595            // - AcceptingGenesisOrEntries: right after the header
1596            (&genesis_or_entries, on_header(), Err(BlockComponentProcessorError::MultipleBlockHeaders)),
1597            (&genesis_or_entries, on_genesis_certificate(), Ok(entries_after_header.clone())),
1598            (&genesis_or_entries, on_entry_batch(), Ok(entries_after_header.clone())),
1599            (&genesis_or_entries, on_update_parent(false), Err(abandoned())),
1600            (&genesis_or_entries, on_update_parent(true), Err(abandoned())),
1601            (&genesis_or_entries, on_footer(), Ok(accepting_alpentick.clone())),
1602            (&genesis_or_entries, on_alpentick(), Err(BlockComponentProcessorError::InvalidAlpentickPosition)),
1603
1604            // - AcceptingEntriesOrFooter { BlockHeader }
1605            (&entries_after_header, on_header(), Err(BlockComponentProcessorError::MultipleBlockHeaders)),
1606            (&entries_after_header, on_genesis_certificate(), Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder)),
1607            (&entries_after_header, on_entry_batch(), Ok(entries_after_header.clone())),
1608            (&entries_after_header, on_update_parent(false), Err(abandoned())),
1609            (&entries_after_header, on_update_parent(true), Err(abandoned())),
1610            (&entries_after_header, on_footer(), Ok(accepting_alpentick.clone())),
1611            (&entries_after_header, on_alpentick(), Err(BlockComponentProcessorError::InvalidAlpentickPosition)),
1612
1613            // - AcceptingEntriesOrFooter { UpdateParent }
1614            // a header here is reported as a spurious UpdateParent, not a second header
1615            (&entries_after_update_parent, on_header(), Err(BlockComponentProcessorError::SpuriousUpdateParent)),
1616            (&entries_after_update_parent, on_genesis_certificate(), Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder)),
1617            (&entries_after_update_parent, on_entry_batch(), Ok(entries_after_update_parent.clone())),
1618            (&entries_after_update_parent, on_update_parent(false), Err(BlockComponentProcessorError::MultipleUpdateParents)),
1619            (&entries_after_update_parent, on_update_parent(true), Err(BlockComponentProcessorError::MultipleUpdateParents)),
1620            (&entries_after_update_parent, on_footer(), Ok(accepting_alpentick.clone())),
1621            (&entries_after_update_parent, on_alpentick(), Err(BlockComponentProcessorError::InvalidAlpentickPosition)),
1622
1623            // - AcceptingAlpentick: only the alpentick
1624            (&accepting_alpentick, on_header(), Err(BlockComponentProcessorError::MultipleBlockHeaders)),
1625            (&accepting_alpentick, on_genesis_certificate(), Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder)),
1626            (&accepting_alpentick, on_entry_batch(), Err(BlockComponentProcessorError::EntryBatchAfterBlockFooter)),
1627            (&accepting_alpentick, on_update_parent(false), Err(BlockComponentProcessorError::SpuriousUpdateParent)),
1628            (&accepting_alpentick, on_update_parent(true), Err(BlockComponentProcessorError::SpuriousUpdateParent)),
1629            (&accepting_alpentick, on_footer(), Err(BlockComponentProcessorError::MultipleBlockFooters)),
1630            (&accepting_alpentick, on_alpentick(), Ok(done.clone())),
1631
1632            // - Done: nothing more
1633            // MultipleBlockHeaders even if the block started with an UpdateParent
1634            (&done, on_header(), Err(BlockComponentProcessorError::MultipleBlockHeaders)),
1635            (&done, on_genesis_certificate(), Err(BlockComponentProcessorError::GenesisCertificateOutOfOrder)),
1636            (&done, on_entry_batch(), Err(BlockComponentProcessorError::EntryBatchAfterBlockFooter)),
1637            (&done, on_update_parent(false), Err(BlockComponentProcessorError::SpuriousUpdateParent)),
1638            (&done, on_update_parent(true), Err(BlockComponentProcessorError::SpuriousUpdateParent)),
1639            (&done, on_footer(), Err(BlockComponentProcessorError::MultipleBlockFooters)),
1640            (&done, on_alpentick(), Err(BlockComponentProcessorError::InvalidAlpentickPosition)),
1641
1642            // - on_final: Ok only from Done
1643            (&pre_parent_marker, on_final(), Err(BlockComponentProcessorError::MissingBlockFooter)),
1644            (&genesis_or_entries, on_final(), Err(BlockComponentProcessorError::MissingBlockFooter)),
1645            (&entries_after_header, on_final(), Err(BlockComponentProcessorError::MissingBlockFooter)),
1646            (&entries_after_update_parent, on_final(), Err(BlockComponentProcessorError::MissingBlockFooter)),
1647            (&accepting_alpentick, on_final(), Err(BlockComponentProcessorError::InvalidAlpentickPosition)),
1648            (&done, on_final(), Ok(done.clone())),
1649        ];
1650
1651        for (case_index, (start, event, expected)) in cases.into_iter().enumerate() {
1652            let mut stage = start.clone();
1653            let result = event(&mut stage);
1654            match (result, expected) {
1655                (Ok(()), Ok(next)) => {
1656                    assert_eq!(stage, next, "case {case_index}: from {start:?}");
1657                }
1658                (Err(err), Err(expected_err)) => {
1659                    assert_eq!(
1660                        std::mem::discriminant(&err),
1661                        std::mem::discriminant(&expected_err),
1662                        "case {case_index}: from {start:?}, got {err:?}, expected {expected_err:?}"
1663                    );
1664                    assert_eq!(&stage, start, "case {case_index}: stage changed on error");
1665                }
1666                (result, expected) => {
1667                    panic!(
1668                        "case {case_index}: from {start:?}, got {result:?}, expected {expected:?}"
1669                    );
1670                }
1671            }
1672        }
1673    }
1674}