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