zakura_network/zakura/header_sync/events.rs
1use super::{config::*, error::*, validation::*, wire::*, *};
2use crate::zakura::{
3 FrontierUpdate, HeaderSyncPeerSession, HeaderSyncServiceSummary, ServicePeerSnapshot,
4 ZakuraHeaderSyncCandidateState,
5};
6
7/// Cached state frontiers used by the header-sync reactor.
8#[derive(Copy, Clone, Debug, Eq, PartialEq)]
9pub struct HeaderSyncFrontiers {
10 /// Shared finalized height `F`, supplied by state.
11 pub finalized_height: block::Height,
12 /// Highest verified block body height, supplied by state.
13 pub verified_block_tip: block::Height,
14 /// Hash at the highest verified block body height, supplied by state.
15 pub verified_block_hash: block::Hash,
16}
17
18/// Startup inputs for the dependency-neutral header-sync reactor.
19#[derive(Clone, Debug)]
20pub struct HeaderSyncStartup {
21 /// Active network.
22 pub network: Network,
23 /// Trusted anchor height and hash.
24 pub anchor: (block::Height, block::Hash),
25 /// Cached state frontiers at startup.
26 pub frontiers: HeaderSyncFrontiers,
27 /// Durable best header tip loaded from storage at startup.
28 pub best_header_tip: Option<(block::Height, block::Hash)>,
29 /// Shared sync exchange frontier stream.
30 pub frontier_updates: Option<watch::Receiver<FrontierUpdate>>,
31 /// Local header-sync advertisement.
32 pub config: ZakuraHeaderSyncConfig,
33 /// Negotiated or local application frame cap for header-sync responses.
34 pub max_frame_bytes: u32,
35 /// Per-request timeout.
36 pub request_timeout: Duration,
37 /// Minimum interval between unsolicited status refreshes to a peer.
38 pub status_refresh_interval: Duration,
39 /// Optional JSONL trace emitter for header-sync runtime events.
40 pub trace: ZakuraTrace,
41 /// Shared shutdown signal owned by the embedding endpoint or test harness.
42 pub shutdown: CancellationToken,
43 /// Enables outbound range scheduling and state-backed header actions.
44 pub range_state_actions_enabled: bool,
45 /// Enables relaying inbound `NewBlock` messages after local block acceptance is wired.
46 pub inbound_new_block_acceptance_enabled: bool,
47}
48
49impl HeaderSyncStartup {
50 /// Build a startup config from the active network and durable/frontier facts.
51 pub fn new(
52 network: Network,
53 anchor: (block::Height, block::Hash),
54 frontiers: HeaderSyncFrontiers,
55 best_header_tip: Option<(block::Height, block::Hash)>,
56 config: ZakuraHeaderSyncConfig,
57 max_frame_bytes: u32,
58 ) -> Self {
59 let status_refresh_interval = config.status_refresh_interval;
60 Self {
61 network,
62 anchor,
63 frontiers,
64 best_header_tip,
65 frontier_updates: None,
66 config,
67 max_frame_bytes,
68 request_timeout: DEFAULT_HS_REQUEST_TIMEOUT,
69 status_refresh_interval,
70 trace: ZakuraTrace::noop(),
71 shutdown: CancellationToken::new(),
72 range_state_actions_enabled: false,
73 inbound_new_block_acceptance_enabled: false,
74 }
75 }
76}
77
78/// Cheap cloneable handle used by other services to inform header sync.
79#[derive(Clone, Debug)]
80pub struct HeaderSyncHandle {
81 pub(super) events: mpsc::Sender<HeaderSyncEvent>,
82 pub(super) lifecycle: mpsc::UnboundedSender<HeaderSyncEvent>,
83 pub(super) tip: watch::Receiver<(block::Height, block::Hash)>,
84 pub(super) peers: watch::Receiver<ServicePeerSnapshot>,
85 pub(super) candidates: watch::Receiver<ZakuraHeaderSyncCandidateState>,
86}
87
88impl HeaderSyncHandle {
89 /// Send a fact/event to the header-sync reactor.
90 pub async fn send(
91 &self,
92 event: HeaderSyncEvent,
93 ) -> Result<(), mpsc::error::SendError<HeaderSyncEvent>> {
94 self.events.send(event).await
95 }
96
97 /// Try to send a fact/event without awaiting.
98 pub fn try_send(
99 &self,
100 event: HeaderSyncEvent,
101 ) -> Result<(), mpsc::error::TrySendError<HeaderSyncEvent>> {
102 self.events.try_send(event)
103 }
104
105 /// Send a peer lifecycle event without sharing the bounded wire-event queue.
106 pub fn send_lifecycle(
107 &self,
108 event: HeaderSyncEvent,
109 ) -> Result<(), mpsc::error::SendError<HeaderSyncEvent>> {
110 self.lifecycle
111 .send(event)
112 .map_err(|error| mpsc::error::SendError(error.0))
113 }
114
115 /// Subscribe to best-header frontier updates.
116 pub fn subscribe_tip(&self) -> watch::Receiver<(block::Height, block::Hash)> {
117 self.tip.clone()
118 }
119
120 /// Return the currently cached best-header frontier.
121 pub fn best_header_tip(&self) -> (block::Height, block::Hash) {
122 *self.tip.borrow()
123 }
124
125 /// Subscribe to header-sync peer slot snapshots.
126 pub fn subscribe_peer_snapshot(&self) -> watch::Receiver<ServicePeerSnapshot> {
127 self.peers.clone()
128 }
129
130 /// Return the currently cached peer slot snapshot.
131 pub fn peer_snapshot(&self) -> ServicePeerSnapshot {
132 *self.peers.borrow()
133 }
134
135 /// Subscribe to header-sync candidate hints for discovery selection.
136 pub fn subscribe_candidate_state(&self) -> watch::Receiver<ZakuraHeaderSyncCandidateState> {
137 self.candidates.clone()
138 }
139
140 /// Return the currently cached header-sync candidate hints.
141 pub fn candidate_state(&self) -> ZakuraHeaderSyncCandidateState {
142 self.candidates.borrow().clone()
143 }
144}
145
146/// Facts accepted by the header-sync reactor.
147#[derive(Clone, Debug)]
148pub enum HeaderSyncEvent {
149 /// A peer became available for negotiated header sync.
150 PeerConnected(HeaderSyncPeerSession),
151 /// A peer disconnected; all of its outstanding work is dropped.
152 PeerDisconnected(ZakuraPeerId),
153 /// First-party header-sync summary observed over the authenticated discovery stream.
154 AdvisoryHeaderSummary {
155 /// Peer that supplied its own summary.
156 peer: ZakuraPeerId,
157 /// Advisory header-sync summary for dial/admission preference only.
158 summary: HeaderSyncServiceSummary,
159 },
160 /// State committed a full block.
161 FullBlockCommitted {
162 /// Committed block height.
163 height: block::Height,
164 /// Committed block hash.
165 hash: block::Hash,
166 },
167 /// The node's block pipeline accepted an inbound `NewBlock` body.
168 NewBlockAccepted {
169 /// Source peer.
170 peer: ZakuraPeerId,
171 /// Accepted block height.
172 height: block::Height,
173 /// Accepted block hash.
174 hash: block::Hash,
175 /// Accepted full block.
176 block: Arc<block::Block>,
177 },
178 /// The node's block pipeline reported an inbound `NewBlock` was already known.
179 NewBlockDuplicate {
180 /// Source peer.
181 peer: ZakuraPeerId,
182 /// Duplicate block height.
183 height: block::Height,
184 /// Duplicate block hash.
185 hash: block::Hash,
186 },
187 /// The node's block pipeline accepted an inbound `NewBlock` body, but it
188 /// did not land on the best chain. The block is remembered for dedup only:
189 /// a non-best-chain block must not advance the header or verified
190 /// frontiers and must not be forwarded to peers, or the whole Zakura layer
191 /// gossips a losing branch while the node's own chain stays on the best
192 /// one.
193 NewBlockAcceptedNonBestChain {
194 /// Source peer.
195 peer: ZakuraPeerId,
196 /// Accepted non-best-chain block height.
197 height: block::Height,
198 /// Accepted non-best-chain block hash.
199 hash: block::Hash,
200 },
201 /// The node's block pipeline rejected an inbound `NewBlock` body.
202 NewBlockRejected {
203 /// Source peer.
204 peer: ZakuraPeerId,
205 /// Rejected block hash.
206 hash: block::Hash,
207 },
208 /// Test-only inbound header-sync message without a session generation.
209 #[cfg(test)]
210 WireMessage {
211 /// Serving peer.
212 peer: ZakuraPeerId,
213 /// Decoded header-sync message.
214 msg: HeaderSyncMessage,
215 },
216 /// Inbound control message from a specific transport session.
217 SessionWireMessage {
218 /// Serving peer.
219 peer: ZakuraPeerId,
220 /// Ordered-stream generation that delivered the message.
221 session_id: u64,
222 /// Decoded header-sync message.
223 msg: HeaderSyncMessage,
224 },
225 /// Inbound `Headers` response with its mandatory request ID.
226 WireHeaders {
227 /// Serving peer.
228 peer: ZakuraPeerId,
229 /// Ordered-stream generation that delivered the response.
230 session_id: u64,
231 /// Request ID echoed by the peer.
232 request_id: HeaderSyncRequestId,
233 /// Headers in ascending height order.
234 headers: Vec<Arc<block::Header>>,
235 /// Advisory serialized body sizes, parallel to `headers`.
236 body_sizes: Vec<u32>,
237 /// Per-height commitment roots, parallel to `headers`.
238 tree_aux_roots: Vec<BlockCommitmentRoots>,
239 },
240 /// Inbound `GetHeaders` request with its mandatory request ID.
241 WireGetHeaders {
242 /// Requesting peer.
243 peer: ZakuraPeerId,
244 /// Ordered-stream generation that delivered the request.
245 session_id: u64,
246 /// Request ID supplied by the peer.
247 request_id: HeaderSyncRequestId,
248 /// First requested height.
249 start_height: block::Height,
250 /// Requested header count.
251 count: u32,
252 /// Whether the requester wants all-or-nothing tree-aux roots.
253 want_tree_aux_roots: bool,
254 },
255 /// Header-sync frame decoding failed after handler admission.
256 WireDecodeFailed {
257 /// Peer that sent the malformed frame.
258 peer: ZakuraPeerId,
259 /// Decode/validation error.
260 error: Arc<HeaderSyncWireError>,
261 },
262 /// Header-sync protocol failure decoded by the peer-owned session.
263 WireProtocolFailure {
264 /// Peer that sent the invalid message.
265 peer: ZakuraPeerId,
266 /// Misbehavior classification for the protocol failure.
267 reason: HeaderSyncMisbehavior,
268 /// Decode/validation error.
269 error: Arc<HeaderSyncWireError>,
270 },
271 /// State finalized or verified-body frontiers changed.
272 StateFrontiersChanged(HeaderSyncFrontiers),
273 /// State needs a bounded re-delivery of VCT supplied roots for a covered height.
274 VctRootRepairRequested {
275 /// Height whose supplied root is missing or was evicted after rejection.
276 height: block::Height,
277 /// State repair generation.
278 generation: u64,
279 /// Parent hash for `height - 1`, used as the first header anchor.
280 anchor_hash: block::Hash,
281 /// Canonical hashes expected in the repair response, starting at `height`.
282 expected_hashes: Vec<(block::Height, block::Hash)>,
283 },
284 /// State successfully committed the formerly parked VCT height.
285 VctRootRepairResolved {
286 /// State repair generation that was resolved.
287 generation: u64,
288 },
289 /// State successfully committed a header range.
290 HeaderRangeCommitted {
291 /// First committed height.
292 start_height: block::Height,
293 /// New best header tip height.
294 tip_height: block::Height,
295 /// New best header tip hash.
296 tip_hash: block::Hash,
297 },
298 /// State rejected a previously requested range.
299 HeaderRangeCommitFailed {
300 /// Peer that supplied the failed range.
301 peer: ZakuraPeerId,
302 /// Ordered-stream generation that supplied the range.
303 session_id: u64,
304 /// First failed range height.
305 start_height: block::Height,
306 /// Failed range count.
307 count: u32,
308 /// Whether state rejected peer data or hit a local resource/channel failure.
309 kind: HeaderSyncCommitFailureKind,
310 },
311 /// Node wiring finished or abandoned a `Headers` response to an inbound `GetHeaders`.
312 HeaderRangeResponseFinished {
313 /// Peer whose served-response slot can be released.
314 peer: ZakuraPeerId,
315 /// Ordered-stream generation that requested the range.
316 session_id: u64,
317 /// Request ID supplied by the peer.
318 request_id: HeaderSyncRequestId,
319 /// First requested height.
320 start_height: block::Height,
321 /// Requested header count.
322 requested_count: u32,
323 /// Number of headers read from state and sent in the response.
324 returned_count: u32,
325 },
326 /// State returned headers requested by a peer and the reactor should send them.
327 HeaderRangeResponseReady {
328 /// Peer whose inbound request is being served.
329 peer: ZakuraPeerId,
330 /// Ordered-stream generation that requested the range.
331 session_id: u64,
332 /// Request ID supplied by the peer.
333 request_id: HeaderSyncRequestId,
334 /// First requested height.
335 start_height: block::Height,
336 /// Requested header count.
337 requested_count: u32,
338 /// Whether the original request wanted all-or-nothing tree-aux roots.
339 want_tree_aux_roots: bool,
340 /// Bounded headers returned by state.
341 headers: Vec<Arc<block::Header>>,
342 /// Advisory serialized body sizes, parallel to `headers`.
343 body_sizes: Vec<u32>,
344 /// Per-height commitment roots, parallel to `headers`.
345 tree_aux_roots: Vec<BlockCommitmentRoots>,
346 },
347}
348
349impl HeaderSyncEvent {
350 /// Low-cardinality variant label for reactor liveness metrics.
351 pub(super) fn metrics_label(&self) -> &'static str {
352 match self {
353 Self::PeerConnected(_) => "peer_connected",
354 Self::PeerDisconnected(_) => "peer_disconnected",
355 Self::AdvisoryHeaderSummary { .. } => "advisory_header_summary",
356 Self::FullBlockCommitted { .. } => "full_block_committed",
357 Self::NewBlockAccepted { .. } => "new_block_accepted",
358 Self::NewBlockDuplicate { .. } => "new_block_duplicate",
359 Self::NewBlockAcceptedNonBestChain { .. } => "new_block_accepted_non_best_chain",
360 Self::NewBlockRejected { .. } => "new_block_rejected",
361 #[cfg(test)]
362 Self::WireMessage { .. } => "wire_message",
363 Self::SessionWireMessage { .. } => "session_wire_message",
364 Self::WireHeaders { .. } => "wire_headers",
365 Self::WireGetHeaders { .. } => "wire_get_headers",
366 Self::WireDecodeFailed { .. } => "wire_decode_failed",
367 Self::WireProtocolFailure { .. } => "wire_protocol_failure",
368 Self::StateFrontiersChanged(_) => "state_frontiers_changed",
369 Self::VctRootRepairRequested { .. } => "vct_root_repair_requested",
370 Self::VctRootRepairResolved { .. } => "vct_root_repair_resolved",
371 Self::HeaderRangeCommitted { .. } => "header_range_committed",
372 Self::HeaderRangeCommitFailed { .. } => "header_range_commit_failed",
373 Self::HeaderRangeResponseFinished { .. } => "header_range_response_finished",
374 Self::HeaderRangeResponseReady { .. } => "header_range_response_ready",
375 }
376 }
377}
378
379/// Actions emitted by the header-sync reactor for the eventual node wiring.
380#[derive(Clone, Debug)]
381pub enum HeaderSyncAction {
382 /// Test-only observation of a header-sync message sent through a typed session.
383 #[cfg(test)]
384 SendMessage {
385 /// Destination peer.
386 peer: ZakuraPeerId,
387 /// Request ID the session allocated, for correlated messages.
388 request_id: Option<HeaderSyncRequestId>,
389 /// Message that was queued.
390 msg: HeaderSyncMessage,
391 },
392 /// Ask state to commit a contiguous header range.
393 CommitHeaderRange {
394 /// Peer that supplied the range.
395 peer: ZakuraPeerId,
396 /// Ordered-stream generation that supplied the range.
397 session_id: u64,
398 /// Parent anchor hash for the first header.
399 anchor: block::Hash,
400 /// First header height.
401 start_height: block::Height,
402 /// Headers to commit. This is an output payload, not reactor state.
403 headers: Vec<Arc<block::Header>>,
404 /// Advisory serialized body sizes, parallel to `headers`.
405 body_sizes: Vec<u32>,
406 /// Per-height commitment roots, parallel to `headers`.
407 tree_aux_roots: Vec<BlockCommitmentRoots>,
408 /// Whether the range is expected to be finalized by checkpoint policy.
409 finalized: bool,
410 },
411 /// Ask state for the durable best header tip.
412 QueryBestHeaderTip,
413 /// Ask state for a bounded contiguous range of headers.
414 QueryHeadersByHeightRange {
415 /// Peer that requested the range.
416 peer: ZakuraPeerId,
417 /// Ordered-stream generation that requested the range.
418 session_id: u64,
419 /// Request ID supplied by the peer.
420 request_id: HeaderSyncRequestId,
421 /// First height.
422 start: block::Height,
423 /// Maximum count.
424 count: u32,
425 /// Whether the requester wants all-or-nothing tree-aux roots.
426 want_tree_aux_roots: bool,
427 },
428 /// Ask state for missing block-body gaps.
429 QueryMissingBlockBodies {
430 /// First height to consider.
431 from: block::Height,
432 /// Maximum number of heights.
433 limit: u32,
434 },
435 /// Report peer misbehavior to the supervisor.
436 Misbehavior {
437 /// Misbehaving peer.
438 peer: ZakuraPeerId,
439 /// Reason for reporting.
440 reason: HeaderSyncMisbehavior,
441 },
442 /// Notify body download wiring that header-known body gaps exist.
443 BodyGaps {
444 /// First missing height.
445 from: block::Height,
446 /// Last missing height.
447 to: block::Height,
448 },
449 /// Notify production wiring that header sync advanced its best header target.
450 HeaderAdvanced {
451 /// New best-header target height.
452 height: block::Height,
453 /// New best-header target hash.
454 hash: block::Hash,
455 },
456 /// Notify production wiring that header sync re-anchored its best header target.
457 HeaderReanchored {
458 /// Previous best-header target.
459 old: (block::Height, block::Hash),
460 /// New best-header target.
461 new: (block::Height, block::Hash),
462 },
463 /// Inform later block-pipeline wiring that a validated tip block arrived.
464 NewBlockReceived {
465 /// Source peer.
466 peer: ZakuraPeerId,
467 /// Block height from the coinbase transaction.
468 height: block::Height,
469 /// Block hash used for deduplication.
470 hash: block::Hash,
471 /// Full block received from the peer.
472 block: Arc<block::Block>,
473 },
474 /// Test-only observation of an unseen valid full tip block forwarded through a typed session.
475 #[cfg(test)]
476 ForwardNewBlock {
477 /// Source peer, if the block was received from the network.
478 source: Option<ZakuraPeerId>,
479 /// Destination peer.
480 peer: ZakuraPeerId,
481 /// Block height from the coinbase transaction.
482 height: block::Height,
483 /// Block hash used for deduplication.
484 hash: block::Hash,
485 /// Full block that was queued.
486 block: Arc<block::Block>,
487 },
488}
489
490/// Header-sync peer-accounting violations.
491#[derive(Copy, Clone, Debug, Eq, PartialEq)]
492pub enum HeaderSyncMisbehavior {
493 /// Peer status is internally impossible.
494 InvalidStatus,
495 /// `Headers` arrived without an outstanding request.
496 UnsolicitedHeaders,
497 /// `Headers` was empty and made no progress.
498 EmptyHeaders,
499 /// `Headers` exceeded the outstanding request contract.
500 ResponseTooLong,
501 /// Peer supplied a range that failed state/contextual commit.
502 InvalidRange,
503 /// A header-sync payload was malformed before semantic handling.
504 MalformedMessage,
505 /// A peer sent semantic `Status` messages faster than the v1 budget.
506 StatusSpam,
507 /// A peer sent semantic `NewBlock` messages faster than the v1 budget.
508 NewBlockSpam,
509 /// A peer exceeded this node's inbound `GetHeaders` serving budget.
510 GetHeadersSpam,
511 /// A peer requested more headers than this node advertised it can serve.
512 GetHeadersTooLong,
513 /// A header-sync message came from a peer with no active header-sync state.
514 UnknownPeer,
515 /// A full-block tip flood failed stateless validation.
516 InvalidNewBlock,
517}
518
519/// State commit failure classification returned to the reactor by node wiring.
520#[derive(Copy, Clone, Debug, Eq, PartialEq)]
521pub enum HeaderSyncCommitFailureKind {
522 /// The supplied headers failed contextual validation or checkpoint consistency.
523 InvalidPeerRange,
524 /// Local storage/channel/resource failure; do not score the peer.
525 Local,
526}
527
528/// Header-sync request identifier, non-zero and strictly increasing per stream session.
529#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
530pub struct HeaderSyncRequestId(u64);
531
532impl HeaderSyncRequestId {
533 /// Create a non-zero request identifier.
534 pub fn new(id: u64) -> Option<Self> {
535 (id != 0).then_some(Self(id))
536 }
537
538 /// Return the wire representation of this request identifier.
539 pub fn get(self) -> u64 {
540 self.0
541 }
542}
543
544/// A single outbound `GetHeaders` range expected by a peer session.
545#[derive(Copy, Clone, Debug, Eq, PartialEq)]
546pub struct ExpectedHeadersResponse {
547 /// Request ID correlating the response with the request that solicited it.
548 pub request_id: HeaderSyncRequestId,
549 /// First requested height.
550 pub start_height: block::Height,
551 /// Requested header count.
552 pub count: u32,
553 /// Whether this request asked the peer to include all-or-nothing roots.
554 pub want_tree_aux_roots: bool,
555}
556
557impl ExpectedHeadersResponse {
558 /// Create a bounded expected response.
559 pub fn new(
560 request_id: HeaderSyncRequestId,
561 start_height: block::Height,
562 count: u32,
563 want_tree_aux_roots: bool,
564 ) -> Result<Self, HeaderSyncWireError> {
565 validate_get_headers_count(count)?;
566 Ok(Self {
567 request_id,
568 start_height,
569 count,
570 want_tree_aux_roots,
571 })
572 }
573}