ab_client_block_verification/
beacon_chain.rs

1use crate::{BlockVerification, BlockVerificationError, GenericBody, GenericHeader};
2use ab_client_api::{BlockOrigin, ChainInfo, ChainSyncStatus};
3use ab_client_consensus_common::ConsensusConstants;
4use ab_client_consensus_common::consensus_parameters::{
5    DeriveConsensusParametersChainInfo, DeriveConsensusParametersError,
6    ShardMembershipEntropySourceChainInfo, derive_consensus_parameters,
7    shard_membership_entropy_source,
8};
9use ab_client_proof_of_time::PotNextSlotInput;
10use ab_client_proof_of_time::verifier::PotVerifier;
11use ab_core_primitives::block::body::{BeaconChainBody, IntermediateShardBlocksInfo};
12use ab_core_primitives::block::header::{
13    BeaconChainHeader, BlockHeaderConsensusParameters, BlockHeaderPrefix,
14    OwnedBlockHeaderConsensusParameters,
15};
16use ab_core_primitives::block::owned::OwnedBeaconChainBlock;
17use ab_core_primitives::block::{BlockNumber, BlockRoot, BlockTimestamp};
18use ab_core_primitives::hashes::Blake3Hash;
19use ab_core_primitives::pot::{PotCheckpoints, PotOutput, PotParametersChange, SlotNumber};
20use ab_core_primitives::segments::SegmentRoot;
21use ab_core_primitives::shard::ShardIndex;
22use ab_core_primitives::solutions::{SolutionVerifyError, SolutionVerifyParams};
23use ab_proof_of_space::Table;
24use rand::prelude::*;
25use rayon::prelude::*;
26use std::iter;
27use std::marker::PhantomData;
28use std::time::SystemTime;
29use tracing::{debug, trace};
30
31/// Errors for [`BeaconChainBlockVerification`]
32#[derive(Debug, thiserror::Error)]
33pub enum BeaconChainBlockVerificationError {
34    /// Consensus parameters derivation error
35    #[error("Consensus parameters derivation error: {error}")]
36    ConsensusParametersDerivation {
37        /// Consensus parameters derivation error
38        #[from]
39        error: DeriveConsensusParametersError,
40    },
41    /// Invalid consensus parameters
42    #[error("Invalid consensus parameters: expected {expected:?}, actual {actual:?}")]
43    InvalidConsensusParameters {
44        /// Expected consensus parameters
45        expected: Box<OwnedBlockHeaderConsensusParameters>,
46        /// Actual consensus parameters
47        actual: Box<OwnedBlockHeaderConsensusParameters>,
48    },
49    /// Invalid PoT checkpoints
50    #[error("Invalid PoT checkpoints")]
51    InvalidPotCheckpoints,
52    /// Invalid proof of time
53    #[error("Invalid proof of time")]
54    InvalidProofOfTime,
55    /// Solution error
56    #[error("Solution error: {error}")]
57    SolutionError {
58        /// Solution error
59        #[from]
60        error: SolutionVerifyError,
61    },
62}
63
64impl From<BeaconChainBlockVerificationError> for BlockVerificationError {
65    #[inline(always)]
66    fn from(error: BeaconChainBlockVerificationError) -> Self {
67        Self::Custom {
68            error: error.into(),
69        }
70    }
71}
72
73#[derive(Debug)]
74pub struct BeaconChainBlockVerification<PosTable, CI, CSS> {
75    consensus_constants: ConsensusConstants,
76    pot_verifier: PotVerifier,
77    chain_info: CI,
78    chain_sync_status: CSS,
79    _pos_table: PhantomData<PosTable>,
80}
81
82impl<PosTable, CI, CSS> BlockVerification<OwnedBeaconChainBlock>
83    for BeaconChainBlockVerification<PosTable, CI, CSS>
84where
85    PosTable: Table,
86    CI: ChainInfo<OwnedBeaconChainBlock>,
87    CSS: ChainSyncStatus,
88{
89    #[inline(always)]
90    async fn verify_concurrent<BCI>(
91        &self,
92        parent_header: &GenericHeader<'_, OwnedBeaconChainBlock>,
93        parent_block_mmr_root: &Blake3Hash,
94        header: &GenericHeader<'_, OwnedBeaconChainBlock>,
95        body: &GenericBody<'_, OwnedBeaconChainBlock>,
96        origin: &BlockOrigin,
97        beacon_chain_info: &BCI,
98    ) -> Result<(), BlockVerificationError>
99    where
100        BCI: DeriveConsensusParametersChainInfo + ShardMembershipEntropySourceChainInfo,
101    {
102        self.verify_concurrent(
103            parent_header,
104            parent_block_mmr_root,
105            header,
106            body,
107            origin,
108            beacon_chain_info,
109        )
110        .await
111    }
112
113    #[inline(always)]
114    async fn verify_sequential(
115        &self,
116        parent_header: &GenericHeader<'_, OwnedBeaconChainBlock>,
117        parent_block_mmr_root: &Blake3Hash,
118        header: &GenericHeader<'_, OwnedBeaconChainBlock>,
119        body: &GenericBody<'_, OwnedBeaconChainBlock>,
120        origin: &BlockOrigin,
121    ) -> Result<(), BlockVerificationError> {
122        self.verify_sequential(parent_header, parent_block_mmr_root, header, body, origin)
123            .await
124    }
125}
126
127impl<PosTable, CI, CSS> BeaconChainBlockVerification<PosTable, CI, CSS>
128where
129    PosTable: Table,
130    CI: ChainInfo<OwnedBeaconChainBlock>,
131    CSS: ChainSyncStatus,
132{
133    /// Create a new instance
134    #[inline(always)]
135    pub fn new(
136        consensus_constants: ConsensusConstants,
137        pot_verifier: PotVerifier,
138        chain_info: CI,
139        chain_sync_status: CSS,
140    ) -> Self {
141        Self {
142            consensus_constants,
143            pot_verifier,
144            chain_info,
145            chain_sync_status,
146            _pos_table: PhantomData,
147        }
148    }
149
150    /// Determine if full proof of time verification is needed for this block number
151    fn full_pot_verification(&self, block_number: BlockNumber) -> bool {
152        let sync_target_block_number = self.chain_sync_status.target_block_number();
153        let Some(diff) = sync_target_block_number.checked_sub(block_number) else {
154            return true;
155        };
156        let diff = diff.as_u64();
157
158        let sample_size = match diff {
159            ..=1_581 => {
160                return true;
161            }
162            1_582..=6_234 => 1_581,
163            6_235..=63_240 => 3_162 * (diff - 3_162) / (diff - 1),
164            63_241..=3_162_000 => 3_162,
165            _ => diff / 1_000,
166        };
167
168        let n = rand::rng().random_range(0..=diff);
169
170        n < sample_size
171    }
172
173    fn check_header_prefix(
174        &self,
175        parent_header_prefix: &BlockHeaderPrefix,
176        parent_block_mmr_root: &Blake3Hash,
177        header_prefix: &BlockHeaderPrefix,
178    ) -> Result<(), BlockVerificationError> {
179        let basic_valid = header_prefix.number == parent_header_prefix.number + BlockNumber::ONE
180            && header_prefix.shard_index == parent_header_prefix.shard_index
181            && &header_prefix.mmr_root == parent_block_mmr_root
182            && header_prefix.timestamp > parent_header_prefix.timestamp;
183
184        if !basic_valid {
185            return Err(BlockVerificationError::InvalidHeaderPrefix);
186        }
187
188        let timestamp_now = SystemTime::now()
189            .duration_since(SystemTime::UNIX_EPOCH)
190            .unwrap_or_default()
191            .as_millis();
192        let timestamp_now =
193            BlockTimestamp::from_millis(u64::try_from(timestamp_now).unwrap_or(u64::MAX));
194
195        if header_prefix.timestamp
196            > timestamp_now.saturating_add(self.consensus_constants.max_block_timestamp_drift)
197        {
198            return Err(BlockVerificationError::TimestampTooFarInTheFuture);
199        }
200
201        Ok(())
202    }
203
204    fn check_consensus_parameters<BCI>(
205        &self,
206        parent_block_root: &BlockRoot,
207        parent_header: &BeaconChainHeader<'_>,
208        header: &BeaconChainHeader<'_>,
209        beacon_chain_info: &BCI,
210    ) -> Result<(), BeaconChainBlockVerificationError>
211    where
212        BCI: DeriveConsensusParametersChainInfo,
213    {
214        let derived_consensus_parameters = derive_consensus_parameters(
215            &self.consensus_constants,
216            beacon_chain_info,
217            parent_block_root,
218            parent_header.consensus_parameters(),
219            parent_header.consensus_info.slot,
220            header.prefix.number,
221            header.consensus_info.slot,
222        )?;
223
224        let expected_consensus_parameters = OwnedBlockHeaderConsensusParameters {
225            fixed_parameters: derived_consensus_parameters.fixed_parameters,
226            // TODO: Super segment support
227            super_segment_root: None,
228            next_solution_range: derived_consensus_parameters.next_solution_range,
229            pot_parameters_change: derived_consensus_parameters.pot_parameters_change,
230        };
231
232        if header.consensus_parameters() != &expected_consensus_parameters.as_ref() {
233            return Err(
234                BeaconChainBlockVerificationError::InvalidConsensusParameters {
235                    expected: Box::new(expected_consensus_parameters),
236                    actual: Box::new(OwnedBlockHeaderConsensusParameters {
237                        fixed_parameters: header.consensus_parameters().fixed_parameters,
238                        super_segment_root: header
239                            .consensus_parameters()
240                            .super_segment_root
241                            .copied(),
242                        next_solution_range: header.consensus_parameters().next_solution_range,
243                        pot_parameters_change: header
244                            .consensus_parameters()
245                            .pot_parameters_change
246                            .copied(),
247                    }),
248                },
249            );
250        }
251
252        Ok(())
253    }
254
255    // TODO: This is a blocking function, but ideally wouldn't be block an executor
256    /// Checks current/future proof of time in the consensus info for the slot and corresponding
257    /// checkpoints.
258    ///
259    /// `consensus_parameters` is assumed to be correct and needs to be verified separately.
260    ///
261    /// When `verify_checkpoints == false` checkpoints are assumed to be correct and verification
262    /// for them is skipped.
263    #[expect(
264        clippy::too_many_arguments,
265        reason = "Explicit minimal input for better testability"
266    )]
267    fn check_proof_of_time(
268        pot_verifier: &PotVerifier,
269        block_authoring_delay: SlotNumber,
270        parent_slot: SlotNumber,
271        parent_proof_of_time: PotOutput,
272        parent_future_proof_of_time: PotOutput,
273        parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
274        slot: SlotNumber,
275        proof_of_time: PotOutput,
276        future_proof_of_time: PotOutput,
277        checkpoints: &[PotCheckpoints],
278        verify_checkpoints: bool,
279    ) -> Result<(), BeaconChainBlockVerificationError> {
280        let parent_pot_parameters_change = parent_consensus_parameters
281            .pot_parameters_change
282            .copied()
283            .map(PotParametersChange::from);
284
285        // The last checkpoint must be the future proof of time
286        if checkpoints.last().map(PotCheckpoints::output) != Some(future_proof_of_time) {
287            return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
288        }
289
290        let future_slot = slot + block_authoring_delay;
291        let parent_future_slot = if parent_slot == SlotNumber::ZERO {
292            parent_slot
293        } else {
294            parent_slot + block_authoring_delay
295        };
296
297        let slots_between_blocks = slot
298            .checked_sub(parent_slot)
299            .ok_or(BeaconChainBlockVerificationError::InvalidPotCheckpoints)?;
300        // The number of checkpoints must match the difference between parent's and this block's
301        // future slots. This also implicitly checks that there is a non-zero number of slots
302        // between this and parent block because the list of checkpoints is already known to be not
303        // empty from the check above.
304        //
305        // The first block after genesis is a special case and is handled separately here.
306        if !(slots_between_blocks.as_u64() == checkpoints.len() as u64
307            || (parent_slot == SlotNumber::ZERO
308                && future_slot.as_u64() == checkpoints.len() as u64))
309        {
310            return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
311        }
312
313        let mut pot_input = if parent_slot == SlotNumber::ZERO {
314            PotNextSlotInput {
315                slot: parent_slot + SlotNumber::ONE,
316                slot_iterations: parent_consensus_parameters.fixed_parameters.slot_iterations,
317                seed: pot_verifier.genesis_seed(),
318            }
319        } else {
320            // Calculate slot iterations as of parent future slot
321            let slot_iterations = parent_pot_parameters_change
322                .and_then(|parameters_change| {
323                    (parameters_change.slot <= parent_future_slot)
324                        .then_some(parameters_change.slot_iterations)
325                })
326                .unwrap_or(parent_consensus_parameters.fixed_parameters.slot_iterations);
327            // Derive inputs to the slot, which follows the parent future slot
328            PotNextSlotInput::derive(
329                slot_iterations,
330                parent_future_slot,
331                parent_future_proof_of_time,
332                &parent_pot_parameters_change,
333            )
334        };
335
336        // Collect all the data we will use for verification so we can process it in parallel
337        let checkpoints_verification_input = iter::once((
338            pot_input,
339            *checkpoints
340                .first()
341                .expect("Not empty, contents was checked above; qed"),
342        ));
343        let checkpoints_verification_input = checkpoints_verification_input
344            .chain(checkpoints.array_windows::<2>().map(|[left, right]| {
345                pot_input = PotNextSlotInput::derive(
346                    pot_input.slot_iterations,
347                    pot_input.slot,
348                    left.output(),
349                    &parent_pot_parameters_change,
350                );
351
352                (pot_input, *right)
353            }))
354            // TODO: Would be nice to avoid extra allocation here
355            .collect::<Vec<_>>();
356
357        // All checkpoints must be valid, search for the first verification failure
358        let all_checkpoints_valid =
359            checkpoints_verification_input
360                .into_par_iter()
361                .all(|(pot_input, checkpoints)| {
362                    if verify_checkpoints {
363                        pot_verifier.verify_checkpoints(
364                            pot_input.seed,
365                            pot_input.slot_iterations,
366                            &checkpoints,
367                        )
368                    } else {
369                        // Store checkpoints as verified when verification is skipped
370                        pot_verifier.inject_verified_checkpoints(
371                            pot_input.seed,
372                            pot_input.slot_iterations,
373                            checkpoints,
374                        );
375                        true
376                    }
377                });
378
379        if !all_checkpoints_valid {
380            return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
381        }
382
383        // Make sure proof of time of this block correctly extends proof of time of the parent block
384        {
385            let pot_input = if parent_slot == SlotNumber::ZERO {
386                PotNextSlotInput {
387                    slot: parent_slot + SlotNumber::ONE,
388                    slot_iterations: parent_consensus_parameters.fixed_parameters.slot_iterations,
389                    seed: pot_verifier.genesis_seed(),
390                }
391            } else {
392                // Calculate slot iterations as of the parent slot
393                let slot_iterations = parent_pot_parameters_change
394                    .and_then(|parameters_change| {
395                        (parameters_change.slot <= parent_slot)
396                            .then_some(parameters_change.slot_iterations)
397                    })
398                    .unwrap_or(parent_consensus_parameters.fixed_parameters.slot_iterations);
399                // Derive inputs to the slot, which follows the parent slot
400                PotNextSlotInput::derive(
401                    slot_iterations,
402                    parent_slot,
403                    parent_proof_of_time,
404                    &parent_pot_parameters_change,
405                )
406            };
407
408            if !pot_verifier.is_output_valid(
409                pot_input,
410                slots_between_blocks,
411                proof_of_time,
412                parent_pot_parameters_change,
413            ) {
414                return Err(BeaconChainBlockVerificationError::InvalidProofOfTime);
415            }
416        }
417
418        Ok(())
419    }
420
421    fn check_body(
422        &self,
423        block_number: BlockNumber,
424        own_segment_roots: &[SegmentRoot],
425        _intermediate_shard_blocks: &IntermediateShardBlocksInfo<'_>,
426    ) -> Result<(), BlockVerificationError> {
427        let expected_segment_headers = self.chain_info.segment_headers_for_block(block_number);
428        let correct_segment_roots = expected_segment_headers
429            .iter()
430            .map(|segment_header| &segment_header.segment_root)
431            .eq(own_segment_roots);
432        if !correct_segment_roots {
433            return Err(BlockVerificationError::InvalidOwnSegmentRoots {
434                expected: expected_segment_headers
435                    .iter()
436                    .map(|segment_header| segment_header.segment_root)
437                    .collect(),
438                actual: own_segment_roots.to_vec(),
439            });
440        }
441
442        // TODO: check intermediate shard blocks
443
444        Ok(())
445    }
446
447    async fn verify_concurrent<BCI>(
448        &self,
449        parent_header: &BeaconChainHeader<'_>,
450        parent_block_mmr_root: &Blake3Hash,
451        header: &BeaconChainHeader<'_>,
452        body: &BeaconChainBody<'_>,
453        _origin: &BlockOrigin,
454        beacon_chain_info: &BCI,
455    ) -> Result<(), BlockVerificationError>
456    where
457        BCI: DeriveConsensusParametersChainInfo + ShardMembershipEntropySourceChainInfo,
458    {
459        trace!(header = ?header, "Verify concurrent");
460
461        let parent_block_root = parent_header.root();
462
463        let block_number = header.prefix.number;
464        let consensus_info = header.consensus_info;
465        let consensus_parameters = header.consensus_parameters();
466        let slot = consensus_info.slot;
467
468        let best_header = self.chain_info.best_header();
469        let best_header = best_header.header();
470        let best_number = best_header.prefix.number;
471
472        // Reject block below archiving point
473        if block_number + self.consensus_constants.confirmation_depth_k < best_number {
474            debug!(
475                ?header,
476                %best_number,
477                "Rejecting a block below the archiving point"
478            );
479
480            return Err(BlockVerificationError::BelowArchivingPoint);
481        }
482
483        self.check_header_prefix(parent_header.prefix, parent_block_mmr_root, header.prefix)?;
484
485        self.check_consensus_parameters(
486            &parent_block_root,
487            parent_header,
488            header,
489            beacon_chain_info,
490        )?;
491
492        if !header.is_sealed_correctly() {
493            return Err(BlockVerificationError::InvalidSeal);
494        }
495
496        // Find shard membership entropy for the slot
497        let shard_membership_entropy = shard_membership_entropy_source(
498            header.prefix.number,
499            best_header,
500            self.consensus_constants.shard_rotation_interval,
501            self.consensus_constants.shard_rotation_delay,
502            beacon_chain_info,
503        )?;
504
505        // Verify that the solution is valid
506        consensus_info
507            .solution
508            .verify::<PosTable>(
509                slot,
510                &SolutionVerifyParams {
511                    shard_index: ShardIndex::BEACON_CHAIN,
512                    proof_of_time: consensus_info.proof_of_time,
513                    solution_range: consensus_parameters.fixed_parameters.solution_range,
514                    shard_membership_entropy,
515                    num_shards: consensus_parameters.fixed_parameters.num_shards,
516                    // TODO: Piece check parameters
517                    piece_check_params: None,
518                },
519            )
520            .map_err(BeaconChainBlockVerificationError::from)?;
521
522        Self::check_proof_of_time(
523            &self.pot_verifier,
524            self.consensus_constants.block_authoring_delay,
525            parent_header.consensus_info.slot,
526            parent_header.consensus_info.proof_of_time,
527            parent_header.consensus_info.future_proof_of_time,
528            parent_header.consensus_parameters(),
529            consensus_info.slot,
530            consensus_info.proof_of_time,
531            consensus_info.future_proof_of_time,
532            body.pot_checkpoints(),
533            self.full_pot_verification(block_number),
534        )?;
535
536        // TODO: Do something about equivocation?
537
538        Ok(())
539    }
540
541    async fn verify_sequential(
542        &self,
543        // TODO: Probable remove these unused arguments
544        _parent_header: &BeaconChainHeader<'_>,
545        _parent_block_mmr_root: &Blake3Hash,
546        header: &BeaconChainHeader<'_>,
547        body: &BeaconChainBody<'_>,
548        _origin: &BlockOrigin,
549    ) -> Result<(), BlockVerificationError> {
550        trace!(header = ?header, "Verify sequential");
551
552        let block_number = header.prefix.number;
553
554        let best_header = self.chain_info.best_header();
555        let best_header = best_header.header();
556        let best_number = best_header.prefix.number;
557
558        // Reject block below archiving point
559        if block_number + self.consensus_constants.confirmation_depth_k < best_number {
560            debug!(
561                ?header,
562                %best_number,
563                "Rejecting a block below the archiving point"
564            );
565
566            return Err(BlockVerificationError::BelowArchivingPoint);
567        }
568
569        self.check_body(
570            block_number,
571            body.own_segment_roots(),
572            body.intermediate_shard_blocks(),
573        )?;
574
575        Ok(())
576    }
577}