Skip to main content

commonware_glue/dkg/bootstrap/
mod.rs

1//! One-shot engine for generating an initial BLS threshold output.
2//!
3//! The engine runs an independent Ed25519 Simplex chain for one epoch and uses
4//! the reshare actor's crate-private DKG mode to perform the ceremony.
5//! The resulting [`EpochInfo`] describes only the ceremony participants. An
6//! application using it to start continuous resharing must supply the next
7//! players and a transport directory covering the resulting participant union.
8//!
9//! See [`reshare`] for the protocol flow that this engine reuses and for the
10//! application contract of a continuously reshared chain.
11
12use crate::dkg::{
13    ParticipantsProvider, Registrar, ReshareBlock, SecretStore,
14    fence::Fence,
15    network::{Directory, Manager},
16    reshare::{self, DkgConfig},
17    state_sync::Plan as StateSyncPlan,
18    types::{EpochInfo, Participants, Payload, SchemeInfo},
19};
20use commonware_broadcast::buffered;
21use commonware_codec::{Encode, EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
22use commonware_consensus::{
23    Application, Block as ConsensusBlock, CertifiableBlock, Heightable,
24    marshal::{
25        self, Start, ancestry::Ancestry, core::Actor as MarshalActor,
26        resolver::p2p as marshal_resolver, standard::Deferred,
27    },
28    simplex::{
29        self, Floor,
30        config::{ForwardPolicy, SkipBudget, SkipPolicy},
31        elector::RoundRobin,
32        types::Context,
33    },
34    types::{Epoch, FixedEpocher, Height, Round, View, ViewDelta},
35};
36use commonware_cryptography::{
37    BatchVerifier, Digest as _, Digestible, Hasher, PublicKey, Sha256, Signer as _,
38    bls12381::{
39        dkg::feldman_desmedt::Reveal,
40        primitives::{
41            sharing::{Mode as SharingMode, ModeVersion},
42            variant::Variant,
43        },
44    },
45    certificate::{ConstantProvider, Verifier as _},
46    ed25519,
47    sha256::{self, Digest as Sha256Digest},
48};
49use commonware_p2p::{Blocker, Receiver, Sender};
50use commonware_parallel::Strategy;
51use commonware_runtime::{
52    Buf, BufMut, BufferPooler, Clock, ContextCell, Handle, Metrics, Spawner, Storage,
53    buffer::paged::CacheRef, spawn_cell,
54};
55use commonware_storage::{archive::prunable, translator::TwoCap};
56use commonware_utils::{
57    NZU16, NZU32, NZU64, NZUsize,
58    channel::{fallible::OneshotExt, oneshot},
59    ordered::Set,
60    sequence::Unit,
61};
62use rand_core::{CryptoRng, Rng};
63use std::{
64    marker::PhantomData,
65    num::{NonZeroU16, NonZeroU32, NonZeroU64, NonZeroUsize},
66    time::Duration,
67};
68
69const MAILBOX_SIZE: NonZeroUsize = NZUsize!(100);
70const PAGE_SIZE: NonZeroU16 = NZU16!(1024);
71const PAGE_CACHE_PAGES: NonZeroUsize = NZUsize!(16);
72const IO_BUFFER_SIZE: NonZeroUsize = NZUsize!(2048);
73const ARCHIVE_ITEMS_PER_SECTION: NonZeroU64 = NZU64!(10);
74
75type ConsensusScheme = simplex::scheme::ed25519::Scheme;
76
77/// Configuration for [`Engine`].
78pub struct Config<M, X, SS, T, D = Unit> {
79    /// Ed25519 signer used for the one-shot consensus chain and DKG protocol messages.
80    pub signer: ed25519::PrivateKey,
81
82    /// P2P manager used for peer tracking.
83    pub manager: M,
84
85    /// Blocker used for invalid peer behavior.
86    pub blocker: X,
87
88    /// User-owned store for private DKG material.
89    pub secret_store: SS,
90
91    /// Parallel verification strategy.
92    pub strategy: T,
93
94    /// Application namespace for DKG transcript separation.
95    pub namespace: &'static [u8],
96
97    /// Sharing mode used for the generated threshold output.
98    pub sharing_mode: SharingMode,
99
100    /// Revealed-share calculation used for the DKG ceremony.
101    pub reveal: Reveal,
102
103    /// Maximum sharing mode version accepted when decoding blocks.
104    pub max_supported_mode: ModeVersion,
105
106    /// Runtime-storage partition prefix.
107    pub partition_prefix: String,
108
109    /// Participants in the DKG.
110    pub participants: Set<ed25519::PublicKey>,
111
112    /// Transport directory for the participants.
113    ///
114    /// Used to activate the one-shot chain's peer set and embedded verbatim in
115    /// the emitted genesis artifact. Every participant must configure the same
116    /// directory.
117    pub directory: D,
118
119    /// Length of the one-shot consensus epoch.
120    pub blocks_per_epoch: NonZeroU64,
121}
122
123/// Completion produced when the one-shot DKG chain finalizes its final block.
124pub struct Completion<V: Variant, D: Directory<ed25519::PublicKey> = Unit> {
125    /// Final DKG artifact, if the ceremony succeeded.
126    ///
127    /// Its `next_players` set is empty and its directory covers the one-shot
128    /// ceremony participants. Before using it as continuous-resharing genesis,
129    /// the application must choose a nonempty next-player set and ensure the
130    /// directory exactly covers the resulting participant union.
131    pub info: Option<EpochInfo<V, ed25519::PublicKey, D>>,
132}
133
134/// Block type used by the one-shot DKG chain.
135#[derive(Clone, PartialEq, Eq)]
136pub struct Block<V: Variant, D: Directory<ed25519::PublicKey> = Unit> {
137    context: Context<sha256::Digest, ed25519::PublicKey>,
138    parent: sha256::Digest,
139    height: Height,
140    payload: Option<Payload<V, ed25519::PrivateKey, D>>,
141}
142
143impl<V: Variant, D: Directory<ed25519::PublicKey>> Block<V, D> {
144    const fn genesis(leader: ed25519::PublicKey) -> Self {
145        Self {
146            context: Context {
147                round: Round::new(Epoch::zero(), View::zero()),
148                leader,
149                parent: (View::zero(), Sha256Digest::EMPTY),
150            },
151            parent: Sha256Digest::EMPTY,
152            height: Height::zero(),
153            payload: None,
154        }
155    }
156
157    /// Returns the DKG result carried by this block, if present.
158    pub const fn epoch_info(&self) -> Option<&EpochInfo<V, ed25519::PublicKey, D>> {
159        match &self.payload {
160            Some(Payload::EpochInfo(info)) => Some(info),
161            _ => None,
162        }
163    }
164}
165
166impl<V: Variant, D: Directory<ed25519::PublicKey>> Write for Block<V, D> {
167    fn write(&self, buf: &mut impl BufMut) {
168        self.context.write(buf);
169        self.parent.write(buf);
170        self.height.write(buf);
171        self.payload.write(buf);
172    }
173}
174
175impl<V: Variant, D: Directory<ed25519::PublicKey>> EncodeSize for Block<V, D> {
176    fn encode_size(&self) -> usize {
177        self.context.encode_size()
178            + self.parent.encode_size()
179            + self.height.encode_size()
180            + self.payload.encode_size()
181    }
182}
183
184impl<V: Variant, D: Directory<ed25519::PublicKey>> Read for Block<V, D> {
185    type Cfg = (NonZeroU32, ModeVersion);
186
187    fn read_cfg(buf: &mut impl Buf, cfg: &Self::Cfg) -> Result<Self, CodecError> {
188        Ok(Self {
189            context: Context::read(buf)?,
190            parent: sha256::Digest::read(buf)?,
191            height: Height::read(buf)?,
192            payload: Option::<Payload<V, ed25519::PrivateKey, D>>::read_cfg(buf, cfg)?,
193        })
194    }
195}
196
197impl<V: Variant, D: Directory<ed25519::PublicKey>> Digestible for Block<V, D> {
198    type Digest = sha256::Digest;
199
200    fn digest(&self) -> sha256::Digest {
201        Sha256::hash(&[&self.encode()])
202    }
203}
204
205impl<V: Variant, D: Directory<ed25519::PublicKey>> Heightable for Block<V, D> {
206    fn height(&self) -> Height {
207        self.height
208    }
209}
210
211impl<V: Variant, D: Directory<ed25519::PublicKey>> ConsensusBlock for Block<V, D> {
212    fn parent(&self) -> sha256::Digest {
213        self.parent
214    }
215}
216
217impl<V: Variant, D: Directory<ed25519::PublicKey>> CertifiableBlock for Block<V, D> {
218    type Context = Context<sha256::Digest, ed25519::PublicKey>;
219
220    fn context(&self) -> Self::Context {
221        self.context.clone()
222    }
223}
224
225impl<V: Variant, D: Directory<ed25519::PublicKey>> ReshareBlock for Block<V, D> {
226    type Variant = V;
227    type Signer = ed25519::PrivateKey;
228    type Directory = D;
229
230    fn payload(&self) -> Option<Payload<Self::Variant, Self::Signer, Self::Directory>> {
231        self.payload.clone()
232    }
233}
234
235/// Self-contained DKG engine.
236pub struct Engine<E, V, M, X, SS, T, D = Unit>
237where
238    V: Variant,
239{
240    context: ContextCell<E>,
241    config: Config<M, X, SS, T, D>,
242    _variant: PhantomData<V>,
243}
244
245impl<E, V, M, X, SS, T, D> Engine<E, V, M, X, SS, T, D>
246where
247    V: Variant,
248{
249    /// Creates a new engine.
250    pub const fn new(context: E, config: Config<M, X, SS, T, D>) -> Self {
251        assert!(
252            config.max_supported_mode.supports(&config.sharing_mode),
253            "sharing mode must be supported by max supported mode",
254        );
255        Self {
256            context: ContextCell::new(context),
257            config,
258            _variant: PhantomData,
259        }
260    }
261}
262
263impl<E, V, M, X, SS, T, D> Engine<E, V, M, X, SS, T, D>
264where
265    E: CryptoRng + Spawner + Metrics + Clock + Storage + BufferPooler,
266    V: Variant,
267    M: Manager<PublicKey = ed25519::PublicKey, Directory = D> + Clone,
268    X: Blocker<PublicKey = ed25519::PublicKey> + Clone,
269    SS: SecretStore,
270    T: Strategy + Clone,
271    D: Directory<ed25519::PublicKey>,
272    ed25519::Batch: BatchVerifier<PublicKey = ed25519::PublicKey> + Send + 'static,
273{
274    /// Starts consensus, marshal, broadcast, and the private reshare DKG actor.
275    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
276    pub fn start(
277        mut self,
278        votes: (
279            impl Sender<PublicKey = ed25519::PublicKey>,
280            impl Receiver<PublicKey = ed25519::PublicKey>,
281        ),
282        certificates: (
283            impl Sender<PublicKey = ed25519::PublicKey>,
284            impl Receiver<PublicKey = ed25519::PublicKey>,
285        ),
286        resolver: (
287            impl Sender<PublicKey = ed25519::PublicKey>,
288            impl Receiver<PublicKey = ed25519::PublicKey>,
289        ),
290        backfill: (
291            impl Sender<PublicKey = ed25519::PublicKey>,
292            impl Receiver<PublicKey = ed25519::PublicKey>,
293        ),
294        broadcast: (
295            impl Sender<PublicKey = ed25519::PublicKey>,
296            impl Receiver<PublicKey = ed25519::PublicKey>,
297        ),
298        dkg: (
299            impl Sender<PublicKey = ed25519::PublicKey>,
300            impl Receiver<PublicKey = ed25519::PublicKey>,
301        ),
302    ) -> (Handle<()>, oneshot::Receiver<Completion<V, D>>) {
303        let (completion_tx, completion_rx) = oneshot::channel();
304        let handle = spawn_cell!(
305            self.context,
306            self.run(
307                votes,
308                certificates,
309                resolver,
310                backfill,
311                broadcast,
312                dkg,
313                completion_tx
314            )
315        );
316        (handle, completion_rx)
317    }
318
319    #[allow(clippy::too_many_arguments)]
320    async fn run(
321        self,
322        votes: (
323            impl Sender<PublicKey = ed25519::PublicKey>,
324            impl Receiver<PublicKey = ed25519::PublicKey>,
325        ),
326        certificates: (
327            impl Sender<PublicKey = ed25519::PublicKey>,
328            impl Receiver<PublicKey = ed25519::PublicKey>,
329        ),
330        resolver_network: (
331            impl Sender<PublicKey = ed25519::PublicKey>,
332            impl Receiver<PublicKey = ed25519::PublicKey>,
333        ),
334        backfill: (
335            impl Sender<PublicKey = ed25519::PublicKey>,
336            impl Receiver<PublicKey = ed25519::PublicKey>,
337        ),
338        broadcast: (
339            impl Sender<PublicKey = ed25519::PublicKey>,
340            impl Receiver<PublicKey = ed25519::PublicKey>,
341        ),
342        dkg: (
343            impl Sender<PublicKey = ed25519::PublicKey>,
344            impl Receiver<PublicKey = ed25519::PublicKey>,
345        ),
346        completion: oneshot::Sender<Completion<V, D>>,
347    ) {
348        assert!(
349            !self.config.participants.is_empty(),
350            "DKG requires at least one participant"
351        );
352        Participants {
353            dealers: self.config.participants.clone(),
354            players: self.config.participants.clone(),
355            next_players: Set::default(),
356        }
357        .validate_epoch_capacity::<V>(self.config.blocks_per_epoch, None)
358        .expect("DKG epoch must have enough dealer-log slots");
359        let participants = self
360            .config
361            .participants
362            .len()
363            .try_into()
364            .expect("too many DKG participants");
365        let max_participants = NZU32!(participants);
366        let block_codec_config = (max_participants, self.config.max_supported_mode);
367
368        let context = self.context.into_present();
369        let page_cache = CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_PAGES);
370        let public_key = self.config.signer.public_key();
371        let consensus_namespace = [self.config.namespace, b"_INITIAL_CONSENSUS"].concat();
372        let scheme = ConsensusScheme::signer(
373            &consensus_namespace,
374            self.config.participants.clone(),
375            self.config.signer.clone(),
376        )
377        .expect("DKG signer must be a participant");
378        let provider = ConstantProvider::<_, Epoch>::new(scheme.clone());
379        let genesis = Block::<V, D>::genesis(
380            self.config
381                .participants
382                .iter()
383                .next()
384                .expect("participants must be non-empty")
385                .clone(),
386        );
387
388        let (buffer, buffer_mailbox) = buffered::Engine::new(
389            context.child("buffer"),
390            buffered::Config {
391                public_key: public_key.clone(),
392                mailbox_size: MAILBOX_SIZE,
393                deque_size: 16,
394                priority: false,
395                codec_config: block_codec_config,
396                peer_provider: self.config.manager.clone(),
397            },
398        );
399        let buffer_handle = buffer.start(broadcast);
400
401        let (backfill_handler, backfill_resolver) = marshal_resolver::init(
402            context.child("backfill"),
403            marshal_resolver::Config {
404                public_key: public_key.clone(),
405                peer_provider: self.config.manager.clone(),
406                blocker: self.config.blocker.clone(),
407                mailbox_size: MAILBOX_SIZE,
408                timeout: Duration::from_secs(2),
409                fetch_retry_timeout: Duration::from_millis(100),
410                priority_requests: false,
411                priority_responses: false,
412            },
413            backfill,
414        );
415
416        let finalizations = prunable::Archive::init(
417            context.child("finalizations"),
418            archive_config(
419                &self.config.partition_prefix,
420                "finalizations",
421                page_cache.clone(),
422                ConsensusScheme::certificate_codec_config_unbounded(),
423            ),
424        )
425        .await
426        .expect("failed to initialize DKG finalization archive");
427        let blocks = prunable::Archive::init(
428            context.child("blocks"),
429            archive_config(
430                &self.config.partition_prefix,
431                "blocks",
432                page_cache.clone(),
433                block_codec_config,
434            ),
435        )
436        .await
437        .expect("failed to initialize DKG block archive");
438
439        let (marshal_actor, marshal_mailbox, _) = MarshalActor::init(
440            context.child("marshal"),
441            finalizations,
442            blocks,
443            marshal::Config {
444                provider: provider.clone(),
445                epocher: FixedEpocher::new(self.config.blocks_per_epoch),
446                start: Start::Genesis(genesis.clone()),
447                partition_prefix: format!("{}-marshal", self.config.partition_prefix),
448                mailbox_size: MAILBOX_SIZE,
449                view_retention: ViewDelta::new(10),
450                prunable_items_per_section: ARCHIVE_ITEMS_PER_SECTION,
451                page_cache: page_cache.clone(),
452                replay_buffer: IO_BUFFER_SIZE,
453                key_write_buffer: IO_BUFFER_SIZE,
454                value_write_buffer: IO_BUFFER_SIZE,
455                block_codec_config,
456                max_repair: NZUsize!(10),
457                max_pending_acks: NZUsize!(1),
458                strategy: self.config.strategy.clone(),
459            },
460        )
461        .await;
462
463        let (fence, _gate) = Fence::new(Epoch::zero());
464        let (reshare_actor, reshare_mailbox) = reshare::Actor::new_dkg(
465            context.child("reshare"),
466            reshare::Config {
467                signer: self.config.signer.clone(),
468                manager: self.config.manager.clone(),
469                blocker: self.config.blocker.clone(),
470                participants_provider: StaticParticipants {
471                    participants: self.config.participants.clone(),
472                    directory: self.config.directory.clone(),
473                },
474                secret_store: self.config.secret_store,
475                strategy: self.config.strategy.clone(),
476                registrar: NoopRegistrar(PhantomData),
477                marshal: marshal_mailbox.clone(),
478                state_sync: StateSyncPlan::disabled(),
479                fence,
480                namespace: self.config.namespace,
481                sharing_mode: self.config.sharing_mode,
482                reveal: self.config.reveal,
483                mailbox_size: MAILBOX_SIZE,
484                partition_prefix: format!("{}-reshare", self.config.partition_prefix),
485                max_participants,
486                blocks_per_epoch: self.config.blocks_per_epoch,
487                batch_verifier: PhantomData::<ed25519::Batch>,
488            },
489            DkgConfig {
490                participants: self.config.participants.clone(),
491                directory: self.config.directory.clone(),
492                completion: Box::new(move |info| {
493                    let _ = completion.send_lossy(Completion { info });
494                }),
495            },
496        );
497
498        let app = reshare::Application::new(
499            DkgApp(PhantomData),
500            reshare_mailbox.clone(),
501            self.config.blocks_per_epoch,
502        );
503        let deferred = Deferred::new(
504            context.child("deferred"),
505            app,
506            marshal_mailbox.clone(),
507            FixedEpocher::new(self.config.blocks_per_epoch),
508        );
509        let simplex = simplex::Engine::new(
510            context.child("simplex"),
511            simplex::Config {
512                scheme,
513                elector: RoundRobin::<Sha256>::default(),
514                blocker: self.config.blocker,
515                automaton: deferred.clone(),
516                relay: deferred,
517                reporter: marshal_mailbox.clone(),
518                strategy: self.config.strategy,
519                partition: format!("{}-simplex", self.config.partition_prefix),
520                mailbox_size: MAILBOX_SIZE,
521                epoch: Epoch::zero(),
522                floor: Floor::Genesis(genesis.digest()),
523                replay_buffer: IO_BUFFER_SIZE,
524                write_buffer: IO_BUFFER_SIZE,
525                page_cache,
526                leader_timeout: Duration::from_secs(1),
527                certification_timeout: Duration::from_secs(2),
528                timeout_retry: Duration::from_millis(500),
529                view_retention: ViewDelta::new(10),
530                skip: SkipPolicy::Enabled {
531                    timeout: Duration::from_secs(5),
532                    budget: SkipBudget::Participants,
533                },
534                fetch_timeout: Duration::from_secs(2),
535                forward: ForwardPolicy::Disabled,
536                track_historical_votes: false,
537            },
538        );
539
540        let reshare_handle = reshare_actor.start(dkg);
541        let marshal_handle = marshal_actor.start(
542            reshare_mailbox,
543            buffer_mailbox,
544            (backfill_handler, backfill_resolver),
545        );
546        let simplex_handle = simplex.start(votes, certificates, resolver_network);
547
548        Handle::select([
549            buffer_handle,
550            reshare_handle,
551            marshal_handle,
552            simplex_handle,
553        ])
554        .await
555        .expect("failed dkg");
556    }
557}
558
559#[derive(Clone)]
560struct DkgApp<V: Variant, D>(PhantomData<(V, D)>);
561
562impl<E, V, D> Application<E> for DkgApp<V, D>
563where
564    E: Rng + Spawner + Metrics + Clock,
565    V: Variant,
566    D: Directory<ed25519::PublicKey>,
567{
568    type SigningScheme = ConsensusScheme;
569    type Context = Context<sha256::Digest, ed25519::PublicKey>;
570    type Block = Block<V, D>;
571    type Input = reshare::Input<(), V, ed25519::PrivateKey, D>;
572
573    async fn propose(
574        &mut self,
575        (_, context): (E, Self::Context),
576        ancestry: impl Ancestry<Self::Block>,
577        input: Self::Input,
578    ) -> Option<Self::Block> {
579        let parent = ancestry.peek()?.clone();
580        let height = parent.height().next();
581        Some(Block {
582            context,
583            parent: parent.digest(),
584            height,
585            payload: input.payload,
586        })
587    }
588
589    async fn verify(
590        &mut self,
591        _: (E, Self::Context),
592        _ancestry: impl Ancestry<Self::Block>,
593    ) -> bool {
594        // The reshare application wrapper validates payload placement and the
595        // final block's epoch info before delegating to this stateless leaf.
596        true
597    }
598}
599
600#[derive(Clone)]
601struct StaticParticipants<P, D> {
602    participants: Set<P>,
603    directory: D,
604}
605
606impl<P, D> ParticipantsProvider for StaticParticipants<P, D>
607where
608    P: PublicKey,
609    D: Directory<P>,
610{
611    type PublicKey = P;
612    type Directory = D;
613
614    async fn participants(&mut self, _: Epoch) -> Set<Self::PublicKey> {
615        self.participants.clone()
616    }
617
618    async fn directory(&mut self, _: Epoch, _: Set<Self::PublicKey>) -> Self::Directory {
619        self.directory.clone()
620    }
621}
622
623#[derive(Clone)]
624struct NoopRegistrar<V, P>(PhantomData<(V, P)>);
625
626impl<V, P> Registrar for NoopRegistrar<V, P>
627where
628    V: Variant,
629    P: PublicKey,
630{
631    type Variant = V;
632    type PublicKey = P;
633
634    async fn register(&self, _: Epoch, _: SchemeInfo<Self::Variant, Self::PublicKey>) {}
635}
636
637fn archive_config<C>(
638    prefix: &str,
639    name: &str,
640    page_cache: CacheRef,
641    codec_config: C,
642) -> prunable::Config<TwoCap, C> {
643    prunable::Config {
644        translator: TwoCap,
645        metadata_partition: format!("{prefix}-{name}-metadata"),
646        key_partition: format!("{prefix}-{name}-key"),
647        key_page_cache: page_cache,
648        value_partition: format!("{prefix}-{name}-value"),
649        compression: None,
650        codec_config,
651        items_per_section: ARCHIVE_ITEMS_PER_SECTION,
652        key_write_buffer: IO_BUFFER_SIZE,
653        value_write_buffer: IO_BUFFER_SIZE,
654        replay_buffer: IO_BUFFER_SIZE,
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use super::*;
661    use commonware_cryptography::bls12381::primitives::variant::MinPk;
662
663    #[test]
664    #[should_panic(expected = "sharing mode must be supported by max supported mode")]
665    fn rejects_unsupported_sharing_mode() {
666        let config = Config {
667            signer: ed25519::PrivateKey::from_seed(0),
668            manager: (),
669            blocker: (),
670            secret_store: (),
671            strategy: (),
672            namespace: b"test",
673            sharing_mode: SharingMode::RootsOfUnity,
674            reveal: Reveal::V1,
675            max_supported_mode: ModeVersion::v0(),
676            partition_prefix: "test".into(),
677            participants: Set::default(),
678            directory: Unit,
679            blocks_per_epoch: NZU64!(1),
680        };
681
682        let _ = Engine::<_, MinPk, _, _, _, _, _>::new((), config);
683    }
684}