ab_client_consensus_common/
consensus_parameters.rs1use 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#[derive(Debug, Copy, Clone)]
28pub struct DerivedConsensusParameters {
29 pub fixed_parameters: BlockHeaderFixedConsensusParameters,
31 pub next_solution_range: Option<SolutionRange>,
33 pub pot_parameters_change: Option<BlockHeaderPotParametersChange>,
35}
36
37#[derive(Debug, thiserror::Error)]
39pub enum DeriveConsensusParametersError {
40 #[error("Failed to get ancestor header")]
42 GetAncestorHeader,
43}
44
45pub 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 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 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 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 slot_iterations,
223 entropy,
224 })
225 } else {
226 None
227 }
228 };
229
230 Ok(PotInfo {
231 slot_iterations,
232 parameters_change,
233 })
234}