zakura_network/zakura/header_sync/
validation.rs1use super::{error::*, events::*, wire::*, *};
2
3pub(super) fn validate_anchor(
4 network: &Network,
5 anchor: (block::Height, block::Hash),
6) -> Result<(), HeaderSyncStartError> {
7 let expected = if anchor.0 == block::Height(0) {
8 Some(network.genesis_hash())
9 } else {
10 network.checkpoint_list().hash(anchor.0)
11 };
12 match expected {
13 Some(hash) if hash == anchor.1 => Ok(()),
14 _ => Err(HeaderSyncStartError::InvalidAnchor { anchor }),
15 }
16}
17
18pub(super) fn next_height(height: block::Height) -> Option<block::Height> {
19 height.0.checked_add(1).map(block::Height)
20}
21
22#[cfg(test)]
23pub(super) fn previous_height(height: block::Height) -> Option<block::Height> {
24 height.0.checked_sub(1).map(block::Height)
25}
26
27pub(super) fn height_after_count(start: block::Height, count: u32) -> Option<block::Height> {
28 start.0.checked_add(count).map(block::Height)
29}
30
31pub(super) fn range_end_height(start: block::Height, count: u32) -> Option<block::Height> {
32 count
33 .checked_sub(1)
34 .and_then(|offset| start.0.checked_add(offset))
35 .map(block::Height)
36}
37
38pub(super) fn count_between(start: block::Height, end: block::Height) -> u32 {
39 end.0
40 .checked_sub(start.0)
41 .and_then(|diff| diff.checked_add(1))
42 .unwrap_or(0)
43}
44
45#[derive(Copy, Clone, Debug, Eq, PartialEq)]
47pub struct HeaderSyncDecodeContext {
48 pub requested: Option<ExpectedHeadersResponse>,
50 pub peer_max_headers_per_response: u32,
52}
53
54impl HeaderSyncDecodeContext {
55 pub fn control() -> Self {
57 Self {
58 requested: None,
59 peer_max_headers_per_response: DEFAULT_HS_RANGE,
60 }
61 }
62
63 pub fn for_headers_response(
65 requested: ExpectedHeadersResponse,
66 peer_max_headers_per_response: u32,
67 ) -> Self {
68 Self {
69 requested: Some(requested),
70 peer_max_headers_per_response: clamp_advertised_range(peer_max_headers_per_response),
71 }
72 }
73
74 pub(super) fn wants_tree_aux_roots(self) -> bool {
75 self.requested
76 .is_some_and(|requested| requested.want_tree_aux_roots)
77 }
78
79 pub(super) fn headers_response_limit(self) -> Result<Option<usize>, HeaderSyncWireError> {
80 let Some(requested) = self.requested else {
81 return Ok(None);
82 };
83 let cap = min(
84 min(requested.count, self.peer_max_headers_per_response),
85 MAX_HS_RANGE,
86 );
87 Some(usize_from_u32(cap, "headers response limit")).transpose()
88 }
89}
90
91#[derive(Copy, Clone, Debug)]
93pub struct HeaderSyncValidationContext<'a> {
94 pub network: &'a Network,
96 pub now: DateTime<Utc>,
98 pub start_height: block::Height,
100 pub decode_context: HeaderSyncDecodeContext,
102}
103
104#[tracing::instrument(skip(headers, context))]
106pub async fn validate_headers_stateless(
107 headers: Vec<Arc<block::Header>>,
108 context: HeaderSyncValidationContext<'_>,
109) -> Result<(), HeaderSyncWireError> {
110 validate_header_count(headers.len(), context.decode_context)?;
111 validate_internal_continuity(&headers)?;
112 validate_header_times(&headers, context.now, context.start_height)?;
113 validate_solution_sizes(&headers, context.network)?;
114 validate_pow_spawn_blocking(headers, context.network).await
115}
116
117pub fn validate_header_range_links(
119 anchor: block::Hash,
120 headers: &[Arc<block::Header>],
121) -> Result<(), HeaderSyncWireError> {
122 let Some(first) = headers.first() else {
123 return Ok(());
124 };
125
126 if first.previous_block_hash != anchor {
127 return Err(HeaderSyncWireError::FirstHeaderDoesNotLink);
128 }
129
130 validate_internal_continuity(headers)
131}
132
133#[tracing::instrument(skip(block, network))]
135pub async fn validate_new_block_stateless(
136 block: Arc<block::Block>,
137 network: &Network,
138 now: DateTime<Utc>,
139 height: block::Height,
140) -> Result<(), HeaderSyncWireError> {
141 let header = block.header.clone();
142 validate_header_times(std::slice::from_ref(&header), now, height)?;
143 validate_solution_sizes(std::slice::from_ref(&header), network)?;
144 validate_pow_spawn_blocking(vec![header], network).await
145}
146
147pub(super) fn validate_header_count(
148 len: usize,
149 context: HeaderSyncDecodeContext,
150) -> Result<(), HeaderSyncWireError> {
151 let Some(max_headers) = context.headers_response_limit()? else {
152 return Err(HeaderSyncWireError::UnsolicitedHeaders);
153 };
154 validate_headers_len(len, max_headers)
155}
156
157pub(super) fn validate_internal_continuity(
158 headers: &[Arc<block::Header>],
159) -> Result<(), HeaderSyncWireError> {
160 for adjacent in headers.windows(2) {
161 let previous_hash = block::Hash::from(adjacent[0].as_ref());
162 if previous_hash != adjacent[1].previous_block_hash {
163 return Err(HeaderSyncWireError::NonContiguousHeaders);
164 }
165 }
166 Ok(())
167}
168
169pub(super) fn validate_header_times(
170 headers: &[Arc<block::Header>],
171 now: DateTime<Utc>,
172 start_height: block::Height,
173) -> Result<(), HeaderSyncWireError> {
174 for (offset, header) in headers.iter().enumerate() {
175 let offset = u32::try_from(offset)
176 .map_err(|_| HeaderSyncWireError::NumericOverflow("header height offset"))?;
177 let height = block::Height(
178 start_height
179 .0
180 .checked_add(offset)
181 .ok_or(HeaderSyncWireError::NumericOverflow("header height"))?,
182 );
183 let hash = block::Hash::from(header.as_ref());
184 header.time_is_valid_at(now, &height, &hash)?;
185 }
186 Ok(())
187}
188
189pub(super) fn validate_solution_sizes(
190 headers: &[Arc<block::Header>],
191 network: &Network,
192) -> Result<(), HeaderSyncWireError> {
193 let expect_regtest = network
194 .parameters()
195 .is_some_and(|parameters| parameters.is_regtest());
196 for header in headers {
197 match (expect_regtest, header.solution) {
198 (true, equihash::Solution::Regtest(_))
199 | (true, equihash::Solution::Common(_))
200 | (false, equihash::Solution::Common(_)) => {}
201 _ => return Err(HeaderSyncWireError::WrongEquihashSolutionSize),
202 }
203 }
204 Ok(())
205}
206
207pub(super) async fn validate_pow_spawn_blocking(
208 headers: Vec<Arc<block::Header>>,
209 network: &Network,
210) -> Result<(), HeaderSyncWireError> {
211 let network = network.clone();
212 tokio::task::spawn_blocking(move || validate_pow_blocking(&headers, &network)).await?
213}
214
215pub(super) fn validate_pow_blocking(
216 headers: &[Arc<block::Header>],
217 network: &Network,
218) -> Result<(), HeaderSyncWireError> {
219 let skip_pow_filter = network
220 .parameters()
221 .is_some_and(|parameters| parameters.is_regtest());
222 if skip_pow_filter {
223 return Ok(());
224 }
225
226 for header in headers {
227 header.solution.check(header, network)?;
230 let hash = block::Hash::from(header.as_ref());
231 validate_difficulty_filter(hash, header.difficulty_threshold)?;
232 }
233 Ok(())
234}
235
236pub(super) fn validate_difficulty_filter(
237 hash: block::Hash,
238 difficulty_threshold: CompactDifficulty,
239) -> Result<(), HeaderSyncWireError> {
240 let threshold = difficulty_threshold
241 .to_expanded()
242 .ok_or(HeaderSyncWireError::InvalidDifficultyThreshold)?;
243 if hash > threshold {
244 return Err(HeaderSyncWireError::DifficultyFilter { hash, threshold });
245 }
246 Ok(())
247}
248
249pub(super) fn validate_get_headers_count(count: u32) -> Result<(), HeaderSyncWireError> {
250 if count == 0 {
251 return Err(HeaderSyncWireError::ZeroHeaderRequestCount);
252 }
253 if count > MAX_HS_RANGE {
254 return Err(HeaderSyncWireError::HeaderCountLimit {
255 actual: usize_from_u32(count, "headers count")?,
256 max: usize_from_u32(MAX_HS_RANGE, "headers cap")?,
257 });
258 }
259 Ok(())
260}
261
262pub(super) fn validate_headers_len(len: usize, max: usize) -> Result<(), HeaderSyncWireError> {
263 if len > max {
264 return Err(HeaderSyncWireError::HeaderCountLimit { actual: len, max });
265 }
266 Ok(())
267}
268
269pub(super) fn validate_body_sizes_len(
270 headers: usize,
271 body_sizes: usize,
272) -> Result<(), HeaderSyncWireError> {
273 if headers != body_sizes {
274 return Err(HeaderSyncWireError::BodySizeCountMismatch {
275 headers,
276 body_sizes,
277 });
278 }
279 Ok(())
280}
281
282pub(super) fn validate_tree_aux_roots_len(
283 headers: usize,
284 roots: usize,
285) -> Result<(), HeaderSyncWireError> {
286 if headers != roots {
287 return Err(HeaderSyncWireError::TreeAuxRootCountMismatch { headers, roots });
288 }
289 Ok(())
290}
291
292pub(super) fn validate_tree_aux_root_heights(
293 start_height: block::Height,
294 roots: &[BlockCommitmentRoots],
295) -> Result<(), HeaderSyncWireError> {
296 for (offset, root) in roots.iter().enumerate() {
297 let height_offset = u32::try_from(offset)
298 .map_err(|_| HeaderSyncWireError::NumericOverflow("tree-aux root height offset"))?;
299 let expected_height = block::Height(
300 start_height
301 .0
302 .checked_add(height_offset)
303 .ok_or(HeaderSyncWireError::NumericOverflow("tree-aux root height"))?,
304 );
305 if root.height != expected_height {
306 return Err(HeaderSyncWireError::TreeAuxRootHeightMismatch {
307 offset,
308 expected_height,
309 root_height: root.height,
310 first_root_height: roots
311 .first()
312 .expect("a mismatching root exists because the loop is processing it")
313 .height,
314 last_root_height: roots
315 .last()
316 .expect("a mismatching root exists because the loop is processing it")
317 .height,
318 });
319 }
320 }
321 Ok(())
322}
323
324pub(super) fn clamp_advertised_range(value: u32) -> u32 {
325 value.clamp(1, MAX_HS_RANGE)
326}
327
328pub(super) fn write_height<W: Write>(
329 writer: &mut W,
330 height: block::Height,
331) -> Result<(), HeaderSyncWireError> {
332 writer.write_u32::<LittleEndian>(height.0)?;
333 Ok(())
334}
335
336pub(super) fn read_height<R: Read>(reader: &mut R) -> Result<block::Height, HeaderSyncWireError> {
337 let height = block::Height(reader.read_u32::<LittleEndian>()?);
338 if height > block::Height::MAX {
339 return Err(HeaderSyncWireError::HeightOutOfRange(height.0));
340 }
341 Ok(height)
342}
343
344pub(super) fn read_bool_marker<R: Read>(
345 reader: &mut R,
346 field: &'static str,
347) -> Result<bool, HeaderSyncWireError> {
348 match reader.read_u8()? {
349 0 => Ok(false),
350 1 => Ok(true),
351 value => Err(HeaderSyncWireError::InvalidBoolMarker { field, value }),
352 }
353}
354
355pub(super) fn reject_trailing(
356 bytes: &[u8],
357 reader: &Cursor<&[u8]>,
358) -> Result<(), HeaderSyncWireError> {
359 let consumed = usize::try_from(reader.position())
360 .map_err(|_| HeaderSyncWireError::NumericOverflow("cursor position"))?;
361 if consumed != bytes.len() {
362 return Err(HeaderSyncWireError::TrailingBytes);
363 }
364 Ok(())
365}
366
367pub(super) fn usize_from_u32(
368 value: u32,
369 field: &'static str,
370) -> Result<usize, HeaderSyncWireError> {
371 usize::try_from(value).map_err(|_| HeaderSyncWireError::NumericOverflow(field))
372}
373
374pub(super) fn u32_from_usize(
375 value: usize,
376 field: &'static str,
377) -> Result<u32, HeaderSyncWireError> {
378 u32::try_from(value).map_err(|_| HeaderSyncWireError::NumericOverflow(field))
379}