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 block_number
170 .as_u64()
171 .is_multiple_of(retarget_interval.as_u64())
172 && block_number > retarget_interval
173 {
174 let interval_start_block = block_number.saturating_sub(retarget_interval);
175 let interval_start_slot = beacon_chain_info
176 .ancestor_header_consensus_info(interval_start_block, parent_block_root)
177 .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?
178 .slot;
179
180 Some(solution_range.derive_next(
181 slot.saturating_sub(interval_start_slot),
182 slot_probability,
183 retarget_interval,
184 ))
185 } else {
186 None
187 };
188
189 Ok(SolutionRanges {
190 current: solution_range,
191 next: next_solution_range,
192 })
193}
194
195#[expect(
196 clippy::too_many_arguments,
197 reason = "Explicit minimal input for better testability"
198)]
199fn derive_pot_info<BCI>(
200 pot_consensus_constants: &PotConsensusConstants,
201 beacon_chain_info: &BCI,
202 parent_block_root: &BlockRoot,
203 parent_slot: SlotNumber,
204 parent_slot_iterations: NonZeroU32,
205 parent_parameters_change: Option<PotParametersChange>,
206 block_number: BlockNumber,
207 slot: SlotNumber,
208) -> Result<PotInfo, DeriveConsensusParametersError>
209where
210 BCI: DeriveConsensusParametersChainInfo,
211{
212 let pot_entropy_injection_interval = pot_consensus_constants.entropy_injection_interval;
213 let pot_entropy_injection_lookback_depth =
214 pot_consensus_constants.entropy_injection_lookback_depth;
215 let pot_entropy_injection_delay = pot_consensus_constants.entropy_injection_delay;
216
217 let slot_iterations = if let Some(change) = &parent_parameters_change
219 && change.slot <= parent_slot.saturating_add(SlotNumber::ONE)
220 {
221 change.slot_iterations
222 } else {
223 parent_slot_iterations
224 };
225
226 let parameters_change = if let Some(change) = parent_parameters_change
227 && change.slot > slot
228 {
229 Some(change)
231 } else {
232 let lookback_in_blocks = BlockNumber::new(
233 pot_entropy_injection_interval.as_u64()
234 * u64::from(pot_entropy_injection_lookback_depth),
235 );
236 let last_entropy_injection_block_number = BlockNumber::new(
237 block_number.as_u64() / pot_entropy_injection_interval.as_u64()
238 * pot_entropy_injection_interval.as_u64(),
239 );
240 let maybe_entropy_source_block_number =
241 last_entropy_injection_block_number.checked_sub(lookback_in_blocks);
242
243 if last_entropy_injection_block_number == block_number
245 && let Some(entropy_source_block_number) = maybe_entropy_source_block_number
246 && entropy_source_block_number > BlockNumber::ZERO
247 {
248 let entropy = {
249 let consensus_info = beacon_chain_info
250 .ancestor_header_consensus_info(entropy_source_block_number, parent_block_root)
251 .ok_or(DeriveConsensusParametersError::GetAncestorHeader)?;
252
253 consensus_info
254 .proof_of_time
255 .derive_pot_entropy(&consensus_info.solution_record_chunk)
256 };
257
258 let target_slot = slot
259 .checked_add(pot_entropy_injection_delay)
260 .unwrap_or(SlotNumber::MAX);
261
262 Some(PotParametersChange {
263 slot: target_slot,
264 slot_iterations,
268 entropy,
269 })
270 } else {
271 None
272 }
273 };
274
275 Ok(PotInfo {
276 slot_iterations,
277 parameters_change,
278 })
279}
280
281pub trait ShardMembershipEntropySourceChainInfo: Send + Sync {
285 fn ancestor_header_proof_of_time(
286 &self,
287 ancestor_block_number: BlockNumber,
288 descendant_block_root: &BlockRoot,
289 ) -> Option<PotOutput>;
290}
291
292impl<T> ShardMembershipEntropySourceChainInfo for T
293where
294 T: ChainInfo<OwnedBeaconChainBlock>,
295{
296 fn ancestor_header_proof_of_time(
297 &self,
298 ancestor_block_number: BlockNumber,
299 descendant_block_root: &BlockRoot,
300 ) -> Option<PotOutput> {
301 let header =
302 ChainInfo::ancestor_header(self, ancestor_block_number, descendant_block_root)?;
303 Some(header.header().consensus_info.proof_of_time)
304 }
305}
306
307#[derive(Debug, thiserror::Error)]
309pub enum ShardMembershipEntropySourceError {
310 #[error(
312 "Failed to find a beacon chain block {block_number} with the shard membership entropy \
313 source"
314 )]
315 FailedToFindBeaconChainBlock {
316 block_number: BlockNumber,
318 },
319}
320
321pub fn shard_membership_entropy_source<BCI>(
323 block_number: BlockNumber,
324 best_beacon_chain_header: &BeaconChainHeader<'_>,
325 shard_rotation_interval: BlockNumber,
326 shard_rotation_delay: BlockNumber,
327 beacon_chain_info: &BCI,
328) -> Result<ShardMembershipEntropy, ShardMembershipEntropySourceError>
329where
330 BCI: ShardMembershipEntropySourceChainInfo,
331{
332 let entropy_source_block_number = BlockNumber::new(
333 block_number.saturating_sub(shard_rotation_delay).as_u64()
334 / shard_rotation_interval.as_u64()
335 * shard_rotation_interval.as_u64(),
336 );
337
338 let proof_of_time = beacon_chain_info
339 .ancestor_header_proof_of_time(
340 entropy_source_block_number,
341 &best_beacon_chain_header.root(),
342 )
343 .ok_or(
344 ShardMembershipEntropySourceError::FailedToFindBeaconChainBlock {
345 block_number: entropy_source_block_number,
346 },
347 )?;
348
349 Ok(proof_of_time.shard_membership_entropy())
350}