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