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