ab_client_consensus_common/
consensus_parameters.rs

1use crate::{ConsensusConstants, PotConsensusConstants};
2use ab_client_api::ChainInfo;
3use ab_core_primitives::block::header::{
4    BlockHeaderConsensusParameters, BlockHeaderFixedConsensusParameters,
5    BlockHeaderPotParametersChange,
6};
7use ab_core_primitives::block::owned::OwnedBeaconChainBlock;
8use ab_core_primitives::block::{BlockNumber, BlockRoot};
9use ab_core_primitives::pot::{PotParametersChange, SlotNumber};
10use ab_core_primitives::solutions::SolutionRange;
11use std::num::NonZeroU32;
12
13struct SolutionRanges {
14    current: SolutionRange,
15    next: Option<SolutionRange>,
16}
17
18struct PotInfo {
19    slot_iterations: NonZeroU32,
20    parameters_change: Option<PotParametersChange>,
21}
22
23/// Derived consensus parameters, to be eventually turned into
24/// [`OwnedBlockHeaderConsensusParameters`]
25///
26/// [`OwnedBlockHeaderConsensusParameters`]: ab_core_primitives::block::header::OwnedBlockHeaderConsensusParameters
27#[derive(Debug, Copy, Clone)]
28pub struct DerivedConsensusParameters {
29    /// Consensus parameters that are always present
30    pub fixed_parameters: BlockHeaderFixedConsensusParameters,
31    /// Solution range for the next block/interval (if any)
32    pub next_solution_range: Option<SolutionRange>,
33    /// Change of parameters to apply to the proof of time chain (if any)
34    pub pot_parameters_change: Option<BlockHeaderPotParametersChange>,
35}
36
37/// Error for [`derive_consensus_parameters`]
38#[derive(Debug, thiserror::Error)]
39pub enum DeriveConsensusParametersError {
40    /// Failed to get ancestor header
41    #[error("Failed to get ancestor header")]
42    GetAncestorHeader,
43}
44
45// TODO: Another domain-specific abstraction over `ChainInfo`, which will be implemented for
46//  `ChainInfo`, but could also be implemented in simpler way directly for tests without dealing
47//  with complete headers, etc.
48pub fn derive_consensus_parameters<CI>(
49    consensus_constants: &ConsensusConstants,
50    chain_info: &CI,
51    parent_block_root: &BlockRoot,
52    parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
53    parent_slot: SlotNumber,
54    block_number: BlockNumber,
55    slot: SlotNumber,
56) -> Result<DerivedConsensusParameters, DeriveConsensusParametersError>
57where
58    CI: ChainInfo<OwnedBeaconChainBlock>,
59{
60    let solution_ranges = derive_solution_ranges(
61        consensus_constants.retarget_interval,
62        consensus_constants.slot_probability,
63        chain_info,
64        parent_block_root,
65        parent_consensus_parameters.fixed_parameters.solution_range,
66        parent_consensus_parameters.next_solution_range,
67        block_number,
68        slot,
69    )?;
70    let pot_info = derive_pot_info(
71        &consensus_constants.pot,
72        chain_info,
73        parent_block_root,
74        parent_slot,
75        parent_consensus_parameters.fixed_parameters.slot_iterations,
76        parent_consensus_parameters
77            .pot_parameters_change
78            .copied()
79            .map(PotParametersChange::from),
80        block_number,
81        slot,
82    )?;
83
84    Ok(DerivedConsensusParameters {
85        fixed_parameters: BlockHeaderFixedConsensusParameters {
86            solution_range: solution_ranges.current,
87            slot_iterations: pot_info.slot_iterations,
88        },
89        next_solution_range: solution_ranges.next,
90        pot_parameters_change: pot_info
91            .parameters_change
92            .map(BlockHeaderPotParametersChange::from),
93    })
94}
95
96#[expect(
97    clippy::too_many_arguments,
98    reason = "Explicit minimal input for better testability"
99)]
100fn derive_solution_ranges<CI>(
101    retarget_interval: BlockNumber,
102    slot_probability: (u64, u64),
103    chain_info: &CI,
104    parent_block_root: &BlockRoot,
105    solution_range: SolutionRange,
106    next_solution_range: Option<SolutionRange>,
107    block_number: BlockNumber,
108    slot: SlotNumber,
109) -> Result<SolutionRanges, DeriveConsensusParametersError>
110where
111    CI: ChainInfo<OwnedBeaconChainBlock>,
112{
113    if let Some(next_solution_range) = next_solution_range {
114        return Ok(SolutionRanges {
115            current: next_solution_range,
116            next: None,
117        });
118    }
119
120    let next_solution_range = if block_number
121        .as_u64()
122        .is_multiple_of(retarget_interval.as_u64())
123        && block_number > retarget_interval
124    {
125        let interval_start_block = block_number.saturating_sub(retarget_interval);
126        let interval_start_slot = chain_info
127            .ancestor_header(interval_start_block, parent_block_root)
128            .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
129            .header()
130            .consensus_info
131            .slot;
132
133        Some(solution_range.derive_next(
134            slot.saturating_sub(interval_start_slot),
135            slot_probability,
136            retarget_interval,
137        ))
138    } else {
139        None
140    };
141
142    Ok(SolutionRanges {
143        current: solution_range,
144        next: next_solution_range,
145    })
146}
147
148#[expect(
149    clippy::too_many_arguments,
150    reason = "Explicit minimal input for better testability"
151)]
152fn derive_pot_info<CI>(
153    pot_consensus_constants: &PotConsensusConstants,
154    chain_info: &CI,
155    parent_block_root: &BlockRoot,
156    parent_slot: SlotNumber,
157    parent_slot_iterations: NonZeroU32,
158    parent_parameters_change: Option<PotParametersChange>,
159    block_number: BlockNumber,
160    slot: SlotNumber,
161) -> Result<PotInfo, DeriveConsensusParametersError>
162where
163    CI: ChainInfo<OwnedBeaconChainBlock>,
164{
165    let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
166    let pot_entropy_injection_lookback_depth =
167        pot_consensus_constants.entropy_injection_lookback_depth;
168    let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
169
170    // Value right after parent block's slot
171    let slot_iterations = if let Some(change) = &parent_parameters_change
172        && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
173    {
174        change.slot_iterations
175    } else {
176        parent_slot_iterations
177    };
178
179    let parameters_change = if let Some(change) = parent_parameters_change
180        && change.slot > slot
181    {
182        // Retain previous PoT parameters change if it applies after the block's slot
183        Some(change)
184    } else {
185        let lookback_in_blocks = BlockNumber::new(
186            pot_entropy_injection_interval.as_u64()
187                * u64::from(pot_entropy_injection_lookback_depth),
188        );
189        let last_entropy_injection_block_number = BlockNumber::new(
190            block_number.as_u64() / pot_entropy_injection_interval.as_u64()
191                * pot_entropy_injection_interval.as_u64(),
192        );
193        let maybe_entropy_source_block_number =
194            last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
195
196        // Inject entropy every `pot_entropy_injection_interval` blocks
197        if last_entropy_injection_block_number == block_number
198            && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
199            && entropy_source_block_number > BlockNumber::ZERO
200        {
201            let entropy = {
202                let entropy_source_block_header = chain_info
203                    .ancestor_header(entropy_source_block_number, parent_block_root)
204                    .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
205                let entropy_source_block_header = entropy_source_block_header.header();
206
207                entropy_source_block_header
208                    .consensus_info
209                    .proof_of_time
210                    .derive_pot_entropy(&entropy_source_block_header.consensus_info.solution.chunk)
211            };
212
213            let target_slot = slot
214                .checked_add(pot_entropy_injection_delay)
215                .unwrap_or(SlotNumber::MAX);
216
217            Some(PotParametersChange {
218                slot: target_slot,
219                // TODO: A mechanism to increase (not decrease!) number of iterations if slots
220                //  are created too frequently on long enough timescale, maybe based on the same
221                //  lookback depth as entropy (would be the cleanest and easiest to explain)
222                slot_iterations,
223                entropy,
224            })
225        } else {
226            None
227        }
228    };
229
230    Ok(PotInfo {
231        slot_iterations,
232        parameters_change,
233    })
234}