Skip to main content

commonware_glue/dkg/reshare/actor/
mod.rs

1//! Drive per-epoch BLS resharing from finalized marshal state.
2//!
3//! The actor bridges finalized epoch metadata, the Feldman-Desmedt reshare
4//! protocol, P2P dealer traffic, and certificate-scheme registration. Each loop
5//! iteration derives the active epoch from marshal's processed height, loads the
6//! epoch's public [`EpochInfo`] from the finalized
7//! boundary block, and either participates in the ceremony or follows until the
8//! next boundary is finalized.
9//!
10//! # Epoch Lifecycle
11//!
12//! A participating epoch has three states:
13//!
14//! 1. **Setup** reads the canonical boundary block, replays durable recovery
15//!    state, opens the epoch peer set, registers the current scheme with the
16//!    [`Registrar`], and prepares optional dealer/player state for this node.
17//! 2. **Dealing** runs during the early half of the epoch. Dealers send private
18//!    shares to players over P2P and players return signed acknowledgements.
19//! 3. **Inclusion** runs from the midpoint through the final block. The actor
20//!    offers one finalized dealer log to the application, observes finalized
21//!    logs on-chain, computes the next [`EpochInfo`],
22//!    and registers the next epoch once that boundary block finalizes.
23//!
24//! ```text
25//! finalized boundary for epoch N
26//!        |
27//!        v
28//! setup: load EpochInfo(N), share, seed, recovery journal
29//!        |
30//!        +-- no boundary info and already inside epoch --> follower mode
31//!        |
32//!        v
33//! early blocks
34//!        |
35//!        v
36//! dealing: dealer shares <--> player acknowledgements
37//!        |
38//!        v
39//! midpoint
40//!        |
41//!        v
42//! inclusion: propose/observe dealer logs
43//!        |
44//!        v
45//! final block carries EpochInfo(N + 1)
46//!        |
47//!        v
48//! register scheme for epoch N + 1
49//! ```
50//!
51//! # Payload Flow
52//!
53//! Consensus asks the actor for an optional payload before proposing each block,
54//! and reports finalized blocks after marshal processes them:
55//!
56//! ```text
57//! application --Next(height)-----------> Actor --Payload?----------> application
58//! marshal     --Finalized(block)-------> Actor --acknowledge-------> marshal
59//! peer        --Dealer/Ack(epoch)------> Actor --Ack/Dealer(epoch)-> peer
60//! ```
61//!
62//! During dealing, `Next` never returns a payload. During inclusion, `Next`
63//! returns at most one dealer log before the final height, and returns the
64//! computed [`EpochInfo`] at the final height when
65//! enough valid logs are available. Finalized blocks are the source of truth:
66//! only logs and epoch info that appear in finalized blocks update durable state
67//! or registered schemes.
68//!
69//! # Crash Recovery
70//!
71//! Recovery state is split by sensitivity. Public, replayable protocol messages
72//! are journaled by [`Store`]: dealer public messages, player acknowledgements,
73//! and finalized dealer logs. Secret material is kept only in [`SecretStore`]:
74//! current shares, private dealings, and dealer RNG seeds. Public epoch info is
75//! normally re-derived from finalized boundary blocks; state-sync startup
76//! material is retained separately and removed on a later startup after marshal
77//! has advanced beyond its epoch.
78//!
79//! ```text
80//! restart
81//!   |
82//!   +--> marshal processed height determines candidate epoch
83//!   |
84//!   +--> boundary block supplies canonical EpochInfo
85//!   |
86//!   +--> Store replays public journal
87//!   |
88//!   +--> SecretStore supplies share, private dealings, and seed
89//!   |
90//!   v
91//! resume as dealer/player/observer when enough state is available
92//! ```
93//!
94//! Reusing the persisted dealer seed makes regenerated dealer shares identical
95//! after a restart. Persisted acknowledgements and finalized logs let a player or
96//! observer rebuild the same outcome even though P2P messages and finalized-block
97//! notifications are not replayed by the runtime. If the node lacks a valid share
98//! for a dealer role, it simply observes or plays instead of manufacturing local
99//! state.
100//!
101//! # Follower Mode
102//!
103//! The actor follows instead of participating when setup cannot read the boundary
104//! [`EpochInfo`] for the epoch containing marshal's next unprocessed height, or
105//! when a state-sync floor skips part of the inclusion window. In either
106//! case the actor lacks the public history needed to reconstruct the ceremony.
107//!
108//! ```text
109//! processed height + 1 = H
110//!        |
111//!        v
112//! H is in epoch N
113//!        |
114//!        v
115//! boundary EpochInfo(N) unavailable locally
116//! or state-sync floor skipped inclusion blocks
117//!        |
118//!        v
119//! follower mode until final(N)
120//!        |
121//!        v
122//! final(N) carries EpochInfo(N + 1)
123//!        |
124//!        +--> failed ceremony and prior share held -> register signer
125//!        |
126//!        +--> otherwise -> register verifier
127//!        |
128//!        v
129//! setup again
130//! ```
131//!
132//! While following, `Next` always returns no payload and finalized blocks are
133//! acknowledged without mutation until the final block of the current epoch. The
134//! final block's epoch info is used as the next loop's boundary state. When a
135//! failed ceremony carries the previous threshold output forward, the actor also
136//! carries forward a locally held share and registers as a signer. Otherwise, it
137//! commits without a share and registers as a verifier.
138
139use crate::dkg::{
140    ParticipantsProvider, Registrar, ReshareBlock, SecretStore,
141    fence::Fence,
142    network::{Directory, Manager},
143    reshare::{Mailbox, Message, metrics::Metrics as ReshareMetrics, store::Store},
144    state_sync::{self, Plan as StateSyncPlan},
145    types::EpochInfo,
146};
147use commonware_actor::mailbox::{self as actor_mailbox, Receiver as MailboxReceiver};
148use commonware_consensus::{
149    Heightable as _,
150    marshal::core::{CommitmentFallback, Mailbox as MarshalMailbox, Variant as MarshalVariant},
151    simplex::scheme::Scheme as SimplexScheme,
152    types::{EpochPhase, FixedEpocher},
153};
154use commonware_cryptography::{
155    BatchVerifier, PublicKey, Signer,
156    bls12381::{
157        dkg::feldman_desmedt::Reveal,
158        primitives::{sharing::Mode as SharingMode, variant::Variant as BlsVariant},
159    },
160    certificate::Scheme,
161};
162use commonware_p2p::{Blocker, Receiver, Sender, utils::mux::Muxer};
163use commonware_parallel::Strategy;
164use commonware_runtime::{
165    BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage, spawn_cell,
166};
167use commonware_utils::{Acknowledgement, acknowledgement::Exact, ordered::Set};
168use rand_core::CryptoRng;
169use std::{
170    marker::PhantomData,
171    num::{NonZeroU32, NonZeroU64, NonZeroUsize},
172};
173
174type DkgCompletion<V, P, D> = Box<dyn FnOnce(Option<EpochInfo<V, P, D>>) + Send>;
175
176mod dealing;
177mod dkg;
178mod follower;
179mod inclusion;
180mod setup;
181#[cfg(test)]
182mod utils;
183use setup::{Setup, StateSyncStart};
184
185/// Configuration for the crate-private one-shot DKG mode.
186pub(crate) struct DkgConfig<V, P, D>
187where
188    V: BlsVariant,
189    P: PublicKey,
190    D: Directory<P>,
191{
192    pub(crate) participants: Set<P>,
193    /// Transport directory for the one-shot ceremony's participants, embedded
194    /// verbatim in the emitted epoch-zero artifact. Every participant must
195    /// configure the same directory.
196    pub(crate) directory: D,
197    pub(crate) completion: DkgCompletion<V, P, D>,
198}
199
200enum Mode<V, P, D>
201where
202    V: BlsVariant,
203    P: PublicKey,
204    D: Directory<P>,
205{
206    Reshare,
207    Dkg {
208        participants: Set<P>,
209        directory: D,
210        completion: Option<DkgCompletion<V, P, D>>,
211    },
212}
213
214/// Configuration for [`Actor`].
215pub struct Config<C, M, X, P, SS, T, BV, S, MV, R>
216where
217    C: Signer,
218    X: Blocker<PublicKey = C::PublicKey>,
219    S: Scheme + SimplexScheme<MV::Commitment, PublicKey = C::PublicKey>,
220    MV: MarshalVariant,
221    MV::ApplicationBlock: ReshareBlock,
222    <MV::ApplicationBlock as ReshareBlock>::Signer: Signer<PublicKey = C::PublicKey>,
223    R: Registrar<PublicKey = C::PublicKey>,
224{
225    /// Signer for player acknowledgments and dealer logs.
226    pub signer: C,
227
228    /// P2P manager used to track peers during one-shot DKG.
229    ///
230    /// Continuous reshare peer tracking is owned by
231    /// [`orchestrator`](crate::dkg::orchestrator).
232    pub manager: M,
233
234    /// Blocker used to block peers that send invalid protocol messages.
235    pub blocker: X,
236
237    /// Provider of participant policy.
238    pub participants_provider: P,
239
240    /// Store for private share material.
241    pub secret_store: SS,
242
243    /// Parallel strategy for cryptographic verification.
244    pub strategy: T,
245
246    /// Registrar for configuring signing scheme providers.
247    pub registrar: R,
248
249    /// Marshal mailbox used to read canonical public epoch state from finalized
250    /// boundary blocks.
251    pub marshal: MarshalMailbox<S, MV>,
252
253    /// Shared DKG state-sync startup recovery plan.
254    pub state_sync: StateSyncPlan<
255        S,
256        MV::Commitment,
257        R::Variant,
258        <MV::ApplicationBlock as ReshareBlock>::Directory,
259    >,
260
261    /// Epoch readiness fence.
262    pub fence: Fence,
263
264    /// Application namespace for transcript separation.
265    pub namespace: &'static [u8],
266
267    /// Sharing mode used for newly generated threshold outputs.
268    pub sharing_mode: SharingMode,
269
270    /// Revealed-share calculation used for each newly prepared ceremony.
271    pub reveal: Reveal,
272
273    /// Actor mailbox capacity.
274    pub mailbox_size: NonZeroUsize,
275
276    /// Runtime-storage partition prefix.
277    pub partition_prefix: String,
278
279    /// Maximum entries accepted in each decoded or provider-supplied
280    /// participant set.
281    pub max_participants: NonZeroU32,
282
283    /// Epoch schedule used to interpret finalized block heights.
284    pub blocks_per_epoch: NonZeroU64,
285
286    /// Batch verifier marker.
287    pub batch_verifier: PhantomData<BV>,
288}
289
290pub struct Actor<E, B, V, C, M, X, P, SS, T, BV, S, MV, R, A = Exact>
291where
292    E: Spawner + CryptoRng + Metrics + BufferPooler + Clock + Storage,
293    B: ReshareBlock<Variant = V, Signer = C>,
294    V: BlsVariant,
295    C: Signer,
296    M: Manager<PublicKey = C::PublicKey, Directory = B::Directory>,
297    X: Blocker<PublicKey = C::PublicKey>,
298    P: ParticipantsProvider<PublicKey = C::PublicKey, Directory = B::Directory>,
299    SS: SecretStore,
300    T: Strategy,
301    BV: BatchVerifier<PublicKey = C::PublicKey> + Send + 'static,
302    S: Scheme + SimplexScheme<MV::Commitment, PublicKey = C::PublicKey>,
303    MV: MarshalVariant<ApplicationBlock = B>,
304    R: Registrar<Variant = V, PublicKey = C::PublicKey>,
305    A: Acknowledgement,
306{
307    context: ContextCell<E>,
308    mailbox: MailboxReceiver<Message<B, V, C, A>>,
309    signer: C,
310    manager: M,
311    blocker: X,
312    participants_provider: P,
313    secret_store: Option<SS>,
314    strategy: T,
315    registrar: R,
316    marshal: MarshalMailbox<S, MV>,
317    state_sync: StateSyncPlan<S, MV::Commitment, V, B::Directory>,
318    fence: Fence,
319    namespace: &'static [u8],
320    sharing_mode: SharingMode,
321    reveal: Reveal,
322    partition_prefix: String,
323    max_participants: NonZeroU32,
324    blocks_per_epoch: NonZeroU64,
325    epocher: FixedEpocher,
326    metrics: ReshareMetrics<C::PublicKey>,
327    mode: Mode<V, C::PublicKey, B::Directory>,
328    batch_verifier: PhantomData<BV>,
329}
330
331impl<E, B, V, C, M, X, P, SS, T, BV, S, MV, R, A> Actor<E, B, V, C, M, X, P, SS, T, BV, S, MV, R, A>
332where
333    E: Spawner + CryptoRng + Metrics + BufferPooler + Clock + Storage,
334    B: ReshareBlock<Variant = V, Signer = C>,
335    V: BlsVariant,
336    C: Signer,
337    M: Manager<PublicKey = C::PublicKey, Directory = B::Directory>,
338    X: Blocker<PublicKey = C::PublicKey>,
339    P: ParticipantsProvider<PublicKey = C::PublicKey, Directory = B::Directory>,
340    SS: SecretStore,
341    T: Strategy,
342    BV: BatchVerifier<PublicKey = C::PublicKey> + Send + 'static,
343    S: Scheme + SimplexScheme<MV::Commitment, PublicKey = C::PublicKey>,
344    MV: MarshalVariant<ApplicationBlock = B>,
345    R: Registrar<Variant = V, PublicKey = C::PublicKey>,
346    A: Acknowledgement,
347{
348    pub fn new(
349        context: E,
350        config: Config<C, M, X, P, SS, T, BV, S, MV, R>,
351    ) -> (Self, Mailbox<B, V, C, A>) {
352        let epocher = FixedEpocher::new(config.blocks_per_epoch);
353        let (sender, mailbox) = actor_mailbox::new(context.child("mailbox"), config.mailbox_size);
354        let metrics = ReshareMetrics::new(&context);
355        (
356            Self {
357                context: ContextCell::new(context),
358                mailbox,
359                signer: config.signer,
360                manager: config.manager,
361                blocker: config.blocker,
362                participants_provider: config.participants_provider,
363                secret_store: Some(config.secret_store),
364                strategy: config.strategy,
365                registrar: config.registrar,
366                marshal: config.marshal,
367                state_sync: config.state_sync,
368                fence: config.fence,
369                namespace: config.namespace,
370                sharing_mode: config.sharing_mode,
371                reveal: config.reveal,
372                partition_prefix: config.partition_prefix,
373                max_participants: config.max_participants,
374                blocks_per_epoch: config.blocks_per_epoch,
375                epocher,
376                metrics,
377                mode: Mode::Reshare,
378                batch_verifier: config.batch_verifier,
379            },
380            Mailbox::new(sender),
381        )
382    }
383
384    pub(crate) fn new_dkg(
385        context: E,
386        config: Config<C, M, X, P, SS, T, BV, S, MV, R>,
387        dkg: DkgConfig<V, C::PublicKey, B::Directory>,
388    ) -> (Self, Mailbox<B, V, C, A>) {
389        let (mut actor, mailbox) = Self::new(context, config);
390        actor.mode = Mode::Dkg {
391            participants: dkg.participants,
392            directory: dkg.directory,
393            completion: Some(dkg.completion),
394        };
395        (actor, mailbox)
396    }
397
398    pub fn start<SE, RE>(mut self, chan: (SE, RE)) -> Handle<()>
399    where
400        SE: Sender<PublicKey = C::PublicKey>,
401        RE: Receiver<PublicKey = C::PublicKey>,
402    {
403        spawn_cell!(self.context, self.run(chan))
404    }
405
406    async fn run<SE, RE>(mut self, (sender, receiver): (SE, RE))
407    where
408        SE: Sender<PublicKey = C::PublicKey>,
409        RE: Receiver<PublicKey = C::PublicKey>,
410    {
411        let secret_store = self
412            .secret_store
413            .take()
414            .expect("secret store must be available when actor starts");
415        let mut store = Store::init(
416            self.context.child("store"),
417            &self.partition_prefix,
418            self.max_participants,
419            secret_store,
420        )
421        .await;
422
423        let (mux, mut dealing_mux) = Muxer::new(self.context.child("mux"), sender, receiver, 128);
424        mux.start();
425
426        let recovered_epoch = state_sync::recovered_epoch(&self.marshal, &self.epocher).await;
427        let state_sync = self
428            .state_sync
429            .resolve(
430                self.context.as_present().child("state_sync"),
431                recovered_epoch,
432            )
433            .await;
434
435        // Install the recovered epoch scheme, then materialize the certified
436        // floor commitment and retain its height with the epoch metadata. Setup
437        // uses that bound to decide whether the public dealer-log window is
438        // replayable.
439        let mut state_sync = if let Some(state_sync) = state_sync {
440            let share = self.recovered_share(&mut store, &state_sync.info).await;
441            self.register_epoch(&state_sync.info, share).await;
442            let floor = self
443                .marshal
444                .subscribe_by_commitment(
445                    state_sync.floor.proposal.payload,
446                    CommitmentFallback::Wait,
447                )
448                .await
449                .expect("marshal must yield state sync floor block");
450            Some(StateSyncStart {
451                info: state_sync.info,
452                floor: floor.height(),
453            })
454        } else {
455            None
456        };
457
458        if matches!(self.mode, Mode::Dkg { .. }) {
459            self.run_dkg(&mut store, &mut dealing_mux).await;
460            return;
461        }
462
463        let mut current_epoch = state_sync.as_ref().map(|start| start.info.epoch);
464        loop {
465            let Some(prepared) = self
466                .setup(&mut store, current_epoch.take(), state_sync.take())
467                .await
468            else {
469                return;
470            };
471            let Setup::Participate(prepared) = prepared else {
472                if self.follow(&mut store).await.is_break() {
473                    return;
474                }
475                current_epoch = store.current().map(|info| info.epoch);
476                continue;
477            };
478            let mut prepared = *prepared;
479
480            let chan = dealing_mux
481                .register(prepared.epoch.get())
482                .await
483                .expect("failed to register reshare epoch channel");
484
485            if prepared.phase == EpochPhase::Early {
486                let dealer = prepared.dealer.as_mut();
487                let player = prepared.player.as_mut();
488                if self
489                    .dealing(prepared.epoch, &mut store, dealer, player, chan)
490                    .await
491                    .is_break()
492                {
493                    return;
494                }
495            }
496
497            if self
498                .inclusion(
499                    prepared.epoch,
500                    &prepared.info,
501                    &mut store,
502                    prepared.dealer.as_mut(),
503                )
504                .await
505                .is_break()
506            {
507                return;
508            }
509            current_epoch = Some(prepared.epoch.next());
510        }
511    }
512}