ab_client_block_verification/
beacon_chain.rs

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