Skip to main content

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}