ab_client_consensus_common/
consensus_parameters.rs1use crate::{ConsensusConstants, PotConsensusConstants};
2use ab_client_api::ChainInfo;
3use ab_core_primitives::block::header::{
4 BeaconChainHeader, BlockHeaderConsensusInfo, BlockHeaderConsensusParameters,
5 BlockHeaderFixedConsensusParameters, BlockHeaderPotParametersChange,
6};
7use ab_core_primitives::block::owned::OwnedBeaconChainBlock;
8use ab_core_primitives::block::{BlockNumber, BlockRoot};
9use ab_core_primitives::pieces::RecordChunk;
10use ab_core_primitives::pot::{PotOutput, PotParametersChange, SlotNumber};
11use ab_core_primitives::solutions::{ShardMembershipEntropy, SolutionRange};
12use std::num::NonZeroU32;
13
14struct SolutionRanges {
15 current: SolutionRange,
16 next: Option<SolutionRange>,
17}
18
19struct PotInfo {
20 slot_iterations: NonZeroU32,
21 parameters_change: Option<PotParametersChange>,
22}
23
24#[derive(Debug, Copy, Clone)]
29pub struct DerivedConsensusParameters {
30 pub fixed_parameters: BlockHeaderFixedConsensusParameters,
32 pub next_solution_range: Option<SolutionRange>,
34 pub pot_parameters_change: Option<BlockHeaderPotParametersChange>,
36}
37
38#[derive(Debug, thiserror::Error)]
40pub enum DeriveConsensusParametersError {
41 #[error("Failed to get ancestor header")]
43 GetAncestorHeader,
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct DeriveConsensusParametersConsensusInfo {
49 pub slot: SlotNumber,
51 pub proof_of_time: PotOutput,
53 pub solution_record_chunk: RecordChunk,
55}
56
57impl DeriveConsensusParametersConsensusInfo {
58 pub fn from_consensus_info(consensus_info: &BlockHeaderConsensusInfo) -> Self {
59 Self {
60 slot: consensus_info.slot,
61 proof_of_time: consensus_info.proof_of_time,
62 solution_record_chunk: consensus_info.solution.chunk,
63 }
64 }
65}
66
67pub trait DeriveConsensusParametersChainInfo: Send + Sync {
71 fn ancestor_header_consensus_info(
73 &self,
74 ancestor_block_number: BlockNumber,
75 descendant_block_root: &BlockRoot,
76 ) -> Option<DeriveConsensusParametersConsensusInfo>;
77}
78
79impl<T> DeriveConsensusParametersChainInfo for T
80where
81 T: ChainInfo<OwnedBeaconChainBlock>,
82{
83 fn ancestor_header_consensus_info(
84 &self,
85 ancestor_block_number: BlockNumber,
86 descendant_block_root: &BlockRoot,
87 ) -> Option<DeriveConsensusParametersConsensusInfo> {
88 let header = self.ancestor_header(ancestor_block_number, descendant_block_root)?;
89
90 Some(DeriveConsensusParametersConsensusInfo::from_consensus_info(
91 header.header().consensus_info,
92 ))
93 }
94}
95
96pub fn derive_consensus_parameters<BCI>(
97 consensus_constants: &ConsensusConstants,
98 beacon_chain_info: &BCI,
99 parent_block_root: &BlockRoot,
100 parent_consensus_parameters: &BlockHeaderConsensusParameters<'_>,
101 parent_slot: SlotNumber,
102 block_number: BlockNumber,
103 slot: SlotNumber,
104) -> Result<DerivedConsensusParameters, DeriveConsensusParametersError>
105where
106 BCI: DeriveConsensusParametersChainInfo,
107{
108 let solution_ranges = derive_solution_ranges(
109 consensus_constants.retarget_interval,
110 consensus_constants.slot_probability,
111 beacon_chain_info,
112 parent_block_root,
113 parent_consensus_parameters.fixed_parameters.solution_range,
114 parent_consensus_parameters.next_solution_range,
115 block_number,
116 slot,
117 )?;
118 let pot_info = derive_pot_info(
119 &consensus_constants.pot,
120 beacon_chain_info,
121 parent_block_root,
122 parent_slot,
123 parent_consensus_parameters.fixed_parameters.slot_iterations,
124 parent_consensus_parameters
125 .pot_parameters_change
126 .copied()
127 .map(PotParametersChange::from),
128 block_number,
129 slot,
130 )?;
131
132 Ok(DerivedConsensusParameters {
133 fixed_parameters: BlockHeaderFixedConsensusParameters {
134 solution_range: solution_ranges.current,
135 slot_iterations: pot_info.slot_iterations,
136 num_shards: parent_consensus_parameters.fixed_parameters.num_shards,
137 },
138 next_solution_range: solution_ranges.next,
139 pot_parameters_change: pot_info
140 .parameters_change
141 .map(BlockHeaderPotParametersChange::from),
142 })
143}
144
145#[expect(
146 clippy::too_many_arguments,
147 reason = "Explicit minimal input for better testability"
148)]
149fn derive_solution_ranges<BCI>(
150 retarget_interval: BlockNumber,
151 slot_probability: (u64, u64),
152 beacon_chain_info: &BCI,
153 parent_block_root: &BlockRoot,
154 solution_range: SolutionRange,
155 next_solution_range: Option<SolutionRange>,
156 block_number: BlockNumber,
157 slot: SlotNumber,
158) -> Result<SolutionRanges, DeriveConsensusParametersError>
159where
160 BCI: DeriveConsensusParametersChainInfo,
161{
162 if let Some(next_solution_range) = next_solution_range {
163 return Ok(SolutionRanges {
164 current: next_solution_range,
165 next: None,
166 });
167 }
168
169 let next_solution_range = if u64::from(block_number)
170 .is_multiple_of(u64::from(retarget_interval))
171 && block_number > retarget_interval
172 {
173 let interval_start_block = block_number.saturating_sub(retarget_interval);
174 let interval_start_slot = beacon_chain_info
175 .ancestor_header_consensus_info(interval_start_block, parent_block_root)
176 .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
177 .slot;
178
179 Some(solution_range.derive_next(
180 slot.saturating_sub(interval_start_slot),
181 slot_probability,
182 retarget_interval,
183 ))
184 } else {
185 None
186 };
187
188 Ok(SolutionRanges {
189 current: solution_range,
190 next: next_solution_range,
191 })
192}
193
194#[expect(
195 clippy::too_many_arguments,
196 reason = "Explicit minimal input for better testability"
197)]
198fn derive_pot_info<BCI>(
199 pot_consensus_constants: &PotConsensusConstants,
200 beacon_chain_info: &BCI,
201 parent_block_root: &BlockRoot,
202 parent_slot: SlotNumber,
203 parent_slot_iterations: NonZeroU32,
204 parent_parameters_change: Option<PotParametersChange>,
205 block_number: BlockNumber,
206 slot: SlotNumber,
207) -> Result<PotInfo, DeriveConsensusParametersError>
208where
209 BCI: DeriveConsensusParametersChainInfo,
210{
211 let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
212 let pot_entropy_injection_lookback_depth =
213 pot_consensus_constants.entropy_injection_lookback_depth;
214 let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
215
216 let slot_iterations = if let Some(change) = &parent_parameters_change
218 && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
219 {
220 change.slot_iterations
221 } else {
222 parent_slot_iterations
223 };
224
225 let parameters_change = if let Some(change) = parent_parameters_change
226 && change.slot > slot
227 {
228 Some(change)
230 } else {
231 let lookback_in_blocks = BlockNumber::from(
232 u64::from(pot_entropy_injection_interval)
233 * u64::from(pot_entropy_injection_lookback_depth),
234 );
235 let last_entropy_injection_block_number = BlockNumber::from(
236 u64::from(block_number) / u64::from(pot_entropy_injection_interval)
237 * u64::from(pot_entropy_injection_interval),
238 );
239 let maybe_entropy_source_block_number =
240 last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
241
242 if last_entropy_injection_block_number == block_number
244 && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
245 && entropy_source_block_number > BlockNumber::ZERO
246 {
247 let entropy = {
248 let consensus_info = beacon_chain_info
249 .ancestor_header_consensus_info(entropy_source_block_number, parent_block_root)
250 .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
251
252 consensus_info
253 .proof_of_time
254 .derive_pot_entropy(&consensus_info.solution_record_chunk)
255 };
256
257 let target_slot = slot
258 .checked_add(pot_entropy_injection_delay)
259 .unwrap_or(SlotNumber::MAX);
260
261 Some(PotParametersChange {
262 slot: target_slot,
263 slot_iterations,
267 entropy,
268 })
269 } else {
270 None
271 }
272 };
273
274 Ok(PotInfo {
275 slot_iterations,
276 parameters_change,
277 })
278}
279
280pub trait ShardMembershipEntropySourceChainInfo: Send + Sync {
284 fn ancestor_header_proof_of_time(
285 &self,
286 ancestor_block_number: BlockNumber,
287 descendant_block_root: &BlockRoot,
288 ) -> Option<PotOutput>;
289}
290
291impl<T> ShardMembershipEntropySourceChainInfo for T
292where
293 T: ChainInfo<OwnedBeaconChainBlock>,
294{
295 fn ancestor_header_proof_of_time(
296 &self,
297 ancestor_block_number: BlockNumber,
298 descendant_block_root: &BlockRoot,
299 ) -> Option<PotOutput> {
300 let header =
301 ChainInfo::ancestor_header(self, ancestor_block_number, descendant_block_root)?;
302 Some(header.header().consensus_info.proof_of_time)
303 }
304}
305
306#[derive(Debug, thiserror::Error)]
308pub enum ShardMembershipEntropySourceError {
309 #[error(
311 "Failed to find a beacon chain block {block_number} with the shard membership entropy \
312 source"
313 )]
314 FailedToFindBeaconChainBlock {
315 block_number: BlockNumber,
317 },
318}
319
320pub fn shard_membership_entropy_source<BCI>(
322 block_number: BlockNumber,
323 best_beacon_chain_header: &BeaconChainHeader<'_>,
324 shard_rotation_interval: BlockNumber,
325 shard_rotation_delay: BlockNumber,
326 beacon_chain_info: &BCI,
327) -> Result<ShardMembershipEntropy, ShardMembershipEntropySourceError>
328where
329 BCI: ShardMembershipEntropySourceChainInfo,
330{
331 let entropy_source_block_number = BlockNumber::from(
332 u64::from(block_number.saturating_sub(shard_rotation_delay))
333 / u64::from(shard_rotation_interval)
334 * u64::from(shard_rotation_interval),
335 );
336
337 let proof_of_time = beacon_chain_info
338 .ancestor_header_proof_of_time(
339 entropy_source_block_number,
340 &best_beacon_chain_header.root(),
341 )
342 .ok_or(
343 ShardMembershipEntropySourceError::FailedToFindBeaconChainBlock {
344 block_number: entropy_source_block_number,
345 },
346 )?;
347
348 Ok(proof_of_time.shard_membership_entropy())
349}