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.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 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 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 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 slot_iterations,
221 entropy,
222 })
223 } else {
224 None
225 }
226 };
227
228 Ok(PotInfo {
229 slot_iterations,
230 parameters_change,
231 })
232}