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
27pub 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 Tick,
80
81 NetworkEvent(NetworkEvent<Ctx>),
83
84 Decided(Ctx::Height),
86
87 StartedHeight(Ctx::Height, bool),
90
91 GotDecidedValue(InboundRequestId, Ctx::Height, Option<RawDecidedValue<Ctx>>),
93
94 TimeoutElapsed(TimeoutElapsed<Timeout>),
96
97 InvalidValue(PeerId, Ctx::Height),
99
100 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 sync: sync::State<Ctx>,
134
135 timers: Timers,
137
138 inflight: InflightRequests<Ctx>,
140
141 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 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 }
366
367 Msg::StartedHeight(height, restart) => {
369 self.process_input(&myself, state, sync::Input::StartedHeight(height, restart))
370 .await?
371 }
372
373 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 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}