Skip to main content

ab_client_consensus_common/
consensus_parameters.rs

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