Skip to main content

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