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