Skip to main content

zakura_network/zakura/header_sync/
config.rs

1use super::{error::*, validation::*, wire::*, *};
2use crate::zakura::ServicePeerLimits;
3
4/// Header-sync peer status advertisement.
5#[derive(Copy, Clone, Debug, Eq, PartialEq)]
6pub struct HeaderSyncStatus {
7    /// Sender's best known tip height.
8    pub tip_height: block::Height,
9    /// Sender's best known tip hash.
10    pub tip_hash: block::Hash,
11    /// Sender's lowest contiguous header height.
12    pub anchor_height: block::Height,
13    /// Maximum headers the sender will serve per response.
14    pub max_headers_per_response: u32,
15    /// Maximum concurrent `GetHeaders` requests the sender will service.
16    pub max_inflight_requests: u16,
17}
18
19impl HeaderSyncStatus {
20    pub(super) fn encode_to<W: Write>(&self, writer: &mut W) -> Result<(), HeaderSyncWireError> {
21        write_height(writer, self.tip_height)?;
22        self.tip_hash.zcash_serialize(&mut *writer)?;
23        write_height(writer, self.anchor_height)?;
24        writer.write_u32::<LittleEndian>(clamp_advertised_range(self.max_headers_per_response))?;
25        writer.write_u16::<LittleEndian>(self.max_inflight_requests)?;
26        Ok(())
27    }
28
29    pub(super) fn decode_from<R: Read>(reader: &mut R) -> Result<Self, HeaderSyncWireError> {
30        Ok(Self {
31            tip_height: read_height(reader)?,
32            tip_hash: block::Hash::zcash_deserialize(&mut *reader)?,
33            anchor_height: read_height(reader)?,
34            max_headers_per_response: clamp_advertised_range(reader.read_u32::<LittleEndian>()?),
35            max_inflight_requests: reader.read_u16::<LittleEndian>()?,
36        })
37    }
38}
39
40impl Default for HeaderSyncStatus {
41    fn default() -> Self {
42        Self {
43            tip_height: block::Height::MIN,
44            tip_hash: block::Hash([0; 32]),
45            anchor_height: block::Height::MIN,
46            max_headers_per_response: DEFAULT_HS_RANGE,
47            max_inflight_requests: DEFAULT_HS_MAX_INFLIGHT,
48        }
49    }
50}
51
52/// Header-sync configuration nested under the Zakura P2P-v2 config.
53#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields, default)]
55pub struct ZakuraHeaderSyncConfig {
56    /// Maximum headers this node advertises per `GetHeaders` response.
57    pub max_headers_per_response: u32,
58    /// Maximum concurrent `GetHeaders` requests per peer.
59    ///
60    /// This is both the inbound limit advertised to remote peers and the local
61    /// outbound requester ceiling. The effective requester limit is the minimum
62    /// of this value, the peer's advertisement, and the hard protocol cap of 16.
63    pub max_inflight_requests: u16,
64    /// How often this node sends unsolicited status refreshes after local frontier changes.
65    #[serde(with = "humantime_serde")]
66    pub status_refresh_interval: Duration,
67    /// Header-sync peer caps and queue limits owned by this reactor.
68    pub peer_limits: ServicePeerLimits,
69    /// Accept full blocks delivered on Zakura full-block gossip paths.
70    ///
71    /// Disabling this keeps range-based header sync and legacy request/response
72    /// active while forcing block bodies to arrive through the block-sync stream.
73    pub accept_new_blocks: bool,
74    /// Optional trusted header-sync anchor height.
75    ///
76    /// When unset, header sync starts from genesis. When set, [`anchor_hash`](Self::anchor_hash)
77    /// must also be set and must match genesis or a configured checkpoint.
78    pub anchor_height: Option<block::Height>,
79    /// Optional trusted header-sync anchor hash.
80    ///
81    /// When unset, header sync starts from genesis. When set, [`anchor_height`](Self::anchor_height)
82    /// must also be set and must match genesis or a configured checkpoint.
83    pub anchor_hash: Option<block::Hash>,
84}
85
86impl Default for ZakuraHeaderSyncConfig {
87    fn default() -> Self {
88        Self {
89            max_headers_per_response: DEFAULT_HS_RANGE,
90            max_inflight_requests: DEFAULT_HS_MAX_INFLIGHT,
91            status_refresh_interval: DEFAULT_HS_STATUS_REFRESH_INTERVAL,
92            peer_limits: ServicePeerLimits::default(),
93            accept_new_blocks: true,
94            anchor_height: None,
95            anchor_hash: None,
96        }
97    }
98}
99
100impl ZakuraHeaderSyncConfig {
101    /// Return the clamped served-range advertisement for wire status messages.
102    pub fn advertised_max_headers_per_response(&self) -> u32 {
103        clamp_advertised_range(self.max_headers_per_response)
104    }
105
106    /// Return the locally capped in-flight advertisement for status messages.
107    pub fn advertised_max_inflight_requests(&self) -> u16 {
108        self.max_inflight_requests
109            .clamp(1, LOCAL_MAX_HS_INFLIGHT_PER_PEER)
110    }
111
112    /// Return the configured trusted anchor, or genesis when no override is configured.
113    pub fn anchor(
114        &self,
115        network: &Network,
116    ) -> Result<(block::Height, block::Hash), HeaderSyncStartError> {
117        match (self.anchor_height, self.anchor_hash) {
118            (Some(height), Some(hash)) => Ok((height, hash)),
119            (None, None) => Ok((block::Height(0), network.genesis_hash())),
120            _ => Err(HeaderSyncStartError::IncompleteAnchor),
121        }
122    }
123}
124
125/// Returns the serialized byte length of a header-sync header on `network`.
126pub fn header_sync_header_bytes_for_network(network: &Network) -> usize {
127    if network
128        .parameters()
129        .is_some_and(|parameters| parameters.is_regtest())
130    {
131        REGTEST_HEADER_BYTES
132    } else {
133        COMMON_HEADER_BYTES
134    }
135}
136
137/// Maximum `Headers` count that fits the header-sync payload and app frame caps.
138///
139/// The fixed overhead includes the per-message request ID.
140pub fn header_sync_count_by_byte_budget(
141    network: &Network,
142    max_frame_bytes: u32,
143    want_tree_aux_roots: bool,
144) -> u32 {
145    let frame_payload_cap = usize::try_from(max_frame_bytes)
146        .unwrap_or(usize::MAX)
147        .saturating_sub(FRAME_HEADER_BYTES);
148    let payload_cap = MAX_HS_MESSAGE_BYTES.min(frame_payload_cap);
149    let root_bytes = if want_tree_aux_roots {
150        HEADER_SYNC_BLOCK_COMMITMENT_ROOTS_BYTES
151    } else {
152        0
153    };
154    let header_bytes = header_sync_header_bytes_for_network(network)
155        .saturating_add(HEADER_SYNC_BODY_SIZE_BYTES)
156        .saturating_add(root_bytes);
157    let count = payload_cap.saturating_sub(
158        HEADER_SYNC_MESSAGE_TYPE_BYTES
159            + HEADER_SYNC_REQUEST_ID_BYTES
160            + HEADER_SYNC_COUNT_BYTES
161            + HEADER_SYNC_HAS_ROOTS_BYTES,
162    ) / header_bytes;
163
164    u32::try_from(count)
165        .unwrap_or(u32::MAX)
166        .clamp(1, MAX_HS_RANGE)
167}
168
169/// Clamp an outbound `GetHeaders.count` by peer, hard, payload, and frame caps.
170pub fn clamp_header_sync_request_count(
171    desired_count: u32,
172    peer_max_headers_per_response: u32,
173    network: &Network,
174    max_frame_bytes: u32,
175    want_tree_aux_roots: bool,
176) -> u32 {
177    desired_count
178        .min(clamp_advertised_range(peer_max_headers_per_response))
179        .min(MAX_HS_RANGE)
180        .min(header_sync_count_by_byte_budget(
181            network,
182            max_frame_bytes,
183            want_tree_aux_roots,
184        ))
185        .max(1)
186}
187
188/// Maximum inbound `GetHeaders.count` this node will serve.
189pub fn inbound_get_headers_count_limit(
190    config: &ZakuraHeaderSyncConfig,
191    network: &Network,
192    max_frame_bytes: u32,
193    want_tree_aux_roots: bool,
194) -> u32 {
195    clamp_header_sync_request_count(
196        u32::MAX,
197        config.advertised_max_headers_per_response(),
198        network,
199        max_frame_bytes,
200        want_tree_aux_roots,
201    )
202}