Skip to main content

zakura_network/zakura/header_sync/
validation.rs

1use 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/// Peer/request bounds required to decode a header response without over-allocation.
46#[derive(Copy, Clone, Debug, Eq, PartialEq)]
47pub struct HeaderSyncDecodeContext {
48    /// The matching in-flight request, when a `Headers` response is expected.
49    pub requested: Option<ExpectedHeadersResponse>,
50    /// Peer's advertised response cap.
51    pub peer_max_headers_per_response: u32,
52}
53
54impl HeaderSyncDecodeContext {
55    /// Context for messages that are not `Headers` responses.
56    pub fn control() -> Self {
57        Self {
58            requested: None,
59            peer_max_headers_per_response: DEFAULT_HS_RANGE,
60        }
61    }
62
63    /// Context for a `Headers` response to `requested`.
64    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/// Stateless header validation context.
92#[derive(Copy, Clone, Debug)]
93pub struct HeaderSyncValidationContext<'a> {
94    /// Active network, used for Equihash solution-size policy.
95    pub network: &'a Network,
96    /// Wall clock used for future-time checks.
97    pub now: DateTime<Utc>,
98    /// First height in the received run.
99    pub start_height: block::Height,
100    /// Matching request and peer cap for count validation.
101    pub decode_context: HeaderSyncDecodeContext,
102}
103
104/// Run all context-free validation checks for an inbound `Headers` response.
105#[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
117/// Check that a header range links to its anchor and is internally contiguous.
118pub 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/// Run all context-free validation checks for an inbound full-block tip flood.
134#[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        // Bind the Equihash parameters to `network` so a short 36-byte
228        // Regtest-shaped solution cannot pass the PoW check on Mainnet/Testnet.
229        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}