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/era (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.era_duration,
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    era_duration: 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.as_u64().is_multiple_of(era_duration.as_u64())
121        && block_number > era_duration
122    {
123        let era_start_block = block_number.saturating_sub(era_duration);
124        let era_start_slot = chain_info
125            .ancestor_header(era_start_block, parent_block_root)
126            .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
127            .header()
128            .consensus_info
129            .slot;
130
131        Some(solution_range.derive_next(
132            slot.saturating_sub(era_start_slot),
133            slot_probability,
134            era_duration,
135        ))
136    } else {
137        None
138    };
139
140    Ok(SolutionRanges {
141        current: solution_range,
142        next: next_solution_range,
143    })
144}
145
146#[expect(
147    clippy::too_many_arguments,
148    reason = "Explicit minimal input for better testability"
149)]
150fn derive_pot_info<CI>(
151    pot_consensus_constants: &PotConsensusConstants,
152    chain_info: &CI,
153    parent_block_root: &BlockRoot,
154    parent_slot: SlotNumber,
155    parent_slot_iterations: NonZeroU32,
156    parent_parameters_change: Option<PotParametersChange>,
157    block_number: BlockNumber,
158    slot: SlotNumber,
159) -> Result<PotInfo, DeriveConsensusParametersError>
160where
161    CI: ChainInfo<OwnedBeaconChainBlock>,
162{
163    let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
164    let pot_entropy_injection_lookback_depth =
165        pot_consensus_constants.entropy_injection_lookback_depth;
166    let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
167
168    // Value right after parent block's slot
169    let slot_iterations = if let Some(change) = &parent_parameters_change
170        && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
171    {
172        change.slot_iterations
173    } else {
174        parent_slot_iterations
175    };
176
177    let parameters_change = if let Some(change) = parent_parameters_change
178        && change.slot > slot
179    {
180        // Retain previous PoT parameters change if it applies after block's slot
181        Some(change)
182    } else {
183        let lookback_in_blocks = BlockNumber::new(
184            pot_entropy_injection_interval.as_u64()
185                * u64::from(pot_entropy_injection_lookback_depth),
186        );
187        let last_entropy_injection_block_number = BlockNumber::new(
188            block_number.as_u64() / pot_entropy_injection_interval.as_u64()
189                * pot_entropy_injection_interval.as_u64(),
190        );
191        let maybe_entropy_source_block_number =
192            last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
193
194        // Inject entropy every `pot_entropy_injection_interval` blocks
195        if last_entropy_injection_block_number == block_number
196            && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
197            && entropy_source_block_number > BlockNumber::ZERO
198        {
199            let entropy = {
200                let entropy_source_block_header = chain_info
201                    .ancestor_header(entropy_source_block_number, parent_block_root)
202                    .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
203                let entropy_source_block_header = entropy_source_block_header.header();
204
205                entropy_source_block_header
206                    .consensus_info
207                    .proof_of_time
208                    .derive_pot_entropy(&entropy_source_block_header.consensus_info.solution.chunk)
209            };
210
211            let target_slot = slot
212                .checked_add(pot_entropy_injection_delay)
213                .unwrap_or(SlotNumber::MAX);
214
215            Some(PotParametersChange {
216                slot: target_slot,
217                // TODO: A mechanism to increase (not decrease!) number of iterations if slots
218                //  are created too frequently on long enough timescale, maybe based on the same
219                //  lookback depth as entropy (would be the cleanest and easiest to explain)
220                slot_iterations,
221                entropy,
222            })
223        } else {
224            None
225        }
226    };
227
228    Ok(PotInfo {
229        slot_iterations,
230        parameters_change,
231    })
232}