Skip to main content

commonware_glue/dkg/reshare/
mailbox.rs

1//! Reshare [`Actor`] ingress.
2//!
3//! [`Actor`]: super::Actor
4
5use 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/// Response to a final-block epoch artifact request.
25#[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    /// The actor derived a stable response.
33    ///
34    /// `None` is a legitimate response only for a failed one-shot DKG final
35    /// block, which intentionally carries no epoch artifact.
36    Available(Option<Payload<V, C, D>>),
37    /// The actor cannot answer this request yet.
38    ///
39    /// This is not evidence that a proposed artifact is invalid. Verification
40    /// remains pending until the request is canceled or local progress catches up.
41    Pending,
42    /// The actor is following the epoch without its protocol history.
43    ///
44    /// It cannot derive the artifact. This is not evidence that a proposed
45    /// artifact is valid or invalid.
46    Following,
47    /// The actor was expected to derive the artifact but cannot produce it.
48    Unavailable,
49}
50
51/// A dealer log reserved for one proposal attempt.
52///
53/// Dropping the reservation releases the log back to the reshare actor. Call
54/// [`included`](Self::included) only after the wrapped application returns a
55/// block for the proposal attempt that received this payload.
56#[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    /// Takes the reserved dealer log payload.
89    ///
90    /// Returns `None` if the payload was already taken.
91    pub const fn take_payload(&mut self) -> Option<Payload<V, C, B::Directory>> {
92        self.payload.take()
93    }
94
95    /// Keeps the log reserved for this height until finalization confirms
96    /// whether the proposal landed on-chain.
97    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/// A message that can be sent to the [`Actor`].
120///
121/// [`Actor`]: super::Actor
122#[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    /// A request for the next finalized dealer log to include before the final
131    /// block of the epoch.
132    ///
133    /// `height` is the height of the block being proposed. The actor uses it to
134    /// avoid re-offering a log into competing proposals while one it already
135    /// served into may still finalize.
136    NextLog {
137        span: Span,
138        height: Height,
139        release: ActorSender<Self>,
140        response: oneshot::Sender<Option<LogReservation<B, V, C, A>>>,
141    },
142
143    /// A proposal attempt was canceled or returned no block after receiving a
144    /// dealer log.
145    ReleaseLog { height: Height },
146
147    /// A request for the final block's speculative [`EpochInfo`](crate::dkg::types::EpochInfo).
148    EpochInfo {
149        span: Span,
150        ancestry: BoxedAncestry<B>,
151        response: oneshot::Sender<EpochInfoResponse<V, C, B::Directory>>,
152    },
153
154    /// A new block has been finalized.
155    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/// Inbox for sending messages to the reshare [`Actor`].
197///
198/// [`Actor`]: super::Actor
199#[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    /// Create a new mailbox.
218    pub const fn new(sender: ActorSender<Message<B, V, C, A>>) -> Self {
219        Self { sender }
220    }
221
222    /// Request a dealer log for inclusion before the final block of the epoch.
223    ///
224    /// `height` is the height of the block being proposed.
225    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    /// Request the final block's next-epoch artifact.
252    ///
253    /// Verification ancestry includes the final candidate, while proposal
254    /// ancestry begins at its parent. The actor reconstructs either view lazily.
255    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}