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