1use crate::dkg::{ReshareBlock, network::Directory, types::Payload};
6use commonware_actor::{
7 Feedback,
8 mailbox::{Policy, Sender as ActorSender},
9};
10use commonware_consensus::{
11 Reporter,
12 marshal::{
13 Update,
14 ancestry::{Ancestry, BoxedAncestry},
15 },
16 types::Height,
17};
18use commonware_cryptography::{Signer, bls12381::primitives::variant::Variant};
19use commonware_runtime::telemetry::traces::TracedExt as _;
20use commonware_utils::{Acknowledgement, acknowledgement::Exact, channel::oneshot, sequence::Unit};
21use std::{collections::VecDeque, sync::Arc};
22use tracing::{Span, error, info_span};
23
24#[derive(Clone, PartialEq, Eq)]
26pub enum EpochInfoResponse<V, C, D = Unit>
27where
28 V: Variant,
29 C: Signer,
30 D: Directory<C::PublicKey>,
31{
32 Available(Option<Payload<V, C, D>>),
37 Pending,
42 Following,
47 Unavailable,
49}
50
51#[must_use = "dropping a log reservation releases it for another proposal"]
57pub struct LogReservation<B, V, C, A = Exact>
58where
59 B: ReshareBlock<Variant = V, Signer = C>,
60 V: Variant,
61 C: Signer,
62 A: Acknowledgement,
63{
64 height: Height,
65 payload: Option<Payload<V, C, B::Directory>>,
66 release: Option<ActorSender<Message<B, V, C, A>>>,
67}
68
69impl<B, V, C, A> LogReservation<B, V, C, A>
70where
71 B: ReshareBlock<Variant = V, Signer = C>,
72 V: Variant,
73 C: Signer,
74 A: Acknowledgement,
75{
76 pub(crate) const fn new(
77 height: Height,
78 payload: Payload<V, C, B::Directory>,
79 release: ActorSender<Message<B, V, C, A>>,
80 ) -> Self {
81 Self {
82 height,
83 payload: Some(payload),
84 release: Some(release),
85 }
86 }
87
88 pub const fn take_payload(&mut self) -> Option<Payload<V, C, B::Directory>> {
92 self.payload.take()
93 }
94
95 pub fn included(mut self) {
98 self.release = None;
99 }
100}
101
102impl<B, V, C, A> Drop for LogReservation<B, V, C, A>
103where
104 B: ReshareBlock<Variant = V, Signer = C>,
105 V: Variant,
106 C: Signer,
107 A: Acknowledgement,
108{
109 fn drop(&mut self) {
110 let Some(release) = self.release.take() else {
111 return;
112 };
113 let _ = release.enqueue(Message::ReleaseLog {
114 height: self.height,
115 });
116 }
117}
118
119#[allow(clippy::large_enum_variant)]
123pub enum Message<B, V, C, A = Exact>
124where
125 B: ReshareBlock<Variant = V, Signer = C>,
126 V: Variant,
127 C: Signer,
128 A: Acknowledgement,
129{
130 NextLog {
137 span: Span,
138 height: Height,
139 release: ActorSender<Self>,
140 response: oneshot::Sender<Option<LogReservation<B, V, C, A>>>,
141 },
142
143 ReleaseLog { height: Height },
146
147 EpochInfo {
149 span: Span,
150 ancestry: BoxedAncestry<B>,
151 response: oneshot::Sender<EpochInfoResponse<V, C, B::Directory>>,
152 },
153
154 Finalized {
156 span: Span,
157 block: Arc<B>,
158 response: A,
159 },
160}
161
162impl<B, V, C, A> Message<B, V, C, A>
163where
164 B: ReshareBlock<Variant = V, Signer = C>,
165 V: Variant,
166 C: Signer,
167 A: Acknowledgement,
168{
169 fn response_closed(&self) -> bool {
170 match self {
171 Self::NextLog { response, .. } => response.is_closed(),
172 Self::ReleaseLog { .. } => false,
173 Self::EpochInfo { response, .. } => response.is_closed(),
174 Self::Finalized { .. } => false,
175 }
176 }
177}
178
179impl<B, V, C, A> Policy for Message<B, V, C, A>
180where
181 B: ReshareBlock<Variant = V, Signer = C>,
182 V: Variant,
183 C: Signer,
184 A: Acknowledgement,
185{
186 type Overflow = VecDeque<Self>;
187
188 fn handle(overflow: &mut VecDeque<Self>, message: Self) {
189 if message.response_closed() {
190 return;
191 }
192 overflow.push_back(message);
193 }
194}
195
196#[derive(Clone)]
200pub struct Mailbox<B, V, C, A = Exact>
201where
202 B: ReshareBlock<Variant = V, Signer = C>,
203 V: Variant,
204 C: Signer,
205 A: Acknowledgement,
206{
207 sender: ActorSender<Message<B, V, C, A>>,
208}
209
210impl<B, V, C, A> Mailbox<B, V, C, A>
211where
212 B: ReshareBlock<Variant = V, Signer = C>,
213 V: Variant,
214 C: Signer,
215 A: Acknowledgement,
216{
217 pub const fn new(sender: ActorSender<Message<B, V, C, A>>) -> Self {
219 Self { sender }
220 }
221
222 pub async fn next_log(&mut self, height: Height) -> Option<LogReservation<B, V, C, A>> {
226 let (response_tx, response_rx) = oneshot::channel();
227 let span = info_span!("dkg.reshare.mailbox.next_log", height = height.traced());
228 if !self
229 .sender
230 .enqueue(Message::NextLog {
231 span,
232 height,
233 release: self.sender.clone(),
234 response: response_tx,
235 })
236 .accepted()
237 {
238 error!("failed to send request for next dealer log");
239 return None;
240 }
241
242 match response_rx.await {
243 Ok(outcome) => outcome,
244 Err(err) => {
245 error!(?err, "failed to receive payload response");
246 None
247 }
248 }
249 }
250
251 pub async fn epoch_info(
256 &mut self,
257 ancestry: impl Ancestry<B>,
258 ) -> EpochInfoResponse<V, C, B::Directory> {
259 let (response_tx, response_rx) = oneshot::channel();
260 let span = info_span!("dkg.reshare.mailbox.epoch_info");
261 if !self
262 .sender
263 .enqueue(Message::EpochInfo {
264 span,
265 ancestry: BoxedAncestry::new(ancestry),
266 response: response_tx,
267 })
268 .accepted()
269 {
270 error!("failed to send request for epoch info");
271 return EpochInfoResponse::Unavailable;
272 }
273
274 match response_rx.await {
275 Ok(outcome) => outcome,
276 Err(err) => {
277 error!(?err, "failed to receive epoch info response");
278 EpochInfoResponse::Unavailable
279 }
280 }
281 }
282}
283
284impl<B, V, C, A> Reporter for Mailbox<B, V, C, A>
285where
286 B: ReshareBlock<Variant = V, Signer = C>,
287 V: Variant,
288 C: Signer,
289 A: Acknowledgement,
290{
291 type Activity = Update<B, A>;
292
293 fn report(&mut self, update: Self::Activity) -> Feedback {
294 let Update::Block(block, ack_tx) = update else {
295 return Feedback::Ok;
296 };
297 let span = info_span!(
298 "dkg.reshare.mailbox.finalized",
299 height = block.height().traced(),
300 digest = %block.digest()
301 );
302 self.sender.enqueue(Message::Finalized {
303 span,
304 block,
305 response: ack_tx,
306 })
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::dkg::tests::mocks::{self, TestBlock, TestBlsVariant};
314 use commonware_actor::mailbox;
315 use commonware_cryptography::{Digestible as _, ed25519::PrivateKey};
316 use commonware_runtime::{Runner, deterministic};
317 use commonware_utils::{NZUsize, channel::oneshot};
318 use futures::{FutureExt as _, StreamExt as _};
319 use std::{
320 pin::Pin,
321 task::{Context, Poll},
322 };
323
324 type TestMessage = Message<TestBlock, TestBlsVariant, PrivateKey>;
325
326 #[derive(Clone)]
327 struct DelayedAncestry {
328 parent: Option<Arc<TestBlock>>,
329 gate: futures::future::Shared<oneshot::Receiver<()>>,
330 }
331
332 impl futures::Stream for DelayedAncestry {
333 type Item = Arc<TestBlock>;
334
335 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
336 if self.gate.poll_unpin(cx).is_pending() {
337 return Poll::Pending;
338 }
339 Poll::Ready(self.parent.take())
340 }
341 }
342
343 impl Ancestry<TestBlock> for DelayedAncestry {
344 fn peek(&self) -> Option<&TestBlock> {
345 None
346 }
347 }
348
349 #[test]
350 fn next_log_returns_none_when_actor_gone() {
351 let executor = deterministic::Runner::default();
352 executor.start(|context| async move {
353 let (sender, receiver) = mailbox::new::<TestMessage>(context, NZUsize!(1));
354 drop(receiver);
355
356 let mut mailbox = Mailbox::<TestBlock, TestBlsVariant, PrivateKey>::new(sender);
357
358 assert!(mailbox.next_log(Height::new(1)).await.is_none());
359 });
360 }
361
362 #[test]
363 fn epoch_info_forwards_delayed_parent_without_polling() {
364 let executor = deterministic::Runner::default();
365 executor.start(|context| async move {
366 let (sender, mut receiver) = mailbox::new::<TestMessage>(context, NZUsize!(1));
367 let mut mailbox = Mailbox::<TestBlock, TestBlsVariant, PrivateKey>::new(sender);
368 let parent = Arc::new(mocks::genesis_block(PrivateKey::from_seed(0).public_key()));
369 let (release, gate) = oneshot::channel();
370 let ancestry = DelayedAncestry {
371 parent: Some(parent.clone()),
372 gate: gate.shared(),
373 };
374 let mut request = Box::pin(mailbox.epoch_info(ancestry));
375
376 assert!(request.as_mut().now_or_never().is_none());
377 let message = receiver
378 .try_recv()
379 .expect("request should reach the actor without polling ancestry");
380 let Message::EpochInfo {
381 mut ancestry,
382 response,
383 ..
384 } = message
385 else {
386 panic!("expected epoch info request");
387 };
388 assert!(ancestry.next().now_or_never().is_none());
389
390 release.send(()).expect("ancestry should still be waiting");
391 assert_eq!(
392 ancestry
393 .next()
394 .await
395 .expect("parent should remain in ancestry")
396 .digest(),
397 parent.digest()
398 );
399 assert!(response.send(EpochInfoResponse::Available(None)).is_ok());
400 assert!(matches!(request.await, EpochInfoResponse::Available(None)));
401 });
402 }
403
404 #[test]
405 fn canceled_epoch_info_closes_forwarded_response_without_polling_ancestry() {
406 let executor = deterministic::Runner::default();
407 executor.start(|context| async move {
408 let (sender, mut receiver) = mailbox::new::<TestMessage>(context, NZUsize!(1));
409 let mut mailbox = Mailbox::<TestBlock, TestBlsVariant, PrivateKey>::new(sender);
410 let parent = Arc::new(mocks::genesis_block(PrivateKey::from_seed(0).public_key()));
411 let (release, gate) = oneshot::channel();
412 let ancestry = DelayedAncestry {
413 parent: Some(parent),
414 gate: gate.shared(),
415 };
416 let mut request = Box::pin(mailbox.epoch_info(ancestry));
417
418 assert!(request.as_mut().now_or_never().is_none());
419 let Message::EpochInfo {
420 ancestry, response, ..
421 } = receiver
422 .try_recv()
423 .expect("request should reach the actor without polling ancestry")
424 else {
425 panic!("expected epoch info request");
426 };
427 drop(request);
428
429 assert!(response.is_closed());
430 assert!(release.send(()).is_ok());
431 drop(ancestry);
432 });
433 }
434}