Skip to main content

ab_client_consensus_common/
consensus_parameters.rs

1use crate::{ConsensusConstants, PotConsensusConstants};
2use ab_client_api::ChainInfo;
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::pieces::RecordChunk;
10use ab_core_primitives::pot::{PotOutput, PotParametersChange, SlotNumber};
11use ab_core_primitives::solutions::{ShardMembershipEntropy, SolutionRange};
12use std::num::NonZeroU32;
13
14struct SolutionRanges {
15    current: SolutionRange,
16    next: Option<SolutionRange>,
17}
18
19struct PotInfo {
20    slot_iterations: NonZeroU32,
21    parameters_change: Option<PotParametersChange>,
22}
23
24/// Derived consensus parameters, to be eventually turned into
25/// [`OwnedBlockHeaderConsensusParameters`]
26///
27/// [`OwnedBlockHeaderConsensusParameters`]: ab_core_primitives::block::header::OwnedBlockHeaderConsensusParameters
28#[derive(Debug, Copy, Clone)]
29pub struct DerivedConsensusParameters {
30    /// Consensus parameters that are always present
31    pub fixed_parameters: BlockHeaderFixedConsensusParameters,
32    /// Solution range for the next block/interval (if any)
33    pub next_solution_range: Option<SolutionRange>,
34    /// Change of parameters to apply to the proof of time chain (if any)
35    pub pot_parameters_change: Option<BlockHeaderPotParametersChange>,
36}
37
38/// Error for [`derive_consensus_parameters()`]
39#[derive(Debug, thiserror::Error)]
40pub enum DeriveConsensusParametersError {
41    /// Failed to get ancestor header
42    #[error("Failed to get ancestor header")]
43    GetAncestorHeader,
44}
45
46/// A limited subset of [`BlockHeaderConsensusInfo`] for [`derive_consensus_parameters()`]
47#[derive(Debug, Clone, Copy)]
48pub struct DeriveConsensusParametersConsensusInfo {
49    /// Slot number
50    pub slot: SlotNumber,
51    /// Proof of time for this slot
52    pub proof_of_time: PotOutput,
53    /// Record chunk used in a solution
54    pub solution_record_chunk: RecordChunk,
55}
56
57impl DeriveConsensusParametersConsensusInfo {
58    pub fn from_consensus_info(consensus_info: &BlockHeaderConsensusInfo) -> Self {
59        Self {
60            slot: consensus_info.slot,
61            proof_of_time: consensus_info.proof_of_time,
62            solution_record_chunk: consensus_info.solution.chunk,
63        }
64    }
65}
66
67/// Chain info for [`derive_consensus_parameters()`].
68///
69/// Must have access to enough parent blocks.
70pub trait DeriveConsensusParametersChainInfo: Send + Sync {
71    /// Get header of ancestor block number for descendant block root
72    fn ancestor_header_consensus_info(
73        &self,
74        ancestor_block_number: BlockNumber,
75        descendant_block_root: &BlockRoot,
76    ) -> Option<DeriveConsensusParametersConsensusInfo>;
77}
78
79impl<T> DeriveConsensusParametersChainInfo for T
80where
81    T: ChainInfo<OwnedBeaconChainBlock>,
82{
83    fn ancestor_header_consensus_info(
84        &self,
85        ancestor_block_number: BlockNumber,
86        descendant_block_root: &BlockRoot,
87    ) -> Option<DeriveConsensusParametersConsensusInfo> {
88        let header = self.ancestor_header(ancestor_block_number, descendant_block_root)?;
89
90        Some(DeriveConsensusParametersConsensusInfo::from_consensus_info(
91            header.header().consensus_info,
92        ))
93    }
94}
95
96pub fn derive_consensus_parameters<BCI>(
97    consensus_constants: &ConsensusConstants,
98    beacon_chain_info: &BCI,
99    parent_block_root: &BlockRoot,
100    parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
101    parent_slot: SlotNumber,
102    block_number: BlockNumber,
103    slot: SlotNumber,
104) -> Result<DerivedConsensusParameters, DeriveConsensusParametersError>
105where
106    BCI: DeriveConsensusParametersChainInfo,
107{
108    let solution_ranges = derive_solution_ranges(
109        consensus_constants.retarget_interval,
110        consensus_constants.slot_probability,
111        beacon_chain_info,
112        parent_block_root,
113        parent_consensus_parameters.fixed_parameters.solution_range,
114        parent_consensus_parameters.next_solution_range,
115        block_number,
116        slot,
117    )?;
118    let pot_info = derive_pot_info(
119        &consensus_constants.pot,
120        beacon_chain_info,
121        parent_block_root,
122        parent_slot,
123        parent_consensus_parameters.fixed_parameters.slot_iterations,
124        parent_consensus_parameters
125            .pot_parameters_change
126            .copied()
127            .map(PotParametersChange::from),
128        block_number,
129        slot,
130    )?;
131
132    Ok(DerivedConsensusParameters {
133        fixed_parameters: BlockHeaderFixedConsensusParameters {
134            solution_range: solution_ranges.current,
135            slot_iterations: pot_info.slot_iterations,
136            num_shards: parent_consensus_parameters.fixed_parameters.num_shards,
137        },
138        next_solution_range: solution_ranges.next,
139        pot_parameters_change: pot_info
140            .parameters_change
141            .map(BlockHeaderPotParametersChange::from),
142    })
143}
144
145#[expect(
146    clippy::too_many_arguments,
147    reason = "Explicit minimal input for better testability"
148)]
149fn derive_solution_ranges<BCI>(
150    retarget_interval: BlockNumber,
151    slot_probability: (u64, u64),
152    beacon_chain_info: &BCI,
153    parent_block_root: &BlockRoot,
154    solution_range: SolutionRange,
155    next_solution_range: Option<SolutionRange>,
156    block_number: BlockNumber,
157    slot: SlotNumber,
158) -> Result<SolutionRanges, DeriveConsensusParametersError>
159where
160    BCI: DeriveConsensusParametersChainInfo,
161{
162    if let Some(next_solution_range) = next_solution_range {
163        return Ok(SolutionRanges {
164            current: next_solution_range,
165            next: None,
166        });
167    }
168
169    let next_solution_range = if u64::from(block_number)
170        .is_multiple_of(u64::from(retarget_interval))
171        && block_number > retarget_interval
172    {
173        let interval_start_block = block_number.saturating_sub(retarget_interval);
174        let interval_start_slot = beacon_chain_info
175            .ancestor_header_consensus_info(interval_start_block, parent_block_root)
176            .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
177            .slot;
178
179        Some(solution_range.derive_next(
180            slot.saturating_sub(interval_start_slot),
181            slot_probability,
182            retarget_interval,
183        ))
184    } else {
185        None
186    };
187
188    Ok(SolutionRanges {
189        current: solution_range,
190        next: next_solution_range,
191    })
192}
193
194#[expect(
195    clippy::too_many_arguments,
196    reason = "Explicit minimal input for better testability"
197)]
198fn derive_pot_info<BCI>(
199    pot_consensus_constants: &PotConsensusConstants,
200    beacon_chain_info: &BCI,
201    parent_block_root: &BlockRoot,
202    parent_slot: SlotNumber,
203    parent_slot_iterations: NonZeroU32,
204    parent_parameters_change: Option<PotParametersChange>,
205    block_number: BlockNumber,
206    slot: SlotNumber,
207) -> Result<PotInfo, DeriveConsensusParametersError>
208where
209    BCI: DeriveConsensusParametersChainInfo,
210{
211    let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
212    let pot_entropy_injection_lookback_depth =
213        pot_consensus_constants.entropy_injection_lookback_depth;
214    let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
215
216    // Value right after parent block's slot
217    let slot_iterations = if let Some(change) = &parent_parameters_change
218        && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
219    {
220        change.slot_iterations
221    } else {
222        parent_slot_iterations
223    };
224
225    let parameters_change = if let Some(change) = parent_parameters_change
226        && change.slot > slot
227    {
228        // Retain previous PoT parameters change if it applies after the block's slot
229        Some(change)
230    } else {
231        let lookback_in_blocks = BlockNumber::from(
232            u64::from(pot_entropy_injection_interval)
233                * u64::from(pot_entropy_injection_lookback_depth),
234        );
235        let last_entropy_injection_block_number = BlockNumber::from(
236            u64::from(block_number) / u64::from(pot_entropy_injection_interval)
237                * u64::from(pot_entropy_injection_interval),
238        );
239        let maybe_entropy_source_block_number =
240            last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
241
242        // Inject entropy every `pot_entropy_injection_interval` blocks
243        if last_entropy_injection_block_number == block_number
244            && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
245            && entropy_source_block_number > BlockNumber::ZERO
246        {
247            let entropy = {
248                let consensus_info = beacon_chain_info
249                    .ancestor_header_consensus_info(entropy_source_block_number, parent_block_root)
250                    .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
251
252                consensus_info
253                    .proof_of_time
254                    .derive_pot_entropy(&consensus_info.solution_record_chunk)
255            };
256
257            let target_slot = slot
258                .checked_add(pot_entropy_injection_delay)
259                .unwrap_or(SlotNumber::MAX);
260
261            Some(PotParametersChange {
262                slot: target_slot,
263                // TODO: A mechanism to increase (not decrease!) number of iterations if slots
264                //  are created too frequently on long enough timescale, maybe based on the same
265                //  lookback depth as entropy (would be the cleanest and easiest to explain)
266                slot_iterations,
267                entropy,
268            })
269        } else {
270            None
271        }
272    };
273
274    Ok(PotInfo {
275        slot_iterations,
276        parameters_change,
277    })
278}
279
280/// Chain info for [`shard_membership_entropy_source()`].
281///
282/// Must have access to enough parent blocks.
283pub trait ShardMembershipEntropySourceChainInfo: Send + Sync {
284    fn ancestor_header_proof_of_time(
285        &self,
286        ancestor_block_number: BlockNumber,
287        descendant_block_root: &BlockRoot,
288    ) -> Option<PotOutput>;
289}
290
291impl<T> ShardMembershipEntropySourceChainInfo for T
292where
293    T: ChainInfo<OwnedBeaconChainBlock>,
294{
295    fn ancestor_header_proof_of_time(
296        &self,
297        ancestor_block_number: BlockNumber,
298        descendant_block_root: &BlockRoot,
299    ) -> Option<PotOutput> {
300        let header =
301            ChainInfo::ancestor_header(self, ancestor_block_number, descendant_block_root)?;
302        Some(header.header().consensus_info.proof_of_time)
303    }
304}
305
306/// Error for [`shard_membership_entropy_source`]
307#[derive(Debug, thiserror::Error)]
308pub enum ShardMembershipEntropySourceError {
309    /// Failed to find a beacon chain block with the shard membership entropy source
310    #[error(
311        "Failed to find a beacon chain block {block_number} with the shard membership entropy \
312        source"
313    )]
314    FailedToFindBeaconChainBlock {
315        /// Entropy source block number
316        block_number: BlockNumber,
317    },
318}
319
320/// Find shard membership entropy for a specified block number
321pub fn shard_membership_entropy_source<BCI>(
322    block_number: BlockNumber,
323    best_beacon_chain_header: &BeaconChainHeader<'_>,
324    shard_rotation_interval: BlockNumber,
325    shard_rotation_delay: BlockNumber,
326    beacon_chain_info: &BCI,
327) -> Result<ShardMembershipEntropy, ShardMembershipEntropySourceError>
328where
329    BCI: ShardMembershipEntropySourceChainInfo,
330{
331    let entropy_source_block_number = BlockNumber::from(
332        u64::from(block_number.saturating_sub(shard_rotation_delay))
333            / u64::from(shard_rotation_interval)
334            * u64::from(shard_rotation_interval),
335    );
336
337    let proof_of_time = beacon_chain_info
338        .ancestor_header_proof_of_time(
339            entropy_source_block_number,
340            &best_beacon_chain_header.root(),
341        )
342        .ok_or(
343            ShardMembershipEntropySourceError::FailedToFindBeaconChainBlock {
344                block_number: entropy_source_block_number,
345            },
346        )?;
347
348    Ok(proof_of_time.shard_membership_entropy())
349}