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