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