1use 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
77pub struct Config<M, X, SS, T, D = Unit> {
79 pub signer: ed25519::PrivateKey,
81
82 pub manager: M,
84
85 pub blocker: X,
87
88 pub secret_store: SS,
90
91 pub strategy: T,
93
94 pub namespace: &'static [u8],
96
97 pub sharing_mode: SharingMode,
99
100 pub reveal: Reveal,
102
103 pub max_supported_mode: ModeVersion,
105
106 pub partition_prefix: String,
108
109 pub participants: Set<ed25519::PublicKey>,
111
112 pub directory: D,
118
119 pub blocks_per_epoch: NonZeroU64,
121}
122
123pub struct Completion<V: Variant, D: Directory<ed25519::PublicKey> = Unit> {
125 pub info: Option<EpochInfo<V, ed25519::PublicKey, D>>,
132}
133
134#[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 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
235pub 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 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 #[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 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}