1use crate::{BlockVerification, BlockVerificationError, GenericBody, GenericHeader};
2use ab_client_api::{BeaconChainInfo, BlockOrigin, ChainSyncStatus};
3use ab_client_consensus_common::ConsensusConstants;
4use ab_client_consensus_common::consensus_parameters::{
5 DeriveConsensusParametersChainInfo, DeriveConsensusParametersError,
6 DeriveSuperSegmentForBlockError, ShardMembershipEntropySourceChainInfo,
7 derive_consensus_parameters, derive_super_segments_for_block, 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::segments::{
21 HistorySize, SuperSegment, SuperSegmentIndex, SuperSegmentRoot,
22};
23use ab_core_primitives::shard::ShardIndex;
24use ab_core_primitives::solutions::{
25 SolutionVerifyError, SolutionVerifyPieceParams, SolutionVerifyStatelessParams,
26};
27use ab_proof_of_space::Table;
28use rand::prelude::*;
29use rayon::prelude::*;
30use std::future::ready;
31use std::iter;
32use std::marker::PhantomData;
33use std::time::SystemTime;
34use tracing::{debug, trace};
35
36#[derive(Debug, thiserror::Error)]
38pub enum BeaconChainBlockVerificationError {
39 #[error("Consensus parameters derivation error: {error}")]
41 ConsensusParametersDerivation {
42 #[from]
44 error: DeriveConsensusParametersError,
45 },
46 #[error("Super segment derivation error: {error}")]
48 SuperSegmentDerivation {
49 #[from]
51 error: DeriveSuperSegmentForBlockError,
52 },
53 #[error("Invalid consensus parameters: expected {expected:?}, actual {actual:?}")]
55 InvalidConsensusParameters {
56 expected: Box<OwnedBlockHeaderConsensusParameters>,
58 actual: Box<OwnedBlockHeaderConsensusParameters>,
60 },
61 #[error("Missing super segment in the first block")]
63 MissingSuperSegmentInFirstBlock,
64 #[error("Previous super segment header not found")]
66 PreviousSuperSegmentHeaderNotFound,
67 #[error("Solution super segment {index} not found")]
69 SolutionSuperSegmentNotFound {
70 index: SuperSegmentIndex,
72 },
73 #[error("Invalid history size {history_size} (current {current_history_size})")]
75 InvalidHistorySize {
76 history_size: HistorySize,
78 current_history_size: HistorySize,
80 },
81 #[error("Invalid super segment root: expected {expected:?}, actual {actual:?}")]
83 InvalidSuperSegmentRoot {
84 expected: Box<Option<SuperSegmentRoot>>,
86 actual: Box<Option<SuperSegmentRoot>>,
88 },
89 #[error("Invalid PoT checkpoints")]
91 InvalidPotCheckpoints,
92 #[error("Invalid proof of time")]
94 InvalidProofOfTime,
95 #[error("Solution error: {error}")]
97 SolutionError {
98 #[from]
100 error: SolutionVerifyError,
101 },
102}
103
104impl From<BeaconChainBlockVerificationError> for BlockVerificationError {
105 #[inline(always)]
106 fn from(error: BeaconChainBlockVerificationError) -> Self {
107 Self::Custom {
108 error: error.into(),
109 }
110 }
111}
112
113#[derive(Debug)]
114pub struct BeaconChainBlockVerification<PosTable, CI, CSS> {
115 consensus_constants: ConsensusConstants,
116 pot_verifier: PotVerifier,
117 chain_info: CI,
118 chain_sync_status: CSS,
119 _pos_table: PhantomData<PosTable>,
120}
121
122impl<PosTable, CI, CSS> BlockVerification<OwnedBeaconChainBlock, Option<SuperSegment>>
123 for BeaconChainBlockVerification<PosTable, CI, CSS>
124where
125 PosTable: Table,
126 CI: BeaconChainInfo,
127 CSS: ChainSyncStatus,
128{
129 #[inline(always)]
130 fn verify_concurrent<BCI>(
131 &self,
132 parent_header: &GenericHeader<'_, OwnedBeaconChainBlock>,
133 parent_block_mmr_root: &Blake3Hash,
134 header: &GenericHeader<'_, OwnedBeaconChainBlock>,
135 body: &GenericBody<'_, OwnedBeaconChainBlock>,
136 origin: &BlockOrigin,
137 beacon_chain_info: &BCI,
138 ) -> impl Future<Output = Result<(), BlockVerificationError>>
139 where
140 BCI: DeriveConsensusParametersChainInfo + ShardMembershipEntropySourceChainInfo,
141 {
142 ready(self.verify_concurrent(
143 parent_header,
144 parent_block_mmr_root,
145 header,
146 body,
147 origin,
148 beacon_chain_info,
149 ))
150 }
151
152 #[inline(always)]
153 fn verify_sequential(
154 &self,
155 parent_header: &GenericHeader<'_, OwnedBeaconChainBlock>,
156 parent_block_mmr_root: &Blake3Hash,
157 header: &GenericHeader<'_, OwnedBeaconChainBlock>,
158 body: &GenericBody<'_, OwnedBeaconChainBlock>,
159 origin: &BlockOrigin,
160 ) -> impl Future<Output = Result<Option<SuperSegment>, BlockVerificationError>> {
161 ready(self.verify_sequential(parent_header, parent_block_mmr_root, header, body, origin))
162 }
163}
164
165impl<PosTable, CI, CSS> BeaconChainBlockVerification<PosTable, CI, CSS>
166where
167 PosTable: Table,
168 CI: BeaconChainInfo,
169 CSS: ChainSyncStatus,
170{
171 #[inline(always)]
173 pub fn new(
174 consensus_constants: ConsensusConstants,
175 pot_verifier: PotVerifier,
176 chain_info: CI,
177 chain_sync_status: CSS,
178 ) -> Self {
179 Self {
180 consensus_constants,
181 pot_verifier,
182 chain_info,
183 chain_sync_status,
184 _pos_table: PhantomData,
185 }
186 }
187
188 fn full_pot_verification(&self, block_number: BlockNumber) -> bool {
190 let sync_target_block_number = self.chain_sync_status.target_block_number();
191 let Some(diff) = sync_target_block_number.checked_sub(block_number) else {
192 return true;
193 };
194 let diff = u64::from(diff);
195
196 let sample_size = match diff {
197 ..=1_581 => {
198 return true;
199 }
200 1_582..=6_234 => 1_581,
201 6_235..=63_240 => 3_162 * (diff - 3_162) / (diff - 1),
202 63_241..=3_162_000 => 3_162,
203 _ => diff / 1_000,
204 };
205
206 let n = rand::rng().random_range(0..=diff);
207
208 n < sample_size
209 }
210
211 fn check_header_prefix(
212 &self,
213 parent_header_prefix: &BlockHeaderPrefix,
214 parent_block_mmr_root: &Blake3Hash,
215 header_prefix: &BlockHeaderPrefix,
216 ) -> Result<(), BlockVerificationError> {
217 let basic_valid = header_prefix.number == parent_header_prefix.number + BlockNumber::ONE
218 && header_prefix.shard_index == parent_header_prefix.shard_index
219 && &header_prefix.mmr_root == parent_block_mmr_root
220 && header_prefix.timestamp > parent_header_prefix.timestamp;
221
222 if !basic_valid {
223 return Err(BlockVerificationError::InvalidHeaderPrefix);
224 }
225
226 let timestamp_now = SystemTime::now()
227 .duration_since(SystemTime::UNIX_EPOCH)
228 .unwrap_or_default()
229 .as_millis();
230 let timestamp_now =
231 BlockTimestamp::from_millis(u64::try_from(timestamp_now).unwrap_or(u64::MAX));
232
233 if header_prefix.timestamp
234 > timestamp_now.saturating_add(self.consensus_constants.max_block_timestamp_drift)
235 {
236 return Err(BlockVerificationError::TimestampTooFarInTheFuture);
237 }
238
239 Ok(())
240 }
241
242 fn check_consensus_parameters_concurrent<BCI>(
243 &self,
244 parent_block_root: &BlockRoot,
245 parent_header: &BeaconChainHeader<'_>,
246 header: &BeaconChainHeader<'_>,
247 beacon_chain_info: &BCI,
248 ) -> Result<(), BeaconChainBlockVerificationError>
249 where
250 BCI: DeriveConsensusParametersChainInfo,
251 {
252 let derived_consensus_parameters = derive_consensus_parameters(
253 &self.consensus_constants,
254 beacon_chain_info,
255 parent_block_root,
256 parent_header.consensus_parameters(),
257 parent_header.consensus_info.slot,
258 header.prefix.number,
259 header.consensus_info.slot,
260 )?;
261
262 let expected_consensus_parameters = OwnedBlockHeaderConsensusParameters {
263 fixed_parameters: derived_consensus_parameters.fixed_parameters,
264 super_segment_root: header.consensus_parameters().super_segment_root.copied(),
266 next_solution_range: derived_consensus_parameters.next_solution_range,
267 pot_parameters_change: derived_consensus_parameters.pot_parameters_change,
268 };
269
270 if header.consensus_parameters() != &expected_consensus_parameters.as_ref() {
271 return Err(
272 BeaconChainBlockVerificationError::InvalidConsensusParameters {
273 expected: Box::new(expected_consensus_parameters),
274 actual: Box::new(OwnedBlockHeaderConsensusParameters {
275 fixed_parameters: header.consensus_parameters().fixed_parameters,
276 super_segment_root: header
277 .consensus_parameters()
278 .super_segment_root
279 .copied(),
280 next_solution_range: header.consensus_parameters().next_solution_range,
281 pot_parameters_change: header
282 .consensus_parameters()
283 .pot_parameters_change
284 .copied(),
285 }),
286 },
287 );
288 }
289
290 Ok(())
291 }
292
293 #[expect(
302 clippy::too_many_arguments,
303 reason = "Explicit minimal input for better testability"
304 )]
305 fn check_proof_of_time(
306 pot_verifier: &PotVerifier,
307 block_authoring_delay: SlotNumber,
308 parent_slot: SlotNumber,
309 parent_proof_of_time: PotOutput,
310 parent_future_proof_of_time: PotOutput,
311 parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
312 slot: SlotNumber,
313 proof_of_time: PotOutput,
314 future_proof_of_time: PotOutput,
315 checkpoints: &[PotCheckpoints],
316 verify_checkpoints: bool,
317 ) -> Result<(), BeaconChainBlockVerificationError> {
318 let parent_pot_parameters_change = parent_consensus_parameters
319 .pot_parameters_change
320 .copied()
321 .map(PotParametersChange::from);
322
323 if checkpoints.last().map(PotCheckpoints::output) != Some(future_proof_of_time) {
325 return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
326 }
327
328 let future_slot = slot + block_authoring_delay;
329 let parent_future_slot = if parent_slot == SlotNumber::ZERO {
330 parent_slot
331 } else {
332 parent_slot + block_authoring_delay
333 };
334
335 let slots_between_blocks = slot
336 .checked_sub(parent_slot)
337 .ok_or(BeaconChainBlockVerificationError::InvalidPotCheckpoints)?;
338 if !(u64::from(slots_between_blocks) == checkpoints.len() as u64
345 || (parent_slot == SlotNumber::ZERO
346 && u64::from(future_slot) == checkpoints.len() as u64))
347 {
348 return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
349 }
350
351 let mut pot_input = if parent_slot == SlotNumber::ZERO {
352 PotNextSlotInput {
353 slot: parent_slot + SlotNumber::ONE,
354 slot_iterations: parent_consensus_parameters.fixed_parameters.slot_iterations,
355 seed: pot_verifier.genesis_seed(),
356 }
357 } else {
358 let slot_iterations = parent_pot_parameters_change
360 .and_then(|parameters_change| {
361 (parameters_change.slot <= parent_future_slot)
362 .then_some(parameters_change.slot_iterations)
363 })
364 .unwrap_or(parent_consensus_parameters.fixed_parameters.slot_iterations);
365 PotNextSlotInput::derive(
367 slot_iterations,
368 parent_future_slot,
369 parent_future_proof_of_time,
370 &parent_pot_parameters_change,
371 )
372 };
373
374 let checkpoints_verification_input = iter::once((
376 pot_input,
377 *checkpoints
378 .first()
379 .expect("Not empty, contents was checked above; qed"),
380 ));
381 let checkpoints_verification_input = checkpoints_verification_input
382 .chain(checkpoints.array_windows::<2>().map(|[left, right]| {
383 pot_input = PotNextSlotInput::derive(
384 pot_input.slot_iterations,
385 pot_input.slot,
386 left.output(),
387 &parent_pot_parameters_change,
388 );
389
390 (pot_input, *right)
391 }))
392 .collect::<Vec<_>>();
394
395 let all_checkpoints_valid =
397 checkpoints_verification_input
398 .into_par_iter()
399 .all(|(pot_input, checkpoints)| {
400 if verify_checkpoints {
401 pot_verifier.verify_checkpoints(
402 pot_input.seed,
403 pot_input.slot_iterations,
404 &checkpoints,
405 )
406 } else {
407 pot_verifier.inject_verified_checkpoints(
409 pot_input.seed,
410 pot_input.slot_iterations,
411 checkpoints,
412 );
413 true
414 }
415 });
416
417 if !all_checkpoints_valid {
418 return Err(BeaconChainBlockVerificationError::InvalidPotCheckpoints);
419 }
420
421 {
423 let pot_input = if parent_slot == SlotNumber::ZERO {
424 PotNextSlotInput {
425 slot: parent_slot + SlotNumber::ONE,
426 slot_iterations: parent_consensus_parameters.fixed_parameters.slot_iterations,
427 seed: pot_verifier.genesis_seed(),
428 }
429 } else {
430 let slot_iterations = parent_pot_parameters_change
432 .and_then(|parameters_change| {
433 (parameters_change.slot <= parent_slot)
434 .then_some(parameters_change.slot_iterations)
435 })
436 .unwrap_or(parent_consensus_parameters.fixed_parameters.slot_iterations);
437 PotNextSlotInput::derive(
439 slot_iterations,
440 parent_slot,
441 parent_proof_of_time,
442 &parent_pot_parameters_change,
443 )
444 };
445
446 if !pot_verifier.is_output_valid(
447 pot_input,
448 slots_between_blocks,
449 proof_of_time,
450 parent_pot_parameters_change,
451 ) {
452 return Err(BeaconChainBlockVerificationError::InvalidProofOfTime);
453 }
454 }
455
456 Ok(())
457 }
458
459 fn check_body(
460 &self,
461 block_number: BlockNumber,
462 own_segments: Option<OwnSegments<'_>>,
463 _intermediate_shard_blocks: &IntermediateShardBlocksInfo<'_>,
464 ) -> Result<(), BlockVerificationError> {
465 let expected_segment_headers = self.chain_info.segment_headers_for_block(block_number);
466 let expected_first_local_segment_index = expected_segment_headers
467 .first()
468 .map(|segment_header| segment_header.index.as_inner());
469 let correct_first_local_segment_index = expected_first_local_segment_index
470 == own_segments
471 .as_ref()
472 .map(|own_segments| own_segments.first_local_segment_index);
473 let correct_segment_roots = expected_segment_headers
474 .iter()
475 .map(|segment_header| &segment_header.root)
476 .eq(own_segments
477 .as_ref()
478 .map(|own_segments| own_segments.segment_roots)
479 .unwrap_or_default());
480 if !(correct_first_local_segment_index && correct_segment_roots) {
481 return Err(BlockVerificationError::InvalidOwnSegments {
482 expected_first_local_segment_index,
483 expected_segment_roots: expected_segment_headers
484 .iter()
485 .map(|segment_header| segment_header.root)
486 .collect(),
487 actual_first_local_segment_index: own_segments
488 .as_ref()
489 .map(|own_segments| own_segments.first_local_segment_index),
490 actual_segment_roots: own_segments
491 .as_ref()
492 .map(|own_segments| own_segments.segment_roots.to_vec())
493 .unwrap_or_default(),
494 });
495 }
496
497 Ok(())
500 }
501
502 fn verify_concurrent<BCI>(
503 &self,
504 parent_header: &BeaconChainHeader<'_>,
505 parent_block_mmr_root: &Blake3Hash,
506 header: &BeaconChainHeader<'_>,
507 body: &BeaconChainBody<'_>,
508 _origin: &BlockOrigin,
509 beacon_chain_info: &BCI,
510 ) -> Result<(), BlockVerificationError>
511 where
512 BCI: DeriveConsensusParametersChainInfo + ShardMembershipEntropySourceChainInfo,
513 {
514 trace!(header = ?header, "Verify concurrent");
515
516 let parent_block_root = parent_header.root();
517
518 let block_number = header.prefix.number;
519 let consensus_info = header.consensus_info;
520 let consensus_parameters = header.consensus_parameters();
521 let slot = consensus_info.slot;
522
523 let best_header = self.chain_info.best_header();
524 let best_header = best_header.header();
525 let best_number = best_header.prefix.number;
526
527 if block_number + self.consensus_constants.block_confirmation_depth < best_number {
529 debug!(
530 ?header,
531 %best_number,
532 "Rejecting a block below the archiving point"
533 );
534
535 return Err(BlockVerificationError::BelowArchivingPoint);
536 }
537
538 self.check_header_prefix(parent_header.prefix, parent_block_mmr_root, header.prefix)?;
539
540 self.check_consensus_parameters_concurrent(
541 &parent_block_root,
542 parent_header,
543 header,
544 beacon_chain_info,
545 )?;
546
547 if !header.is_sealed_correctly() {
548 return Err(BlockVerificationError::InvalidSeal);
549 }
550
551 let shard_membership_entropy = shard_membership_entropy_source(
553 header.prefix.number,
554 best_header,
555 self.consensus_constants.shard_rotation_interval,
556 self.consensus_constants.shard_rotation_delay,
557 beacon_chain_info,
558 )?;
559
560 consensus_info
562 .solution
563 .verify_stateless::<PosTable>(
564 slot,
565 &SolutionVerifyStatelessParams {
566 shard_index: ShardIndex::BEACON_CHAIN,
567 proof_of_time: consensus_info.proof_of_time,
568 solution_range: consensus_parameters.fixed_parameters.solution_range,
569 shard_membership_entropy,
570 num_shards: consensus_parameters.fixed_parameters.num_shards,
571 },
572 )
573 .map_err(BeaconChainBlockVerificationError::from)?;
574
575 Self::check_proof_of_time(
576 &self.pot_verifier,
577 self.consensus_constants.block_authoring_delay,
578 parent_header.consensus_info.slot,
579 parent_header.consensus_info.proof_of_time,
580 parent_header.consensus_info.future_proof_of_time,
581 parent_header.consensus_parameters(),
582 consensus_info.slot,
583 consensus_info.proof_of_time,
584 consensus_info.future_proof_of_time,
585 body.pot_checkpoints(),
586 self.full_pot_verification(block_number),
587 )?;
588
589 Ok(())
592 }
593
594 fn verify_sequential(
595 &self,
596 parent_header: &BeaconChainHeader<'_>,
597 _parent_block_mmr_root: &Blake3Hash,
599 header: &BeaconChainHeader<'_>,
600 body: &BeaconChainBody<'_>,
601 _origin: &BlockOrigin,
602 ) -> Result<Option<SuperSegment>, BlockVerificationError> {
603 trace!(header = ?header, "Verify sequential");
604
605 let block_number = header.prefix.number;
606 let consensus_info = header.consensus_info;
607
608 let best_header = self.chain_info.best_header();
609 let best_header = best_header.header();
610 let best_number = best_header.prefix.number;
611
612 if block_number + self.consensus_constants.block_confirmation_depth < best_number {
614 debug!(
615 ?header,
616 %best_number,
617 "Rejecting a block below the archiving point"
618 );
619
620 return Err(BlockVerificationError::BelowArchivingPoint);
621 }
622
623 let maybe_super_segment = derive_super_segments_for_block(
624 &self.chain_info,
625 parent_header.prefix.number,
626 self.consensus_constants.block_confirmation_depth,
627 self.consensus_constants.shard_confirmation_depth,
628 )
629 .map_err(BeaconChainBlockVerificationError::from)?;
630 let maybe_super_segment_root = maybe_super_segment
631 .as_ref()
632 .map(|super_segment| super_segment.header.root);
633
634 if maybe_super_segment_root.as_ref() != header.consensus_parameters().super_segment_root {
635 return Err(BlockVerificationError::from(
636 BeaconChainBlockVerificationError::InvalidSuperSegmentRoot {
637 expected: Box::new(maybe_super_segment_root),
638 actual: Box::new(header.consensus_parameters().super_segment_root.copied()),
639 },
640 ));
641 }
642
643 {
645 let (current_history_size, solution_num_segments, solution_super_segment_root) =
646 if block_number == BlockNumber::ONE {
647 let latest_super_segment = maybe_super_segment.as_ref().ok_or(
648 BeaconChainBlockVerificationError::MissingSuperSegmentInFirstBlock,
649 )?;
650
651 (
652 HistorySize::ONE,
653 latest_super_segment.header.num_segments,
654 latest_super_segment.header.root,
655 )
656 } else {
657 let max_segment_index = self
658 .chain_info
659 .previous_super_segment_header(block_number)
660 .ok_or(
661 BeaconChainBlockVerificationError::PreviousSuperSegmentHeaderNotFound,
662 )?
663 .max_segment_index
664 .as_inner();
665 let current_history_size = HistorySize::from(max_segment_index);
666
667 let solution_super_segment_header = self
668 .chain_info
669 .get_super_segment_header(consensus_info.solution.piece_super_segment_index)
670 .ok_or(
671 BeaconChainBlockVerificationError::SolutionSuperSegmentNotFound {
672 index: consensus_info.solution.piece_super_segment_index,
673 },
674 )?;
675
676 (
677 current_history_size,
678 solution_super_segment_header.num_segments,
679 solution_super_segment_header.root,
680 )
681 };
682
683 let sector_expiration_check_super_segment_root = self
684 .chain_info
685 .get_super_segment_header_for_segment_index(
686 consensus_info
687 .solution
688 .history_size
689 .sector_expiration_check(self.consensus_constants.min_sector_lifetime)
690 .ok_or(BeaconChainBlockVerificationError::InvalidHistorySize {
691 history_size: consensus_info.solution.history_size,
692 current_history_size,
693 })?
694 .segment_index(),
695 )
696 .map(|super_segment_header| super_segment_header.root);
697
698 consensus_info
699 .solution
700 .verify_piece(&SolutionVerifyPieceParams {
701 max_pieces_in_sector: 1000,
703 super_segment_root: solution_super_segment_root,
704 num_segments: solution_num_segments,
705 recent_segments: self.consensus_constants.recent_segments,
706 recent_history_fraction: self.consensus_constants.recent_history_fraction,
707 min_sector_lifetime: self.consensus_constants.min_sector_lifetime,
708 current_history_size,
709 sector_expiration_check_super_segment_root,
710 })
711 .map_err(BeaconChainBlockVerificationError::from)?;
712 }
713
714 self.check_body(
715 block_number,
716 body.own_segments(),
717 body.intermediate_shard_blocks(),
718 )?;
719
720 Ok(maybe_super_segment)
721 }
722}