Skip to main content

zakura_network/zakura/block_sync/
service.rs

1use super::{config::*, events::*, wire::*, *};
2use crate::zakura::{
3    handle_pipe_exit, spawn_supervised_pipe, FramedRecv, FramedSend, OrderedSendError, Peer,
4    PeerStreamSession, Service, SinkReject, Stream, StreamMode, ZakuraConnId, ZakuraPeerId,
5    FRAME_HEADER_BYTES,
6};
7use std::{
8    sync::atomic::{AtomicU64, Ordering},
9    time::Instant,
10};
11
12/// Maximum frame bytes for one stream-6 body frame plus protocol framing.
13///
14/// A block body is still decoded and validated against Zebra's
15/// `MAX_BLOCK_BYTES`; this frame cap has extra slack so stream-6 can classify
16/// oversized or incompatible block-sync payloads in the codec instead of
17/// dropping them at the raw transport gate.
18pub const MAX_BS_FRAME_BYTES: u32 = {
19    // This cast is safe: MAX_BS_MESSAGE_BYTES is asserted below 4 MiB.
20    (MAX_BS_MESSAGE_BYTES + FRAME_HEADER_BYTES) as u32
21};
22
23const BLOCK_SYNC_SERVICE_STREAMS: [Stream; 1] = [Stream {
24    kind: ZAKURA_STREAM_BLOCK_SYNC,
25    version: ZAKURA_BLOCK_SYNC_STREAM_VERSION,
26    frame_cap: MAX_BS_FRAME_BYTES,
27    capability: ZAKURA_CAP_BLOCK_SYNC,
28    mode: StreamMode::Ordered,
29}];
30
31/// Service-declared streams for native block sync.
32pub(crate) fn block_sync_streams() -> &'static [Stream] {
33    &BLOCK_SYNC_SERVICE_STREAMS
34}
35
36/// Cloneable typed stream-6 sender.
37#[derive(Clone, Debug)]
38pub struct BlockSyncPeerSession {
39    peer_id: ZakuraPeerId,
40    direction: ServicePeerDirection,
41    send: FramedSend,
42    cancel_token: CancellationToken,
43}
44
45impl BlockSyncPeerSession {
46    pub(crate) fn new(session: &PeerStreamSession, direction: ServicePeerDirection) -> Self {
47        Self {
48            peer_id: session.peer_id().clone(),
49            direction,
50            send: session.sender(),
51            cancel_token: session.cancel_token(),
52        }
53    }
54
55    /// Build a session directly from a `FramedSend` for routine-level unit tests,
56    /// bypassing a full `PeerStreamSession`. The `send` half feeds a `framed_channel`
57    /// the test reads, and `cancel_token` lets the test tear the routine down.
58    #[cfg(test)]
59    pub(super) fn for_test(
60        peer_id: ZakuraPeerId,
61        send: FramedSend,
62        cancel_token: CancellationToken,
63    ) -> Self {
64        Self {
65            peer_id,
66            direction: ServicePeerDirection::Outbound,
67            send,
68            cancel_token,
69        }
70    }
71
72    /// Authenticated peer identity for this block-sync session.
73    pub fn peer_id(&self) -> &ZakuraPeerId {
74        &self.peer_id
75    }
76
77    /// Direction of the underlying Zakura connection.
78    pub fn direction(&self) -> ServicePeerDirection {
79        self.direction
80    }
81
82    /// Peer disconnect/local shutdown cancellation token.
83    pub fn cancel_token(&self) -> CancellationToken {
84        self.cancel_token.clone()
85    }
86
87    /// Current free slots in this peer's bounded outbound stream queue.
88    pub fn outbound_capacity(&self) -> usize {
89        self.send.capacity()
90    }
91
92    /// Total slots in this peer's bounded outbound stream queue.
93    pub fn outbound_max_capacity(&self) -> usize {
94        self.send.max_capacity()
95    }
96
97    /// Send a typed status advertisement.
98    pub fn try_send_status(&self, status: BlockSyncStatus) -> Result<(), OrderedSendError> {
99        self.try_send_message(BlockSyncMessage::Status(status))
100    }
101
102    /// Send a typed status advertisement, waiting for transport queue capacity.
103    pub async fn send_status(&self, status: BlockSyncStatus) -> Result<(), OrderedSendError> {
104        self.send_message(BlockSyncMessage::Status(status)).await
105    }
106
107    /// Send a typed block range request.
108    pub fn try_send_get_blocks(
109        &self,
110        start_height: block::Height,
111        count: u32,
112    ) -> Result<(), OrderedSendError> {
113        self.try_send_message(BlockSyncMessage::GetBlocks {
114            start_height,
115            count,
116        })
117    }
118
119    /// Send one typed block body frame.
120    pub fn try_send_block(&self, block: Arc<block::Block>) -> Result<(), OrderedSendError> {
121        self.try_send_message(BlockSyncMessage::Block(block))
122    }
123
124    /// Send one typed block body frame, waiting for transport queue capacity.
125    pub async fn send_block(&self, block: Arc<block::Block>) -> Result<(), OrderedSendError> {
126        self.send_message(BlockSyncMessage::Block(block)).await
127    }
128
129    /// Send a typed response terminator.
130    pub fn try_send_blocks_done(
131        &self,
132        start_height: block::Height,
133        returned: u32,
134    ) -> Result<(), OrderedSendError> {
135        self.try_send_message(BlockSyncMessage::BlocksDone {
136            start_height,
137            returned,
138        })
139    }
140
141    /// Send a typed response terminator, waiting for transport queue capacity.
142    pub async fn send_blocks_done(
143        &self,
144        start_height: block::Height,
145        returned: u32,
146    ) -> Result<(), OrderedSendError> {
147        self.send_message(BlockSyncMessage::BlocksDone {
148            start_height,
149            returned,
150        })
151        .await
152    }
153
154    /// Send a typed unavailable-range response.
155    pub fn try_send_range_unavailable(
156        &self,
157        start_height: block::Height,
158        count: u32,
159    ) -> Result<(), OrderedSendError> {
160        self.try_send_message(BlockSyncMessage::RangeUnavailable {
161            start_height,
162            count,
163        })
164    }
165
166    /// Send a typed unavailable-range response, waiting for transport queue capacity.
167    pub async fn send_range_unavailable(
168        &self,
169        start_height: block::Height,
170        count: u32,
171    ) -> Result<(), OrderedSendError> {
172        self.send_message(BlockSyncMessage::RangeUnavailable {
173            start_height,
174            count,
175        })
176        .await
177    }
178
179    fn try_send_message(&self, msg: BlockSyncMessage) -> Result<(), OrderedSendError> {
180        let frame = msg
181            .encode_frame()
182            .map_err(|error| OrderedSendError::Encode(Box::new(error)))?;
183        match self.send.try_send(frame) {
184            Ok(()) => Ok(()),
185            Err(mpsc::error::TrySendError::Full(_frame)) => Err(OrderedSendError::Full),
186            Err(mpsc::error::TrySendError::Closed(_frame)) => Err(OrderedSendError::Closed),
187        }
188    }
189
190    async fn send_message(&self, msg: BlockSyncMessage) -> Result<(), OrderedSendError> {
191        let frame = msg
192            .encode_frame()
193            .map_err(|error| OrderedSendError::Encode(Box::new(error)))?;
194        self.send
195            .send(frame)
196            .await
197            .map_err(|_error| OrderedSendError::Closed)
198    }
199}
200
201/// Native stream-6 block-sync service scaffold.
202#[derive(Debug)]
203pub(crate) struct BlockSyncService {
204    inner: Arc<BlockSyncServiceInner>,
205    _held_events: Option<Arc<StdMutex<mpsc::Receiver<BlockSyncEvent>>>>,
206    _reactor_task: Option<JoinHandle<()>>,
207}
208
209#[derive(Debug)]
210struct BlockSyncServiceInner {
211    config: ZakuraBlockSyncConfig,
212    lifecycle: mpsc::UnboundedSender<BlockSyncEvent>,
213    /// Shared download primitives every per-peer pipe-routine is wired with at
214    /// `add_peer` (per-peer routines). `None` for the inert/handle-less constructors that never
215    /// spawn routines (they only observe `events`/`lifecycle`).
216    routine_wiring: Option<super::state::RoutineWiring>,
217    peers: StdMutex<HashMap<ZakuraPeerId, BlockSyncPeerRecord>>,
218    next_session_id: AtomicU64,
219}
220
221#[derive(Debug)]
222struct BlockSyncPeerRecord {
223    conn_id: ZakuraConnId,
224    session_id: u64,
225    direction: ServicePeerDirection,
226    cancel_token: CancellationToken,
227}
228
229impl BlockSyncService {
230    pub(crate) fn new(config: ZakuraBlockSyncConfig) -> Self {
231        Self::new_with_startup(BlockSyncStartup::inert(config))
232    }
233
234    pub(crate) fn new_with_handle(config: ZakuraBlockSyncConfig, handle: BlockSyncHandle) -> Self {
235        Self {
236            inner: Arc::new(BlockSyncServiceInner {
237                config,
238                lifecycle: handle.lifecycle.clone(),
239                routine_wiring: handle.routine_wiring.clone(),
240                peers: StdMutex::new(HashMap::new()),
241                next_session_id: AtomicU64::new(1),
242            }),
243            _held_events: None,
244            _reactor_task: None,
245        }
246    }
247
248    pub(crate) fn new_with_header_tip(
249        config: ZakuraBlockSyncConfig,
250        header_tip: watch::Receiver<(block::Height, block::Hash)>,
251    ) -> Self {
252        let best_header_tip = *header_tip.borrow();
253        let startup = BlockSyncStartup::new(
254            BlockSyncFrontiers {
255                finalized_height: block::Height::MIN,
256                verified_block_tip: block::Height::MIN,
257                verified_block_hash: block::Hash([0; 32]),
258            },
259            best_header_tip,
260            header_tip,
261            config,
262        );
263        Self::new_with_startup(startup)
264    }
265
266    fn new_with_startup(startup: BlockSyncStartup) -> Self {
267        let config = startup.config.clone();
268        let (handle, _actions, reactor_task) = spawn_block_sync_reactor(startup);
269        Self {
270            inner: Arc::new(BlockSyncServiceInner {
271                config,
272                lifecycle: handle.lifecycle.clone(),
273                routine_wiring: handle.routine_wiring.clone(),
274                peers: StdMutex::new(HashMap::new()),
275                next_session_id: AtomicU64::new(1),
276            }),
277            _held_events: None,
278            _reactor_task: Some(reactor_task),
279        }
280    }
281
282    #[cfg(test)]
283    pub(crate) fn new_for_test(
284        config: ZakuraBlockSyncConfig,
285    ) -> (Self, mpsc::Receiver<BlockSyncEvent>) {
286        let (events, event_rx) = mpsc::channel(config.peer_limits.inbound_queue_depth.max(1));
287        let (lifecycle, mut lifecycle_rx) = mpsc::unbounded_channel();
288        let events_for_lifecycle = events.clone();
289        tokio::spawn(async move {
290            while let Some(event) = lifecycle_rx.recv().await {
291                let _ = events_for_lifecycle.send(event).await;
292            }
293        });
294        (
295            Self {
296                inner: Arc::new(BlockSyncServiceInner {
297                    config,
298                    lifecycle,
299                    routine_wiring: None,
300                    peers: StdMutex::new(HashMap::new()),
301                    next_session_id: AtomicU64::new(1),
302                }),
303                _held_events: None,
304                _reactor_task: None,
305            },
306            event_rx,
307        )
308    }
309
310    #[cfg(test)]
311    pub(crate) fn new_with_handle_for_test(
312        config: ZakuraBlockSyncConfig,
313        handle: BlockSyncHandle,
314    ) -> Self {
315        Self::new_with_handle(config, handle)
316    }
317
318    #[cfg(test)]
319    pub(crate) fn peer_count(&self) -> usize {
320        self.inner
321            .peers
322            .lock()
323            .expect("block-sync peer map mutex is never poisoned")
324            .len()
325    }
326
327    fn peer_slots_free(&self, direction: ServicePeerDirection) -> bool {
328        let peers = self
329            .inner
330            .peers
331            .lock()
332            .expect("block-sync peer map mutex is never poisoned");
333        let count = peers
334            .values()
335            .filter(|record| record.direction == direction)
336            .count();
337        let cap = match direction {
338            ServicePeerDirection::Inbound => self.inner.config.peer_limits.max_inbound_peers,
339            ServicePeerDirection::Outbound => self.inner.config.peer_limits.max_outbound_peers,
340        };
341        count < cap
342    }
343
344    fn peer_is_parked(&self, peer_id: &ZakuraPeerId) -> bool {
345        self.inner
346            .routine_wiring
347            .as_ref()
348            .is_some_and(|wiring| wiring.registry.is_peer_parked(peer_id, Instant::now()))
349    }
350
351    /// Whether `add_peer` may install a session for this peer. A peer that is
352    /// already registered may always *replace* its session (the
353    /// connection-symmetry collision where both sides opened a block-sync stream
354    /// resolves to the winner's stream by replacing the loser's already-counted
355    /// session); only genuinely new peers are held to the per-direction cap.
356    fn can_admit_peer(&self, peer_id: &ZakuraPeerId, direction: ServicePeerDirection) -> bool {
357        let peers = self
358            .inner
359            .peers
360            .lock()
361            .expect("block-sync peer map mutex is never poisoned");
362        if peers.contains_key(peer_id) {
363            return true;
364        }
365        let count = peers
366            .values()
367            .filter(|record| record.direction == direction)
368            .count();
369        let cap = match direction {
370            ServicePeerDirection::Inbound => self.inner.config.peer_limits.max_inbound_peers,
371            ServicePeerDirection::Outbound => self.inner.config.peer_limits.max_outbound_peers,
372        };
373        count < cap
374    }
375}
376
377impl Service for BlockSyncService {
378    fn name(&self) -> &'static str {
379        "block-sync"
380    }
381
382    fn streams(&self) -> &[Stream] {
383        block_sync_streams()
384    }
385
386    fn wants_peer(
387        &self,
388        peer: &ZakuraPeerId,
389        _negotiated: u64,
390        direction: ServicePeerDirection,
391    ) -> bool {
392        !self.peer_is_parked(peer) && self.peer_slots_free(direction)
393    }
394
395    fn add_peer(&self, mut peer: Peer) {
396        if self.peer_is_parked(&peer.id) {
397            peer.service_cancel_token().cancel();
398            return;
399        }
400
401        if !self.can_admit_peer(&peer.id, peer.direction) {
402            return;
403        }
404
405        let Some((recv, send)) = peer.take_stream(ZAKURA_STREAM_BLOCK_SYNC) else {
406            return;
407        };
408
409        let peer_id = peer.id.clone();
410        let session = PeerStreamSession::new(
411            peer_id.clone(),
412            ZAKURA_STREAM_BLOCK_SYNC,
413            recv,
414            send,
415            peer.service_cancel_token(),
416        );
417        let service_cancel_token = session.cancel_token();
418        let connection_cancel_token = peer.cancel_token();
419        let close_cause = peer.close_cause();
420        let block_sync_session = BlockSyncPeerSession::new(&session, peer.direction);
421        let session_id = self.inner.next_session_id.fetch_add(1, Ordering::Relaxed);
422        let (_session_peer, _stream_kind, recv, send, _session_cancel) = session.into_parts();
423
424        // Production outbound block-sync frames go directly through
425        // `BlockSyncPeerSession` (the per-peer routine's `try_send_get_blocks` /
426        // the reactor's `try_send_status`/serving sends), so the raw transport
427        // sender taken from the stream here is redundant. The outbound stream stays
428        // alive through the `BlockSyncPeerSession` clone the reactor holds, so
429        // nothing is lost by dropping it.
430        drop(send);
431
432        {
433            let mut peers = self
434                .inner
435                .peers
436                .lock()
437                .expect("block-sync peer map mutex is never poisoned");
438            if peers
439                .get(&peer_id)
440                .is_some_and(|record| record.conn_id > peer.conn_id)
441            {
442                service_cancel_token.cancel();
443                return;
444            }
445            if let Some(old_record) = peers.insert(
446                peer_id.clone(),
447                BlockSyncPeerRecord {
448                    conn_id: peer.conn_id,
449                    session_id,
450                    direction: peer.direction,
451                    cancel_token: service_cancel_token.clone(),
452                },
453            ) {
454                old_record.cancel_token.cancel();
455            }
456        }
457
458        let run_cancel = service_cancel_token.clone();
459        let on_teardown = {
460            let lifecycle = self.inner.lifecycle.clone();
461            let peer_id = peer_id.clone();
462            let inner = self.inner.clone();
463            move || {
464                let should_notify = if let Ok(mut peers) = inner.peers.lock() {
465                    if peers
466                        .get(&peer_id)
467                        .is_some_and(|record| record.session_id == session_id)
468                    {
469                        peers.remove(&peer_id);
470                        true
471                    } else {
472                        false
473                    }
474                } else {
475                    false
476                };
477
478                if should_notify {
479                    let _ = lifecycle.send(BlockSyncEvent::PeerDisconnected(peer_id));
480                }
481            }
482        };
483        let on_panic = {
484            let connection_cancel_token = connection_cancel_token.clone();
485            let close_cause = close_cause.clone();
486            move || {
487                close_cause.record("service_panic");
488                connection_cancel_token.cancel();
489            }
490        };
491        // the per-peer pipe-routine is spawned HERE (the pipe spawn point), so
492        // a protocol reject still cancels the whole connection via
493        // `handle_pipe_exit`. The routine owns `recv` (the transport read), decodes
494        // each frame, and runs the download/serving dispatch in its own task —
495        // there is no reactor inbound demux. When the service has no reactor wiring
496        // (inert/handle-less test constructors) there is no routine to run; drain
497        // the stream so frames are not silently mishandled and the lifecycle still
498        // flows.
499        let pipe = {
500            let connection_cancel_token = connection_cancel_token.clone();
501            let close_cause = close_cause.clone();
502            let routine_wiring = self.inner.routine_wiring.clone();
503            let block_sync_session = block_sync_session.clone();
504            let peer_id = peer_id.clone();
505            let direction = peer.direction;
506            async move {
507                let result = match routine_wiring {
508                    Some(wiring) => {
509                        let generation = wiring.registry.admit(&peer_id, direction, &wiring.config);
510                        let routine = super::peer_routine::PeerRoutine::new(
511                            peer_id,
512                            block_sync_session,
513                            recv,
514                            wiring.config,
515                            generation,
516                            wiring.budget,
517                            wiring.work,
518                            wiring.registry,
519                            wiring.received_throughput,
520                            wiring.sequencer_input,
521                            wiring.sequencer_input_bytes,
522                            wiring.sequencer_input_decoded_attributed_memory_bytes,
523                            wiring.actions,
524                            wiring.routine_to_reactor,
525                            wiring.view,
526                            run_cancel,
527                            wiring.trace,
528                        );
529                        routine.run().await
530                    }
531                    None => drain_inbound(recv, run_cancel).await,
532                };
533                handle_pipe_exit("block-sync", &connection_cancel_token, &close_cause, result);
534            }
535        };
536        // Let the returned handle drop to detach the supervised task (like
537        // `tokio::spawn`); the `PipeTeardown` still runs on every exit path.
538        spawn_supervised_pipe(
539            peer_id.clone(),
540            service_cancel_token.clone(),
541            on_teardown,
542            on_panic,
543            pipe,
544        );
545
546        let _ = self
547            .inner
548            .lifecycle
549            .send(BlockSyncEvent::PeerConnected(block_sync_session));
550    }
551
552    fn remove_peer(&self, peer: &ZakuraPeerId, conn_id: ZakuraConnId) {
553        let removed = {
554            let mut peers = self
555                .inner
556                .peers
557                .lock()
558                .expect("block-sync peer map mutex is never poisoned");
559            if peers
560                .get(peer)
561                .is_some_and(|record| record.conn_id == conn_id)
562            {
563                peers.remove(peer)
564            } else {
565                None
566            }
567        };
568        if let Some(record) = removed {
569            record.cancel_token.cancel();
570            let _ = self
571                .inner
572                .lifecycle
573                .send(BlockSyncEvent::PeerDisconnected(peer.clone()));
574        }
575    }
576
577    fn deliver_frame(
578        &self,
579        _peer_id: ZakuraPeerId,
580        _stream_kind: u16,
581        _frame: Frame,
582    ) -> Result<(), SinkReject> {
583        // The inbound data flow is inverted: block sync is an `Ordered` stream
584        // whose `FramedRecv` is taken by `add_peer` and owned by the per-peer
585        // pipe-routine ([`PeerRoutine`](super::peer_routine)), which decodes and
586        // dispatches every frame in its own task. The `Service::deliver_frame`
587        // entry point (driven only by the testkit recorder / `registry.deliver`,
588        // never the production ordered-stream reader) therefore has no routine to
589        // route into and no reactor inbound path to emit to. It is not the
590        // block-sync inbound path; accept-and-ignore rather than constructing a
591        // detached one-shot decode that could never reach the owning routine. No
592        // production frame reaches here (the routine consumes the stream), so this
593        // drops nothing that the routine would otherwise handle.
594        Ok(())
595    }
596}
597
598/// Drain a peer's inbound block-sync stream when the service has no reactor
599/// wiring to spawn a pipe-routine (the inert / handle-less test constructors).
600/// Frames are read and discarded until cancellation or stream close, so the
601/// transport reader makes progress and the lifecycle still fires; no routine
602/// exists to act on them.
603async fn drain_inbound(mut recv: FramedRecv, cancel: CancellationToken) -> Result<(), SinkReject> {
604    loop {
605        tokio::select! {
606            () = cancel.cancelled() => return Ok(()),
607            frame = recv.recv() => {
608                if frame.is_none() {
609                    return Ok(());
610                }
611            }
612        }
613    }
614}