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