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 block_number
170        .as_u64()
171        .is_multiple_of(retarget_interval.as_u64())
172        && block_number > retarget_interval
173    {
174        let interval_start_block = block_number.saturating_sub(retarget_interval);
175        let interval_start_slot = beacon_chain_info
176            .ancestor_header_consensus_info(interval_start_block, parent_block_root)
177            .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
178            .slot;
179
180        Some(solution_range.derive_next(
181            slot.saturating_sub(interval_start_slot),
182            slot_probability,
183            retarget_interval,
184        ))
185    } else {
186        None
187    };
188
189    Ok(SolutionRanges {
190        current: solution_range,
191        next: next_solution_range,
192    })
193}
194
195#[expect(
196    clippy::too_many_arguments,
197    reason = "Explicit minimal input for better testability"
198)]
199fn derive_pot_info<BCI>(
200    pot_consensus_constants: &PotConsensusConstants,
201    beacon_chain_info: &BCI,
202    parent_block_root: &BlockRoot,
203    parent_slot: SlotNumber,
204    parent_slot_iterations: NonZeroU32,
205    parent_parameters_change: Option<PotParametersChange>,
206    block_number: BlockNumber,
207    slot: SlotNumber,
208) -> Result<PotInfo, DeriveConsensusParametersError>
209where
210    BCI: DeriveConsensusParametersChainInfo,
211{
212    let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
213    let pot_entropy_injection_lookback_depth =
214        pot_consensus_constants.entropy_injection_lookback_depth;
215    let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
216
217    // Value right after parent block's slot
218    let slot_iterations = if let Some(change) = &parent_parameters_change
219        && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
220    {
221        change.slot_iterations
222    } else {
223        parent_slot_iterations
224    };
225
226    let parameters_change = if let Some(change) = parent_parameters_change
227        && change.slot > slot
228    {
229        // Retain previous PoT parameters change if it applies after the block's slot
230        Some(change)
231    } else {
232        let lookback_in_blocks = BlockNumber::new(
233            pot_entropy_injection_interval.as_u64()
234                * u64::from(pot_entropy_injection_lookback_depth),
235        );
236        let last_entropy_injection_block_number = BlockNumber::new(
237            block_number.as_u64() / pot_entropy_injection_interval.as_u64()
238                * pot_entropy_injection_interval.as_u64(),
239        );
240        let maybe_entropy_source_block_number =
241            last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
242
243        // Inject entropy every `pot_entropy_injection_interval` blocks
244        if last_entropy_injection_block_number == block_number
245            && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
246            && entropy_source_block_number > BlockNumber::ZERO
247        {
248            let entropy = {
249                let consensus_info = beacon_chain_info
250                    .ancestor_header_consensus_info(entropy_source_block_number, parent_block_root)
251                    .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
252
253                consensus_info
254                    .proof_of_time
255                    .derive_pot_entropy(&consensus_info.solution_record_chunk)
256            };
257
258            let target_slot = slot
259                .checked_add(pot_entropy_injection_delay)
260                .unwrap_or(SlotNumber::MAX);
261
262            Some(PotParametersChange {
263                slot: target_slot,
264                // TODO: A mechanism to increase (not decrease!) number of iterations if slots
265                //  are created too frequently on long enough timescale, maybe based on the same
266                //  lookback depth as entropy (would be the cleanest and easiest to explain)
267                slot_iterations,
268                entropy,
269            })
270        } else {
271            None
272        }
273    };
274
275    Ok(PotInfo {
276        slot_iterations,
277        parameters_change,
278    })
279}
280
281/// Chain info for [`shard_membership_entropy_source()`].
282///
283/// Must have access to enough parent blocks.
284pub trait ShardMembershipEntropySourceChainInfo: Send + Sync {
285    fn ancestor_header_proof_of_time(
286        &self,
287        ancestor_block_number: BlockNumber,
288        descendant_block_root: &BlockRoot,
289    ) -> Option<PotOutput>;
290}
291
292impl<T> ShardMembershipEntropySourceChainInfo for T
293where
294    T: ChainInfo<OwnedBeaconChainBlock>,
295{
296    fn ancestor_header_proof_of_time(
297        &self,
298        ancestor_block_number: BlockNumber,
299        descendant_block_root: &BlockRoot,
300    ) -> Option<PotOutput> {
301        let header =
302            ChainInfo::ancestor_header(self, ancestor_block_number, descendant_block_root)?;
303        Some(header.header().consensus_info.proof_of_time)
304    }
305}
306
307/// Error for [`shard_membership_entropy_source`]
308#[derive(Debug, thiserror::Error)]
309pub enum ShardMembershipEntropySourceError {
310    /// Failed to find a beacon chain block with the shard membership entropy source
311    #[error(
312        "Failed to find a beacon chain block {block_number} with the shard membership entropy \
313        source"
314    )]
315    FailedToFindBeaconChainBlock {
316        /// Entropy source block number
317        block_number: BlockNumber,
318    },
319}
320
321/// Find shard membership entropy for a specified block number
322pub fn shard_membership_entropy_source<BCI>(
323    block_number: BlockNumber,
324    best_beacon_chain_header: &BeaconChainHeader<'_>,
325    shard_rotation_interval: BlockNumber,
326    shard_rotation_delay: BlockNumber,
327    beacon_chain_info: &BCI,
328) -> Result<ShardMembershipEntropy, ShardMembershipEntropySourceError>
329where
330    BCI: ShardMembershipEntropySourceChainInfo,
331{
332    let entropy_source_block_number = BlockNumber::new(
333        block_number.saturating_sub(shard_rotation_delay).as_u64()
334            / shard_rotation_interval.as_u64()
335            * shard_rotation_interval.as_u64(),
336    );
337
338    let proof_of_time = beacon_chain_info
339        .ancestor_header_proof_of_time(
340            entropy_source_block_number,
341            &best_beacon_chain_header.root(),
342        )
343        .ok_or(
344            ShardMembershipEntropySourceError::FailedToFindBeaconChainBlock {
345                block_number: entropy_source_block_number,
346            },
347        )?;
348
349    Ok(proof_of_time.shard_membership_entropy())
350}