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