Skip to main content

ab_client_consensus_common/
consensus_parameters.rs

1use crate::{ConsensusConstants, PotConsensusConstants};
2use ab_client_api::{BeaconChainInfo, ChainInfo, ShardSegmentRoot, ShardSegmentRootsError};
3use ab_core_primitives::block::header::{
4    BeaconChainHeader, BlockHeaderConsensusInfo, BlockHeaderConsensusParameters,
5    BlockHeaderFixedConsensusParameters, BlockHeaderPotParametersChange,
6};
7use ab_core_primitives::block::owned::OwnedBeaconChainBlock;
8use ab_core_primitives::block::{BlockNumber, BlockRoot};
9use ab_core_primitives::hashes::Blake3Hash;
10use ab_core_primitives::pieces::RecordChunk;
11use ab_core_primitives::pot::{PotOutput, PotParametersChange, SlotNumber};
12use ab_core_primitives::segments::{
13    LocalSegmentIndex, SegmentIndex, SegmentPosition, SegmentRoot, ShardSegmentRootWithPosition,
14    SuperSegment, SuperSegmentHeader, SuperSegmentIndex, SuperSegmentRoot,
15};
16use ab_core_primitives::shard::ShardIndex;
17use ab_core_primitives::solutions::{ShardMembershipEntropy, SolutionRange};
18use std::collections::HashMap;
19use std::num::NonZeroU32;
20use std::sync::Arc as StdArc;
21
22struct SolutionRanges {
23    current: SolutionRange,
24    next: Option<SolutionRange>,
25}
26
27struct PotInfo {
28    slot_iterations: NonZeroU32,
29    parameters_change: Option<PotParametersChange>,
30}
31
32/// Derived consensus parameters, to be eventually turned into
33/// [`OwnedBlockHeaderConsensusParameters`]
34///
35/// [`OwnedBlockHeaderConsensusParameters`]: ab_core_primitives::block::header::OwnedBlockHeaderConsensusParameters
36#[derive(Debug, Copy, Clone)]
37pub struct DerivedConsensusParameters {
38    /// Consensus parameters that are always present
39    pub fixed_parameters: BlockHeaderFixedConsensusParameters,
40    /// Solution range for the next block/interval (if any)
41    pub next_solution_range: Option<SolutionRange>,
42    /// Change of parameters to apply to the proof of time chain (if any)
43    pub pot_parameters_change: Option<BlockHeaderPotParametersChange>,
44}
45
46/// Error for [`derive_consensus_parameters()`]
47#[derive(Debug, thiserror::Error)]
48pub enum DeriveConsensusParametersError {
49    /// Failed to get ancestor header
50    #[error("Failed to get ancestor header")]
51    GetAncestorHeader,
52}
53
54/// A limited subset of [`BlockHeaderConsensusInfo`] for [`derive_consensus_parameters()`]
55#[derive(Debug, Clone, Copy)]
56pub struct DeriveConsensusParametersConsensusInfo {
57    /// Slot number
58    pub slot: SlotNumber,
59    /// Proof of time for this slot
60    pub proof_of_time: PotOutput,
61    /// Record chunk used in a solution
62    pub solution_record_chunk: RecordChunk,
63}
64
65impl DeriveConsensusParametersConsensusInfo {
66    pub fn from_consensus_info(consensus_info: &BlockHeaderConsensusInfo) -> Self {
67        Self {
68            slot: consensus_info.slot,
69            proof_of_time: consensus_info.proof_of_time,
70            solution_record_chunk: consensus_info.solution.chunk,
71        }
72    }
73}
74
75/// Chain info for [`derive_consensus_parameters()`].
76///
77/// Must have access to enough parent blocks.
78pub trait DeriveConsensusParametersChainInfo: Send + Sync {
79    /// Get header of ancestor block number for descendant block root
80    fn ancestor_header_consensus_info(
81        &self,
82        ancestor_block_number: BlockNumber,
83        descendant_block_root: &BlockRoot,
84    ) -> Option<DeriveConsensusParametersConsensusInfo>;
85}
86
87impl<T> DeriveConsensusParametersChainInfo for T
88where
89    T: ChainInfo<OwnedBeaconChainBlock>,
90{
91    fn ancestor_header_consensus_info(
92        &self,
93        ancestor_block_number: BlockNumber,
94        descendant_block_root: &BlockRoot,
95    ) -> Option<DeriveConsensusParametersConsensusInfo> {
96        let header = self.ancestor_header(ancestor_block_number, descendant_block_root)?;
97
98        Some(DeriveConsensusParametersConsensusInfo::from_consensus_info(
99            header.header().consensus_info,
100        ))
101    }
102}
103
104pub fn derive_consensus_parameters<BCI>(
105    consensus_constants: &ConsensusConstants,
106    beacon_chain_info: &BCI,
107    parent_block_root: &BlockRoot,
108    parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
109    parent_slot: SlotNumber,
110    block_number: BlockNumber,
111    slot: SlotNumber,
112) -> Result<DerivedConsensusParameters, DeriveConsensusParametersError>
113where
114    BCI: DeriveConsensusParametersChainInfo,
115{
116    let solution_ranges = derive_solution_ranges(
117        consensus_constants.retarget_interval,
118        consensus_constants.slot_probability,
119        beacon_chain_info,
120        parent_block_root,
121        parent_consensus_parameters.fixed_parameters.solution_range,
122        parent_consensus_parameters.next_solution_range,
123        block_number,
124        slot,
125    )?;
126    let pot_info = derive_pot_info(
127        &consensus_constants.pot,
128        beacon_chain_info,
129        parent_block_root,
130        parent_slot,
131        parent_consensus_parameters.fixed_parameters.slot_iterations,
132        parent_consensus_parameters
133            .pot_parameters_change
134            .copied()
135            .map(PotParametersChange::from),
136        block_number,
137        slot,
138    )?;
139
140    Ok(DerivedConsensusParameters {
141        fixed_parameters: BlockHeaderFixedConsensusParameters {
142            solution_range: solution_ranges.current,
143            slot_iterations: pot_info.slot_iterations,
144            num_shards: parent_consensus_parameters.fixed_parameters.num_shards,
145        },
146        next_solution_range: solution_ranges.next,
147        pot_parameters_change: pot_info
148            .parameters_change
149            .map(BlockHeaderPotParametersChange::from),
150    })
151}
152
153#[expect(
154    clippy::too_many_arguments,
155    reason = "Explicit minimal input for better testability"
156)]
157fn derive_solution_ranges<BCI>(
158    retarget_interval: BlockNumber,
159    slot_probability: (u64, u64),
160    beacon_chain_info: &BCI,
161    parent_block_root: &BlockRoot,
162    solution_range: SolutionRange,
163    next_solution_range: Option<SolutionRange>,
164    block_number: BlockNumber,
165    slot: SlotNumber,
166) -> Result<SolutionRanges, DeriveConsensusParametersError>
167where
168    BCI: DeriveConsensusParametersChainInfo,
169{
170    if let Some(next_solution_range) = next_solution_range {
171        return Ok(SolutionRanges {
172            current: next_solution_range,
173            next: None,
174        });
175    }
176
177    let next_solution_range = if u64::from(block_number)
178        .is_multiple_of(u64::from(retarget_interval))
179        && block_number > retarget_interval
180    {
181        let interval_start_block = block_number.saturating_sub(retarget_interval);
182        let interval_start_slot = beacon_chain_info
183            .ancestor_header_consensus_info(interval_start_block, parent_block_root)
184            .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
185            .slot;
186
187        Some(solution_range.derive_next(
188            slot.saturating_sub(interval_start_slot),
189            slot_probability,
190            retarget_interval,
191        ))
192    } else {
193        None
194    };
195
196    Ok(SolutionRanges {
197        current: solution_range,
198        next: next_solution_range,
199    })
200}
201
202#[expect(
203    clippy::too_many_arguments,
204    reason = "Explicit minimal input for better testability"
205)]
206fn derive_pot_info<BCI>(
207    pot_consensus_constants: &PotConsensusConstants,
208    beacon_chain_info: &BCI,
209    parent_block_root: &BlockRoot,
210    parent_slot: SlotNumber,
211    parent_slot_iterations: NonZeroU32,
212    parent_parameters_change: Option<PotParametersChange>,
213    block_number: BlockNumber,
214    slot: SlotNumber,
215) -> Result<PotInfo, DeriveConsensusParametersError>
216where
217    BCI: DeriveConsensusParametersChainInfo,
218{
219    let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
220    let pot_entropy_injection_lookback_depth =
221        pot_consensus_constants.entropy_injection_lookback_depth;
222    let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
223
224    // Value right after parent block's slot
225    let slot_iterations = if let Some(change) = &parent_parameters_change
226        && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
227    {
228        change.slot_iterations
229    } else {
230        parent_slot_iterations
231    };
232
233    let parameters_change = if let Some(change) = parent_parameters_change
234        && change.slot > slot
235    {
236        // Retain previous PoT parameters change if it applies after the block's slot
237        Some(change)
238    } else {
239        let lookback_in_blocks = BlockNumber::from(
240            u64::from(pot_entropy_injection_interval)
241                * u64::from(pot_entropy_injection_lookback_depth),
242        );
243        let last_entropy_injection_block_number = BlockNumber::from(
244            u64::from(block_number) / u64::from(pot_entropy_injection_interval)
245                * u64::from(pot_entropy_injection_interval),
246        );
247        let maybe_entropy_source_block_number =
248            last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
249
250        // Inject entropy every `pot_entropy_injection_interval` blocks
251        if last_entropy_injection_block_number == block_number
252            && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
253            && entropy_source_block_number > BlockNumber::ZERO
254        {
255            let entropy = {
256                let consensus_info = beacon_chain_info
257                    .ancestor_header_consensus_info(entropy_source_block_number, parent_block_root)
258                    .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
259
260                consensus_info
261                    .proof_of_time
262                    .derive_pot_entropy(&consensus_info.solution_record_chunk)
263            };
264
265            let target_slot = slot
266                .checked_add(pot_entropy_injection_delay)
267                .unwrap_or(SlotNumber::MAX);
268
269            Some(PotParametersChange {
270                slot: target_slot,
271                // TODO: A mechanism to increase (not decrease!) number of iterations if slots
272                //  are created too frequently on long enough timescale, maybe based on the same
273                //  lookback depth as entropy (would be the cleanest and easiest to explain)
274                slot_iterations,
275                entropy,
276            })
277        } else {
278            None
279        }
280    };
281
282    Ok(PotInfo {
283        slot_iterations,
284        parameters_change,
285    })
286}
287
288/// Chain info for [`shard_membership_entropy_source()`].
289///
290/// Must have access to enough parent blocks.
291pub trait ShardMembershipEntropySourceChainInfo: Send + Sync {
292    fn ancestor_header_proof_of_time(
293        &self,
294        ancestor_block_number: BlockNumber,
295        descendant_block_root: &BlockRoot,
296    ) -> Option<PotOutput>;
297}
298
299impl<T> ShardMembershipEntropySourceChainInfo for T
300where
301    T: ChainInfo<OwnedBeaconChainBlock>,
302{
303    fn ancestor_header_proof_of_time(
304        &self,
305        ancestor_block_number: BlockNumber,
306        descendant_block_root: &BlockRoot,
307    ) -> Option<PotOutput> {
308        let header =
309            ChainInfo::ancestor_header(self, ancestor_block_number, descendant_block_root)?;
310        Some(header.header().consensus_info.proof_of_time)
311    }
312}
313
314/// Error for [`shard_membership_entropy_source`]
315#[derive(Debug, thiserror::Error)]
316pub enum ShardMembershipEntropySourceError {
317    /// Failed to find a beacon chain block with the shard membership entropy source
318    #[error(
319        "Failed to find a beacon chain block {block_number} with the shard membership entropy \
320        source"
321    )]
322    FailedToFindBeaconChainBlock {
323        /// Entropy source block number
324        block_number: BlockNumber,
325    },
326}
327
328/// Find shard membership entropy for a specified block number
329pub fn shard_membership_entropy_source<BCI>(
330    block_number: BlockNumber,
331    best_beacon_chain_header: &BeaconChainHeader<'_>,
332    shard_rotation_interval: BlockNumber,
333    shard_rotation_delay: BlockNumber,
334    beacon_chain_info: &BCI,
335) -> Result<ShardMembershipEntropy, ShardMembershipEntropySourceError>
336where
337    BCI: ShardMembershipEntropySourceChainInfo,
338{
339    let entropy_source_block_number = BlockNumber::from(
340        u64::from(block_number.saturating_sub(shard_rotation_delay))
341            / u64::from(shard_rotation_interval)
342            * u64::from(shard_rotation_interval),
343    );
344
345    let proof_of_time = beacon_chain_info
346        .ancestor_header_proof_of_time(
347            entropy_source_block_number,
348            &best_beacon_chain_header.root(),
349        )
350        .ok_or(
351            ShardMembershipEntropySourceError::FailedToFindBeaconChainBlock {
352                block_number: entropy_source_block_number,
353            },
354        )?;
355
356    Ok(proof_of_time.shard_membership_entropy())
357}
358
359/// Error for [`derive_super_segments_for_block()`]
360#[derive(Debug, thiserror::Error)]
361pub enum DeriveSuperSegmentForBlockError {
362    /// Genesis beacon chain segment header not found
363    #[error("Genesis beacon chain segment header not found")]
364    GenesisBeaconChainSegmentHeaderNotFound,
365    /// Parent super segment header not found
366    #[error("Parent super segment header not found for block {block_number}")]
367    ParentSuperSegmentHeaderNotFound {
368        /// Block number for which the parent super segment header was not found
369        block_number: BlockNumber,
370    },
371    /// Shard segment roots error
372    #[error("Shard segment roots error: {error}")]
373    ShardSegmentRootsError {
374        /// Low-level error
375        #[from]
376        error: ShardSegmentRootsError,
377    },
378    /// Too many segments
379    #[error("Too many segments: {extra_segment_roots} extra segment roots")]
380    TooManySegments {
381        /// Number of extra segment roots
382        extra_segment_roots: usize,
383    },
384}
385
386/// Chain info for [`derive_super_segments_for_block()`].
387///
388/// Must have access to enough parent blocks.
389pub trait DeriveSuperSegmentsForBlockChainInfo: Send + Sync {
390    /// Get genesis segment root of the beacon chain
391    fn get_genesis_segment_root(&self) -> Result<SegmentRoot, DeriveSuperSegmentForBlockError>;
392
393    /// Get segment roots that are expected to be included at specified block number
394    fn segment_roots_for_block(
395        &self,
396        block_number: BlockNumber,
397    ) -> impl ExactSizeIterator<Item = ShardSegmentRoot> + Send + Sync + 'static;
398
399    /// Returns the previous super segment header for the block built with the specified number
400    fn previous_super_segment_header(
401        &self,
402        block_number: BlockNumber,
403    ) -> Option<SuperSegmentHeader>;
404
405    /// Returns intermediate and leaf shard segment roots included in the specified block number.
406    ///
407    /// NOTE: Since blocks at this depth are already confirmed, only a block number is needed as a
408    /// reference.
409    fn shard_segment_roots(
410        &self,
411        block_number: BlockNumber,
412    ) -> Result<StdArc<[ShardSegmentRoot]>, ShardSegmentRootsError>;
413}
414
415impl<T> DeriveSuperSegmentsForBlockChainInfo for T
416where
417    T: BeaconChainInfo,
418{
419    #[inline]
420    fn get_genesis_segment_root(&self) -> Result<SegmentRoot, DeriveSuperSegmentForBlockError> {
421        Ok(self
422            .get_segment_header(LocalSegmentIndex::ZERO)
423            .ok_or(DeriveSuperSegmentForBlockError::GenesisBeaconChainSegmentHeaderNotFound)?
424            .segment_root)
425    }
426
427    #[inline]
428    fn segment_roots_for_block(
429        &self,
430        block_number: BlockNumber,
431    ) -> impl ExactSizeIterator<Item = ShardSegmentRoot> + Send + Sync + 'static {
432        self.segment_headers_for_block(block_number)
433            .into_iter()
434            .map(|segment_header| ShardSegmentRoot {
435                shard_index: ShardIndex::BEACON_CHAIN,
436                segment_index: segment_header.segment_index.as_inner(),
437                segment_root: segment_header.segment_root,
438            })
439    }
440
441    #[inline(always)]
442    fn previous_super_segment_header(
443        &self,
444        target_block_number: BlockNumber,
445    ) -> Option<SuperSegmentHeader> {
446        BeaconChainInfo::previous_super_segment_header(self, target_block_number)
447    }
448
449    #[inline(always)]
450    fn shard_segment_roots(
451        &self,
452        block_number: BlockNumber,
453    ) -> Result<StdArc<[ShardSegmentRoot]>, ShardSegmentRootsError> {
454        BeaconChainInfo::shard_segment_roots(self, block_number)
455    }
456}
457
458/// Derive a super segment for a block with a specified parent block number
459pub fn derive_super_segments_for_block<BCI>(
460    chain_info: &BCI,
461    parent_block_number: BlockNumber,
462    block_confirmation_depth: BlockNumber,
463    shard_confirmation_depth: BlockNumber,
464) -> Result<Option<SuperSegment>, DeriveSuperSegmentForBlockError>
465where
466    BCI: DeriveSuperSegmentsForBlockChainInfo,
467{
468    if parent_block_number == BlockNumber::ZERO {
469        let shard_segment_root = ShardSegmentRootWithPosition {
470            shard_index: ShardIndex::BEACON_CHAIN,
471            segment_position: SegmentPosition::from(0),
472            local_segment_index: LocalSegmentIndex::ZERO,
473            segment_root: chain_info.get_genesis_segment_root()?,
474        };
475
476        let mut super_segment = SuperSegment::new(
477            &SuperSegmentHeader {
478                // Placeholder value will be fixed up later
479                index: SuperSegmentIndex::ZERO.into(),
480                root: SuperSegmentRoot::default(),
481                prev_super_segment_header_hash: Blake3Hash::default(),
482                // Placeholder value will be fixed up later
483                max_segment_index: SegmentIndex::ZERO.into(),
484                target_beacon_chain_block_number: BlockNumber::ZERO.into(),
485                num_segments: 0,
486            },
487            BlockNumber::ONE,
488            StdArc::new([shard_segment_root]),
489        )
490        .expect("Genesis super segment is always valid; qed");
491
492        super_segment.header = SuperSegmentHeader {
493            index: SuperSegmentIndex::ZERO.into(),
494            max_segment_index: SegmentIndex::ZERO.into(),
495            prev_super_segment_header_hash: Blake3Hash::default(),
496            ..super_segment.header
497        };
498
499        return Ok(Some(super_segment));
500    }
501
502    let target_block_number = parent_block_number + BlockNumber::ONE;
503
504    let own_segment_roots = chain_info.segment_roots_for_block(target_block_number);
505
506    let shard_segment_roots = if let Some(base_shard_segment_roots_depth) = target_block_number
507        .checked_sub(block_confirmation_depth.saturating_add(shard_confirmation_depth))
508    {
509        let shard_segment_roots = chain_info.shard_segment_roots(base_shard_segment_roots_depth)?;
510        let mut shard_segment_roots_map =
511            HashMap::<ShardIndex, Vec<ShardSegmentRoot>>::with_capacity(shard_segment_roots.len());
512
513        // Group shard segment roots by shard index
514        for &shard_segment_root in shard_segment_roots.iter() {
515            // Segment indices are already sorted in the beacon chain block body, hence a simple
516            // vector for storing them
517            shard_segment_roots_map
518                .entry(shard_segment_root.shard_index)
519                .or_default()
520                .push(shard_segment_root);
521        }
522
523        // Clean up anything that might have reorged since and should not be included in the
524        // super segment yet
525        for block_number_to_check in (base_shard_segment_roots_depth + BlockNumber::ONE..)
526            .take(u64::from(shard_confirmation_depth) as usize)
527        {
528            for shard_segment_root in chain_info
529                .shard_segment_roots(block_number_to_check)?
530                .iter()
531            {
532                if let Some(shard_segments) =
533                    shard_segment_roots_map.get_mut(&shard_segment_root.shard_index)
534                    && let Some(first_shard_segment) = shard_segments.first()
535                    && let Some(offset) = shard_segment_root
536                        .segment_index
537                        .checked_sub(first_shard_segment.segment_index)
538                {
539                    // Truncate the shard segments if there was a reorg
540                    shard_segments.truncate(u64::from(offset) as usize);
541                }
542            }
543        }
544
545        // Collect anything that was not reorged into a flat list of segment roots
546        Some(
547            shard_segment_roots
548                .iter()
549                .filter(|shard_segment_root| {
550                    if let Some(shard_segments) =
551                        shard_segment_roots_map.get(&shard_segment_root.shard_index)
552                        && let Some(first_shard_segment) = shard_segments.first()
553                        && let Some(offset) = shard_segment_root
554                            .segment_index
555                            .checked_sub(first_shard_segment.segment_index)
556                    {
557                        (u64::from(offset) as usize) < shard_segments.len()
558                    } else {
559                        false
560                    }
561                })
562                .copied()
563                .collect::<Vec<_>>(),
564        )
565    } else {
566        None
567    };
568    let shard_segment_roots = shard_segment_roots.into_flat_iter();
569
570    let segment_roots = own_segment_roots
571        .chain(shard_segment_roots)
572        .zip(0..)
573        .map(
574            |(shard_segment_root, segment_position)| ShardSegmentRootWithPosition {
575                shard_index: shard_segment_root.shard_index,
576                segment_position: SegmentPosition::from(segment_position),
577                local_segment_index: shard_segment_root.segment_index,
578                segment_root: shard_segment_root.segment_root,
579            },
580        )
581        .collect::<StdArc<_>>();
582
583    if segment_roots.is_empty() {
584        return Ok(None);
585    }
586
587    let num_segments = segment_roots.len();
588
589    let previous_super_segment_header = chain_info
590        .previous_super_segment_header(target_block_number)
591        .ok_or(
592            DeriveSuperSegmentForBlockError::ParentSuperSegmentHeaderNotFound {
593                block_number: target_block_number,
594            },
595        )?;
596
597    SuperSegment::new(
598        &previous_super_segment_header,
599        target_block_number,
600        segment_roots,
601    )
602    .ok_or({
603        // TODO: While very unlikely, this is hypothetically possible and will need to be
604        //  worked around in the block builder by excluding extra block headers, especially since
605        //  the error will happen much later in the life cycle and it might be too late to revert
606        //  once it is actually hit
607        DeriveSuperSegmentForBlockError::TooManySegments {
608            extra_segment_roots: num_segments - SuperSegmentRoot::MAX_SEGMENTS as usize,
609        }
610    })
611    .map(Some)
612}