Skip to main content

informalsystems_malachitebft_engine/
sync.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use bytes::Bytes;
6use derive_where::derive_where;
7use eyre::eyre;
8
9use ractor::{Actor, ActorProcessingErr, ActorRef};
10use rand::SeedableRng;
11use tokio::task::JoinHandle;
12use tracing::{debug, error, info, warn, Instrument};
13
14use malachitebft_codec as codec;
15use malachitebft_core_consensus::PeerId;
16use malachitebft_core_types::{CommitCertificate, Context};
17use malachitebft_sync::{
18    self as sync, InboundRequestId, OutboundRequestId, RawDecidedValue, Request, Response,
19    Resumable,
20};
21
22use crate::host::{HostMsg, HostRef};
23use crate::network::{NetworkEvent, NetworkMsg, NetworkRef, Status};
24use crate::util::ticker::ticker;
25use crate::util::timers::{TimeoutElapsed, TimerScheduler};
26
27/// Codec for sync protocol messages
28///
29/// This trait is automatically implemented for any type that implements:
30/// - [`codec::Codec<sync::Status<Ctx>>`]
31/// - [`codec::Codec<sync::Request<Ctx>>`]
32/// - [`codec::Codec<sync::Response<Ctx>>`]
33pub trait SyncCodec<Ctx>
34where
35    Ctx: Context,
36    Self: codec::Codec<sync::Status<Ctx>>,
37    Self: codec::Codec<sync::Request<Ctx>>,
38    Self: codec::Codec<sync::Response<Ctx>>,
39{
40}
41
42impl<Ctx, Codec> SyncCodec<Ctx> for Codec
43where
44    Ctx: Context,
45    Codec: codec::Codec<sync::Status<Ctx>>,
46    Codec: codec::Codec<sync::Request<Ctx>>,
47    Codec: codec::Codec<sync::Response<Ctx>>,
48{
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Hash)]
52pub enum Timeout {
53    Request(OutboundRequestId),
54}
55
56type Timers = TimerScheduler<Timeout>;
57
58pub type SyncRef<Ctx> = ActorRef<Msg<Ctx>>;
59
60#[derive_where(Clone, Debug)]
61pub struct RawDecidedBlock<Ctx: Context> {
62    pub height: Ctx::Height,
63    pub certificate: CommitCertificate<Ctx>,
64    pub value_bytes: Bytes,
65}
66
67#[derive_where(Clone, Debug)]
68pub struct InflightRequest<Ctx: Context> {
69    pub peer_id: PeerId,
70    pub request_id: OutboundRequestId,
71    pub request: Request<Ctx>,
72}
73
74pub type InflightRequests<Ctx> = HashMap<OutboundRequestId, InflightRequest<Ctx>>;
75
76#[derive_where(Debug)]
77pub enum Msg<Ctx: Context> {
78    /// Internal tick
79    Tick,
80
81    /// Receive an even from gossip layer
82    NetworkEvent(NetworkEvent<Ctx>),
83
84    /// Consensus has decided on a value at the given height
85    Decided(Ctx::Height),
86
87    /// Consensus has (re)started a new height.
88    /// The boolean indicates whether this is a restart or not.
89    StartedHeight(Ctx::Height, bool),
90
91    /// Host has a response for the blocks request
92    GotDecidedValue(InboundRequestId, Ctx::Height, Option<RawDecidedValue<Ctx>>),
93
94    /// A timeout has elapsed
95    TimeoutElapsed(TimeoutElapsed<Timeout>),
96
97    /// We received an invalid value (either certificate or value) from a peer
98    InvalidValue(PeerId, Ctx::Height),
99
100    /// An error occurred while processing a value
101    ValueProcessingError(PeerId, Ctx::Height),
102}
103
104impl<Ctx: Context> From<NetworkEvent<Ctx>> for Msg<Ctx> {
105    fn from(event: NetworkEvent<Ctx>) -> Self {
106        Msg::NetworkEvent(event)
107    }
108}
109
110impl<Ctx: Context> From<TimeoutElapsed<Timeout>> for Msg<Ctx> {
111    fn from(elapsed: TimeoutElapsed<Timeout>) -> Self {
112        Msg::TimeoutElapsed(elapsed)
113    }
114}
115
116#[derive(Debug)]
117pub struct Params {
118    pub status_update_interval: Duration,
119    pub request_timeout: Duration,
120}
121
122impl Default for Params {
123    fn default() -> Self {
124        Self {
125            status_update_interval: Duration::from_secs(5),
126            request_timeout: Duration::from_secs(10),
127        }
128    }
129}
130
131pub struct State<Ctx: Context> {
132    /// The state of the sync state machine
133    sync: sync::State<Ctx>,
134
135    /// Scheduler for timers
136    timers: Timers,
137
138    /// In-flight requests
139    inflight: InflightRequests<Ctx>,
140
141    /// Task for sending status updates
142    ticker: JoinHandle<()>,
143}
144
145#[allow(dead_code)]
146pub struct Sync<Ctx: Context> {
147    ctx: Ctx,
148    gossip: NetworkRef<Ctx>,
149    host: HostRef<Ctx>,
150    params: Params,
151    sync_config: sync::Config,
152    metrics: sync::Metrics,
153    span: tracing::Span,
154}
155
156impl<Ctx> Sync<Ctx>
157where
158    Ctx: Context,
159{
160    pub fn new(
161        ctx: Ctx,
162        gossip: NetworkRef<Ctx>,
163        host: HostRef<Ctx>,
164        params: Params,
165        sync_config: sync::Config,
166        metrics: sync::Metrics,
167        span: tracing::Span,
168    ) -> Self {
169        Self {
170            ctx,
171            gossip,
172            host,
173            params,
174            sync_config,
175            metrics,
176            span,
177        }
178    }
179
180    pub async fn spawn(
181        ctx: Ctx,
182        gossip: NetworkRef<Ctx>,
183        host: HostRef<Ctx>,
184        params: Params,
185        sync_config: sync::Config,
186        metrics: sync::Metrics,
187        span: tracing::Span,
188    ) -> Result<SyncRef<Ctx>, ractor::SpawnErr> {
189        let actor = Self::new(ctx, gossip, host, params, sync_config, metrics, span);
190        let (actor_ref, _) = Actor::spawn(None, actor, ()).await?;
191        Ok(actor_ref)
192    }
193
194    async fn process_input(
195        &self,
196        myself: &ActorRef<Msg<Ctx>>,
197        state: &mut State<Ctx>,
198        input: sync::Input<Ctx>,
199    ) -> Result<(), ActorProcessingErr> {
200        malachitebft_sync::process!(
201            input: input,
202            state: &mut state.sync,
203            metrics: &self.metrics,
204            with: effect => {
205                self.handle_effect(myself, &mut state.timers, &mut state.inflight, effect).await
206            }
207        )
208    }
209
210    async fn get_history_min_height(&self) -> Result<Ctx::Height, ActorProcessingErr> {
211        ractor::call!(self.host, |reply_to| HostMsg::GetHistoryMinHeight {
212            reply_to
213        })
214        .map_err(|e| eyre!("Failed to get earliest history height: {e:?}").into())
215    }
216
217    async fn handle_effect(
218        &self,
219        myself: &ActorRef<Msg<Ctx>>,
220        timers: &mut Timers,
221        inflight: &mut InflightRequests<Ctx>,
222        effect: sync::Effect<Ctx>,
223    ) -> Result<sync::Resume<Ctx>, ActorProcessingErr> {
224        use sync::Effect;
225
226        match effect {
227            Effect::BroadcastStatus(height, r) => {
228                let history_min_height = self.get_history_min_height().await?;
229
230                self.gossip.cast(NetworkMsg::BroadcastStatus(Status::new(
231                    height,
232                    history_min_height,
233                )))?;
234
235                Ok(r.resume_with(()))
236            }
237
238            Effect::SendValueRequest(peer_id, value_request, r) => {
239                let request = Request::ValueRequest(value_request);
240                let result = ractor::call!(self.gossip, |reply_to| {
241                    NetworkMsg::OutgoingRequest(peer_id, request.clone(), reply_to)
242                });
243
244                match result {
245                    Ok(request_id) => {
246                        let request_id = OutboundRequestId::new(request_id);
247
248                        timers.start_timer(
249                            Timeout::Request(request_id.clone()),
250                            self.params.request_timeout,
251                        );
252
253                        inflight.insert(
254                            request_id.clone(),
255                            InflightRequest {
256                                peer_id,
257                                request_id: request_id.clone(),
258                                request,
259                            },
260                        );
261
262                        Ok(r.resume_with(Some(request_id)))
263                    }
264                    Err(e) => {
265                        error!("Failed to send request to network layer: {e}");
266                        Ok(r.resume_with(None))
267                    }
268                }
269            }
270
271            Effect::SendValueResponse(request_id, value_response, r) => {
272                let response = Response::ValueResponse(value_response);
273                self.gossip
274                    .cast(NetworkMsg::OutgoingResponse(request_id, response))?;
275
276                Ok(r.resume_with(()))
277            }
278
279            Effect::GetDecidedValue(request_id, height, r) => {
280                self.host.call_and_forward(
281                    |reply_to| HostMsg::GetDecidedValue { height, reply_to },
282                    myself,
283                    move |synced_value| {
284                        Msg::<Ctx>::GotDecidedValue(request_id, height, synced_value)
285                    },
286                    None,
287                )?;
288
289                Ok(r.resume_with(()))
290            }
291        }
292    }
293
294    async fn handle_msg(
295        &self,
296        myself: ActorRef<Msg<Ctx>>,
297        msg: Msg<Ctx>,
298        state: &mut State<Ctx>,
299    ) -> Result<(), ActorProcessingErr> {
300        match msg {
301            Msg::Tick => {
302                self.process_input(&myself, state, sync::Input::Tick)
303                    .await?;
304            }
305
306            Msg::NetworkEvent(NetworkEvent::PeerDisconnected(peer_id)) => {
307                info!(%peer_id, "Disconnected from peer");
308
309                if state.sync.peers.remove(&peer_id).is_some() {
310                    debug!(%peer_id, "Removed disconnected peer");
311                }
312            }
313
314            Msg::NetworkEvent(NetworkEvent::Status(peer_id, status)) => {
315                let status = sync::Status {
316                    peer_id,
317                    tip_height: status.tip_height,
318                    history_min_height: status.history_min_height,
319                };
320
321                self.process_input(&myself, state, sync::Input::Status(status))
322                    .await?;
323            }
324
325            Msg::NetworkEvent(NetworkEvent::SyncRequest(request_id, from, request)) => {
326                match request {
327                    Request::ValueRequest(value_request) => {
328                        self.process_input(
329                            &myself,
330                            state,
331                            sync::Input::ValueRequest(request_id, from, value_request),
332                        )
333                        .await?;
334                    }
335                };
336            }
337
338            Msg::NetworkEvent(NetworkEvent::SyncResponse(request_id, peer, response)) => {
339                // Cancel the timer associated with the request for which we just received a response
340                state.timers.cancel(&Timeout::Request(request_id.clone()));
341
342                match response {
343                    Some(Response::ValueResponse(value_response)) => {
344                        self.process_input(
345                            &myself,
346                            state,
347                            sync::Input::ValueResponse(request_id, peer, Some(value_response)),
348                        )
349                        .await?;
350                    }
351
352                    None => {
353                        self.process_input(
354                            &myself,
355                            state,
356                            sync::Input::ValueResponse(request_id, peer, None),
357                        )
358                        .await?;
359                    }
360                }
361            }
362
363            Msg::NetworkEvent(_) => {
364                // Ignore other gossip events
365            }
366
367            // (Re)Started a new height
368            Msg::StartedHeight(height, restart) => {
369                self.process_input(&myself, state, sync::Input::StartedHeight(height, restart))
370                    .await?
371            }
372
373            // Decided on a value
374            Msg::Decided(height) => {
375                self.process_input(&myself, state, sync::Input::Decided(height))
376                    .await?;
377            }
378
379            Msg::GotDecidedValue(request_id, height, block) => {
380                self.process_input(
381                    &myself,
382                    state,
383                    sync::Input::GotDecidedValue(request_id, height, block),
384                )
385                .await?;
386            }
387
388            Msg::InvalidValue(peer, height) => {
389                self.process_input(&myself, state, sync::Input::InvalidValue(peer, height))
390                    .await?
391            }
392
393            Msg::ValueProcessingError(peer, height) => {
394                self.process_input(
395                    &myself,
396                    state,
397                    sync::Input::ValueProcessingError(peer, height),
398                )
399                .await?
400            }
401
402            Msg::TimeoutElapsed(elapsed) => {
403                let Some(timeout) = state.timers.intercept_timer_msg(elapsed) else {
404                    // Timer was cancelled or already processed, ignore
405                    return Ok(());
406                };
407
408                warn!(?timeout, "Timeout elapsed");
409
410                match timeout {
411                    Timeout::Request(request_id) => {
412                        if let Some(inflight) = state.inflight.remove(&request_id) {
413                            self.process_input(
414                                &myself,
415                                state,
416                                sync::Input::SyncRequestTimedOut(
417                                    inflight.peer_id,
418                                    inflight.request,
419                                ),
420                            )
421                            .await?;
422                        } else {
423                            debug!(%request_id, "Timeout for unknown request");
424                        }
425                    }
426                }
427            }
428        }
429
430        Ok(())
431    }
432}
433
434#[async_trait]
435impl<Ctx> Actor for Sync<Ctx>
436where
437    Ctx: Context,
438{
439    type Msg = Msg<Ctx>;
440    type State = State<Ctx>;
441    type Arguments = ();
442
443    async fn pre_start(
444        &self,
445        myself: ActorRef<Self::Msg>,
446        _args: Self::Arguments,
447    ) -> Result<Self::State, ActorProcessingErr> {
448        self.gossip
449            .cast(NetworkMsg::Subscribe(Box::new(myself.clone())))?;
450
451        let ticker = tokio::spawn(
452            ticker(self.params.status_update_interval, myself.clone(), || {
453                Msg::Tick
454            })
455            .in_current_span(),
456        );
457
458        let rng = Box::new(rand::rngs::StdRng::from_entropy());
459
460        Ok(State {
461            sync: sync::State::new(rng, self.sync_config),
462            timers: Timers::new(Box::new(myself.clone())),
463            inflight: HashMap::new(),
464            ticker,
465        })
466    }
467
468    #[tracing::instrument(
469        name = "sync",
470        parent = &self.span,
471        skip_all,
472        fields(
473            height.tip = %state.sync.tip_height,
474            height.sync = %state.sync.sync_height,
475        ),
476    )]
477    async fn handle(
478        &self,
479        myself: ActorRef<Self::Msg>,
480        msg: Self::Msg,
481        state: &mut Self::State,
482    ) -> Result<(), ActorProcessingErr> {
483        if let Err(e) = self.handle_msg(myself, msg, state).await {
484            error!("Error handling message: {e:?}");
485        }
486
487        Ok(())
488    }
489
490    async fn post_stop(
491        &self,
492        _myself: ActorRef<Self::Msg>,
493        state: &mut Self::State,
494    ) -> Result<(), ActorProcessingErr> {
495        state.ticker.abort();
496        Ok(())
497    }
498}