Skip to main content

sui_core/epoch/
randomness.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use anemo::PeerId;
5use fastcrypto::encoding::{Encoding, Hex};
6use fastcrypto::error::{FastCryptoError, FastCryptoResult};
7use fastcrypto::groups::bls12381;
8use fastcrypto::serde_helpers::ToFromByteArray;
9use fastcrypto::traits::{KeyPair, ToFromBytes};
10use fastcrypto_tbls::{dkg_v1, dkg_v1::Output, nodes, nodes::PartyId};
11use futures::StreamExt;
12use futures::stream::FuturesUnordered;
13use mysten_common::debug_fatal;
14use parking_lot::Mutex;
15use rand::SeedableRng;
16use rand::rngs::{OsRng, StdRng};
17use serde::{Deserialize, Serialize};
18use std::collections::{BTreeMap, HashMap};
19use std::sync::{Arc, Weak};
20use std::time::Instant;
21use sui_macros::fail_point_if;
22use sui_network::randomness;
23use sui_types::base_types::AuthorityName;
24use sui_types::committee::{Committee, EpochId, StakeUnit};
25use sui_types::crypto::{AuthorityKeyPair, RandomnessRound};
26use sui_types::error::{SuiErrorKind, SuiResult};
27use sui_types::messages_consensus::{
28    ConsensusTransaction, Round, TimestampMs, VersionedDkgConfirmation, VersionedDkgMessage,
29};
30use sui_types::sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait;
31use tokio::sync::OnceCell;
32use tokio::task::JoinHandle;
33use tracing::{debug, error, info, warn};
34use typed_store::Map;
35
36use crate::authority::authority_per_epoch_store::{
37    AuthorityPerEpochStore, consensus_quarantine::ConsensusCommitOutput,
38};
39use crate::authority::epoch_start_configuration::EpochStartConfigTrait;
40use crate::consensus_adapter::SubmitToConsensus;
41use crate::randomness_round_receiver::RandomnessRoundReceiverHandle;
42
43type PkG = bls12381::G2Element;
44type EncG = bls12381::G2Element;
45
46pub const SINGLETON_KEY: u64 = 0;
47
48// Wrappers for DKG messages (to simplify upgrades).
49
50#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51#[allow(clippy::large_enum_variant)]
52pub enum VersionedProcessedMessage {
53    V0(), // deprecated
54    V1(dkg_v1::ProcessedMessage<PkG, EncG>),
55}
56
57impl VersionedProcessedMessage {
58    pub fn sender(&self) -> PartyId {
59        match self {
60            VersionedProcessedMessage::V0() => {
61                panic!("BUG: invalid VersionedProcessedMessage version V0")
62            }
63            VersionedProcessedMessage::V1(msg) => msg.message.sender,
64        }
65    }
66
67    pub fn unwrap_v1(self) -> dkg_v1::ProcessedMessage<PkG, EncG> {
68        if let VersionedProcessedMessage::V1(msg) = self {
69            msg
70        } else {
71            panic!("BUG: expected message version is 1")
72        }
73    }
74
75    pub fn as_v1(&self) -> Option<&dkg_v1::ProcessedMessage<PkG, EncG>> {
76        if let VersionedProcessedMessage::V1(msg) = self {
77            Some(msg)
78        } else {
79            None
80        }
81    }
82}
83
84#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
85pub enum VersionedUsedProcessedMessages {
86    V0(), // deprecated
87    V1(dkg_v1::UsedProcessedMessages<PkG, EncG>),
88}
89
90impl VersionedUsedProcessedMessages {
91    pub fn as_v1(&self) -> Option<&dkg_v1::UsedProcessedMessages<PkG, EncG>> {
92        if let VersionedUsedProcessedMessages::V1(msg) = self {
93            Some(msg)
94        } else {
95            None
96        }
97    }
98}
99
100/// Distinguishes between an active DKG participant (validator) and a read-only observer (fullnode).
101enum DkgRole {
102    Party(dkg_v1::Party<PkG, EncG>),
103    Observer(dkg_v1::Observer<PkG, EncG>),
104}
105
106impl DkgRole {
107    /// Creates a new DkgRole. When `authority_key_pair` is Some, creates an active Party
108    /// (validator). When None, creates a read-only Observer (fullnode).
109    ///
110    /// * `authority_key_pair` - The validator's BLS key pair used to derive the DKG private key.
111    ///   When None, an Observer is created that can follow DKG but not produce shares.
112    /// * `nodes` - The reduced set of DKG participants with their public keys and weights.
113    /// * `t` - The threshold number of shares required to reconstruct randomness.
114    /// * `random_oracle` - Epoch-specific oracle used to derive deterministic challenges during DKG.
115    fn try_new(
116        authority_key_pair: Option<&AuthorityKeyPair>,
117        nodes: nodes::Nodes<EncG>,
118        t: u16,
119        random_oracle: fastcrypto_tbls::random_oracle::RandomOracle,
120    ) -> Option<Self> {
121        let total_weight = nodes.total_weight();
122        let num_nodes = nodes.num_nodes();
123
124        if let Some(authority_key_pair) = authority_key_pair {
125            let randomness_private_key = bls12381::Scalar::from_byte_array(
126                authority_key_pair
127                    .copy()
128                    .private()
129                    .as_bytes()
130                    .try_into()
131                    .expect("key length should match"),
132            )
133            .expect("should work to convert BLS key to Scalar");
134            let party = match dkg_v1::Party::<PkG, EncG>::new(
135                fastcrypto_tbls::ecies_v1::PrivateKey::<bls12381::G2Element>::from(
136                    randomness_private_key,
137                ),
138                nodes,
139                t,
140                random_oracle,
141                &mut rand::thread_rng(),
142            ) {
143                Ok(party) => party,
144                Err(err) => {
145                    debug_fatal!("random beacon: error while initializing Party: {err:?}");
146                    return None;
147                }
148            };
149            let name: AuthorityName = authority_key_pair.public().into();
150            info!(
151                "random beacon: Party initialized with authority={name}, total_weight={total_weight}, t={t}, num_nodes={num_nodes}",
152            );
153            Some(DkgRole::Party(party))
154        } else {
155            let observer = match dkg_v1::Observer::<PkG, EncG>::new(nodes, t, random_oracle) {
156                Ok(observer) => observer,
157                Err(err) => {
158                    debug_fatal!("random beacon: error while initializing Observer: {err:?}");
159                    return None;
160                }
161            };
162            info!(
163                "random beacon: Observer initialized with total_weight={total_weight}, t={t}, num_nodes={num_nodes}",
164            );
165            Some(DkgRole::Observer(observer))
166        }
167    }
168
169    fn is_party(&self) -> bool {
170        matches!(self, DkgRole::Party(_))
171    }
172
173    fn is_observer(&self) -> bool {
174        matches!(self, DkgRole::Observer(_))
175    }
176
177    /// Processes a received DKG message according to the role.
178    fn process_message(
179        &self,
180        message: VersionedDkgMessage,
181    ) -> FastCryptoResult<VersionedProcessedMessage> {
182        match self {
183            DkgRole::Party(party) => {
184                let processed =
185                    party.process_message(message.unwrap_v1(), &mut rand::thread_rng())?;
186                Ok(VersionedProcessedMessage::V1(processed))
187            }
188            DkgRole::Observer(observer) => {
189                let raw_msg = message.unwrap_v1();
190                observer.process_message(raw_msg.clone())?;
191                Ok(VersionedProcessedMessage::V1(dkg_v1::ProcessedMessage {
192                    message: raw_msg,
193                    shares: vec![],
194                    complaint: None,
195                }))
196            }
197        }
198    }
199
200    /// Merges processed DKG messages. For Party, produces a confirmation and used messages.
201    /// For Observer, produces only used messages, confirmation is None, as observer nodes do
202    /// not have any voting rights.
203    fn merge_messages(
204        &self,
205        messages: Vec<VersionedProcessedMessage>,
206    ) -> FastCryptoResult<(
207        Option<VersionedDkgConfirmation>,
208        VersionedUsedProcessedMessages,
209    )> {
210        match self {
211            DkgRole::Party(party) => {
212                let (conf, msgs) = party.merge(
213                    &messages
214                        .into_iter()
215                        .map(|vm| vm.unwrap_v1())
216                        .collect::<Vec<_>>(),
217                )?;
218                Ok((
219                    Some(VersionedDkgConfirmation::V1(conf)),
220                    VersionedUsedProcessedMessages::V1(msgs),
221                ))
222            }
223            DkgRole::Observer(observer) => {
224                let raw_messages: Vec<_> = messages
225                    .into_iter()
226                    .map(|pm| pm.unwrap_v1().message)
227                    .collect();
228                let used = observer.merge(raw_messages)?;
229                Ok((
230                    None,
231                    VersionedUsedProcessedMessages::V1(dkg_v1::UsedProcessedMessages(
232                        used.into_iter()
233                            .map(|m| dkg_v1::ProcessedMessage {
234                                message: m,
235                                shares: vec![],
236                                complaint: None,
237                            })
238                            .collect(),
239                    )),
240                ))
241            }
242        }
243    }
244
245    /// Completes DKG from used messages and confirmations. The output contains the shared public key which can be used
246    /// from there after to validate the randomness round signatures. For the observer case the output will contain the public
247    /// key but no shares, as again the node does not participate in the voting process.
248    fn complete_dkg<'a>(
249        &self,
250        used_messages: &VersionedUsedProcessedMessages,
251        confirmations: impl Iterator<Item = &'a VersionedDkgConfirmation>,
252    ) -> FastCryptoResult<Output<PkG, EncG>> {
253        match self {
254            DkgRole::Party(party) => {
255                let rng = &mut StdRng::from_rng(OsRng).expect("RNG construction should not fail");
256                let msg = used_messages
257                    .as_v1()
258                    .expect("expected V1 used processed messages");
259                party.complete(
260                    msg,
261                    &confirmations
262                        .map(|vm| vm.as_v1().expect("expected V1 confirmation"))
263                        .cloned()
264                        .collect::<Vec<_>>(),
265                    rng,
266                )
267            }
268            DkgRole::Observer(observer) => {
269                let raw_messages: Vec<_> = used_messages
270                    .as_v1()
271                    .expect("expected V1 used processed messages")
272                    .0
273                    .iter()
274                    .map(|pm| pm.message.clone())
275                    .collect();
276                let confirmations: Vec<_> = confirmations
277                    .map(|c| c.as_v1().expect("expected V1 confirmation").clone())
278                    .collect();
279                observer.complete(&raw_messages, &confirmations)
280            }
281        }
282    }
283}
284
285// State machine for randomness DKG and generation.
286//
287// DKG protocol:
288// 1. This validator sends out a `Message` to all other validators.
289// 2. Once sufficient valid `Message`s are received from other validators via consensus and
290//    processed, this validator sends out a `Confirmation` to all other validators.
291// 3. Once sufficient `Confirmation`s are received from other validators via consensus and
292//    processed, they are combined to form a public VSS key and local private key shares.
293// 4. Randomness generation begins.
294//
295// Randomness generation:
296// 1. For each new round, AuthorityPerEpochStore eventually calls `generate_randomness`.
297// 2. This kicks off a process in RandomnessEventLoop to send partial signatures for the new
298//    round to all other validators.
299// 3. Once enough partial signautres for the round are collected, a RandomnessStateUpdate
300//    transaction is generated and injected into the ExecutionScheduler.
301// 4. Once the RandomnessStateUpdate transaction is seen in a certified checkpoint,
302//    `notify_randomness_in_checkpoint` is called to complete the round and stop sending
303//    partial signatures for it.
304pub struct RandomnessManager {
305    epoch_store: Weak<AuthorityPerEpochStore>,
306    epoch: EpochId,
307    consensus_adapter: Box<dyn SubmitToConsensus>,
308    network_handle: randomness::Handle,
309    authority_info: HashMap<AuthorityName, (PeerId, PartyId)>,
310
311    // State for DKG.
312    dkg_start_time: OnceCell<Instant>,
313    role: Arc<DkgRole>,
314    enqueued_messages: BTreeMap<PartyId, JoinHandle<Option<VersionedProcessedMessage>>>,
315    processed_messages: BTreeMap<PartyId, VersionedProcessedMessage>,
316    used_messages: OnceCell<VersionedUsedProcessedMessages>,
317    confirmations: BTreeMap<PartyId, VersionedDkgConfirmation>,
318    dkg_output: OnceCell<Option<dkg_v1::Output<PkG, EncG>>>,
319
320    // State for randomness generation.
321    next_randomness_round: RandomnessRound,
322    highest_completed_round: Arc<Mutex<Option<RandomnessRound>>>,
323
324    randomness_receiver_handle: Arc<RandomnessRoundReceiverHandle>,
325}
326
327impl RandomnessManager {
328    // Returns None in case of invalid input or other failure to initialize DKG.
329    pub async fn try_new(
330        epoch_store_weak: Weak<AuthorityPerEpochStore>,
331        consensus_adapter: Box<dyn SubmitToConsensus>,
332        network_handle: randomness::Handle,
333        authority_key_pair: Option<&AuthorityKeyPair>,
334        randomness_receiver_handle: Arc<RandomnessRoundReceiverHandle>,
335    ) -> Option<Self> {
336        let epoch_store = match epoch_store_weak.upgrade() {
337            Some(epoch_store) => epoch_store,
338            None => {
339                error!(
340                    "could not construct RandomnessManager: AuthorityPerEpochStore already gone"
341                );
342                return None;
343            }
344        };
345        let tables = match epoch_store.tables() {
346            Ok(tables) => tables,
347            Err(_) => {
348                error!(
349                    "could not construct RandomnessManager: AuthorityPerEpochStore tables already gone"
350                );
351                return None;
352            }
353        };
354        let protocol_config = epoch_store.protocol_config();
355        epoch_store.metrics.epoch_random_beacon_dkg_failed.set(0);
356        epoch_store
357            .metrics
358            .epoch_random_beacon_dkg_num_shares
359            .set(0);
360
361        let committee = epoch_store.committee();
362        let info = RandomnessManager::randomness_dkg_info_from_committee(committee);
363        if tracing::enabled!(tracing::Level::DEBUG) {
364            // Log first few entries in DKG info for debugging.
365            for (id, name, pk, stake) in info.iter().filter(|(id, _, _, _)| *id < 3) {
366                let pk_bytes = pk.as_element().to_byte_array();
367                debug!(
368                    "random beacon: DKG info: id={id}, stake={stake}, name={name}, pk={pk_bytes:x?}"
369                );
370            }
371        }
372        let authority_ids: HashMap<_, _> =
373            info.iter().map(|(id, name, _, _)| (*name, *id)).collect();
374        let authority_peer_ids = epoch_store
375            .epoch_start_config()
376            .epoch_start_state()
377            .get_authority_names_to_peer_ids();
378        let authority_info = authority_ids
379            .into_iter()
380            .map(|(name, id)| {
381                let peer_id = *authority_peer_ids
382                    .get(&name)
383                    .expect("authority name should be in peer_ids");
384                (name, (peer_id, id))
385            })
386            .collect();
387        let nodes = info
388            .iter()
389            .map(|(id, _, pk, stake)| nodes::Node::<EncG> {
390                id: *id,
391                pk: pk.clone(),
392                weight: (*stake).try_into().expect("stake should fit in u16"),
393            })
394            .collect();
395        let (nodes, t) = match nodes::Nodes::new_reduced(
396            nodes,
397            committee
398                .validity_threshold()
399                .try_into()
400                .expect("validity threshold should fit in u16"),
401            protocol_config.random_beacon_reduction_allowed_delta(),
402            protocol_config
403                .random_beacon_reduction_lower_bound()
404                .try_into()
405                .expect("should fit u16"),
406        ) {
407            Ok((nodes, t)) => (nodes, t),
408            Err(err) => {
409                error!("random beacon: error while initializing Nodes: {err:?}");
410                return None;
411            }
412        };
413        let random_oracle = fastcrypto_tbls::random_oracle::RandomOracle::new(&format!(
414            "dkg {} {}",
415            Hex::encode(epoch_store.get_chain_identifier().as_bytes()),
416            committee.epoch()
417        ));
418
419        let role = Arc::new(DkgRole::try_new(
420            authority_key_pair,
421            nodes,
422            t,
423            random_oracle,
424        )?);
425
426        // Load existing data from store.
427        let highest_completed_round = tables
428            .randomness_highest_completed_round
429            .get(&SINGLETON_KEY)
430            .expect("typed_store should not fail");
431        let mut rm = RandomnessManager {
432            epoch_store: epoch_store_weak,
433            epoch: committee.epoch(),
434            consensus_adapter,
435            network_handle: network_handle.clone(),
436            authority_info,
437            dkg_start_time: OnceCell::new(),
438            role,
439            enqueued_messages: BTreeMap::new(),
440            processed_messages: BTreeMap::new(),
441            used_messages: OnceCell::new(),
442            confirmations: BTreeMap::new(),
443            dkg_output: OnceCell::new(),
444            next_randomness_round: RandomnessRound(0),
445            highest_completed_round: Arc::new(Mutex::new(highest_completed_round)),
446            randomness_receiver_handle,
447        };
448        let dkg_output = tables
449            .dkg_output_v2
450            .get(&SINGLETON_KEY)
451            .expect("typed_store should not fail");
452        match dkg_output {
453            Some(Some(dkg_output)) => {
454                info!(
455                    "random beacon: loaded existing DKG output for epoch {}",
456                    committee.epoch()
457                );
458                epoch_store
459                    .metrics
460                    .epoch_random_beacon_dkg_num_shares
461                    .set(dkg_output.shares.as_ref().map_or(0, |shares| shares.len()) as i64);
462                rm.dkg_output
463                    .set(Some(dkg_output.clone()))
464                    .expect("setting new OnceCell should succeed");
465                // Update the randomness round receiver with the public key, so it can now
466                // verify randomness round signatures received out of consensus.
467                rm.randomness_receiver_handle
468                    .set_public_key(dkg_output.vss_pk.c0());
469
470                if let DkgRole::Party(party) = rm.role.as_ref() {
471                    network_handle.update_epoch(
472                        committee.epoch(),
473                        rm.authority_info.clone(),
474                        dkg_output,
475                        party.t(),
476                        highest_completed_round,
477                    );
478                }
479            }
480            Some(None) => {
481                // DKG previously completed as a failure (recorded only in `dkg_output_v2`).
482                // Restore that terminal state so DKG isn't re-run and randomness stays disabled
483                // for the epoch.
484                error!(
485                    "random beacon: loaded failed DKG for epoch {}. Randomness disabled for this epoch. All randomness-using transactions will fail.",
486                    committee.epoch()
487                );
488                epoch_store.metrics.epoch_random_beacon_dkg_failed.set(1);
489                rm.dkg_output
490                    .set(None)
491                    .expect("setting new OnceCell should succeed");
492            }
493            None => {
494                info!(
495                    "random beacon: no existing DKG output found for epoch {}",
496                    committee.epoch()
497                );
498
499                // Load intermediate data.
500                assert!(
501                    epoch_store.protocol_config().dkg_version() > 0,
502                    "BUG: DKG version 0 is deprecated"
503                );
504                rm.processed_messages.extend(
505                    tables
506                        .dkg_processed_messages_v2
507                        .safe_iter()
508                        .map(|result| result.expect("typed_store should not fail")),
509                );
510                if let Some(used_messages) = tables
511                    .dkg_used_messages_v2
512                    .get(&SINGLETON_KEY)
513                    .expect("typed_store should not fail")
514                {
515                    rm.used_messages
516                        .set(used_messages.clone())
517                        .expect("setting new OnceCell should succeed");
518                }
519                rm.confirmations.extend(
520                    tables
521                        .dkg_confirmations_v2
522                        .safe_iter()
523                        .map(|result| result.expect("typed_store should not fail")),
524                );
525            }
526        }
527
528        // Resume randomness generation from where we left off.
529        // This must be loaded regardless of whether DKG has finished yet, since the
530        // RandomnessEventLoop and commit-handling logic in AuthorityPerEpochStore both depend on
531        // this state.
532        rm.next_randomness_round = tables
533            .randomness_next_round
534            .get(&SINGLETON_KEY)
535            .expect("typed_store should not fail")
536            .unwrap_or(RandomnessRound(0));
537        info!(
538            "random beacon: starting from next_randomness_round={}",
539            rm.next_randomness_round.0
540        );
541
542        // Re-send partial signatures for incomplete rounds (validators only).
543        if rm.role.is_party() {
544            let first_incomplete_round = highest_completed_round
545                .map(|r| r + 1)
546                .unwrap_or(RandomnessRound(0));
547            if first_incomplete_round < rm.next_randomness_round {
548                info!(
549                    "random beacon: resuming generation for randomness rounds from {} to {}",
550                    first_incomplete_round,
551                    rm.next_randomness_round - 1,
552                );
553                for r in first_incomplete_round.0..rm.next_randomness_round.0 {
554                    network_handle.send_partial_signatures(committee.epoch(), RandomnessRound(r));
555                }
556            }
557        }
558
559        Some(rm)
560    }
561
562    /// Sends the initial dkg::Message to begin the randomness DKG protocol.
563    /// For observers, this is a no-op (observers don't send messages).
564    pub async fn start_dkg(&mut self) -> SuiResult {
565        let party = match self.role.as_ref() {
566            DkgRole::Observer(_) => {
567                info!("random beacon: observer started observing DKG");
568                return Ok(());
569            }
570            DkgRole::Party(party) => party,
571        };
572
573        if self.used_messages.initialized() || self.dkg_output.initialized() {
574            // DKG already started (or completed or failed).
575            return Ok(());
576        }
577
578        let _ = self.dkg_start_time.set(Instant::now());
579
580        let epoch_store = self.epoch_store()?;
581        let dkg_version = epoch_store.protocol_config().dkg_version();
582        info!("random beacon: starting DKG, version {dkg_version}");
583
584        let msg = match VersionedDkgMessage::create(dkg_version, party) {
585            Ok(msg) => msg,
586            Err(FastCryptoError::IgnoredMessage) => {
587                info!(
588                    "random beacon: no DKG Message for party id={} (zero weight)",
589                    party.id
590                );
591                return Ok(());
592            }
593            Err(e) => {
594                error!("random beacon: error while creating a DKG Message: {e:?}");
595                return Ok(());
596            }
597        };
598
599        info!("random beacon: created {msg:?} with dkg version {dkg_version}");
600        let transaction = ConsensusTransaction::new_randomness_dkg_message(epoch_store.name, &msg);
601
602        #[allow(unused_mut)]
603        let mut fail_point_skip_sending = false;
604        fail_point_if!("rb-dkg", || {
605            // maybe skip sending in simtests
606            fail_point_skip_sending = true;
607        });
608        if !fail_point_skip_sending {
609            self.consensus_adapter
610                .submit_to_consensus(&[transaction], &epoch_store)?;
611        }
612
613        epoch_store
614            .metrics
615            .epoch_random_beacon_dkg_message_time_ms
616            .set(
617                self.dkg_start_time
618                    .get()
619                    .unwrap() // already set above
620                    .elapsed()
621                    .as_millis() as i64,
622            );
623        Ok(())
624    }
625
626    /// Processes all received messages and advances the randomness DKG state machine when possible,
627    /// sending out a dkg::Confirmation and generating final output.
628    pub(crate) async fn advance_dkg(
629        &mut self,
630        consensus_output: &mut ConsensusCommitOutput,
631        round: Round,
632    ) -> SuiResult {
633        let epoch_store = self.epoch_store()?;
634
635        self.try_merge_messages(consensus_output, &epoch_store)
636            .await?;
637        self.try_complete_dkg(consensus_output, round, &epoch_store)?;
638
639        // If we ran out of time, mark DKG as failed.
640        if !self.dkg_output.initialized()
641            && round
642                > epoch_store
643                    .protocol_config()
644                    .random_beacon_dkg_timeout_round()
645                    .into()
646        {
647            error!(
648                "random beacon: DKG timed out. Randomness disabled for this epoch. All randomness-using transactions will fail."
649            );
650            epoch_store.metrics.epoch_random_beacon_dkg_failed.set(1);
651            self.dkg_output
652                .set(None)
653                .expect("checked above that `dkg_output` is uninitialized");
654            consensus_output.set_dkg_output(None);
655        }
656
657        Ok(())
658    }
659
660    /// Drains enqueued messages and attempts to merge them. For validators, a successful merge
661    /// produces and broadcasts a DKG Confirmation. For observers, it just records the used messages.
662    async fn try_merge_messages(
663        &mut self,
664        consensus_output: &mut ConsensusCommitOutput,
665        epoch_store: &Arc<AuthorityPerEpochStore>,
666    ) -> SuiResult {
667        if self.dkg_output.initialized() || self.used_messages.initialized() {
668            return Ok(());
669        }
670
671        // Process all enqueued messages.
672        let mut handles: FuturesUnordered<_> = std::mem::take(&mut self.enqueued_messages)
673            .into_values()
674            .collect();
675        while let Some(res) = handles.next().await {
676            if let Ok(Some(processed)) = res {
677                self.processed_messages
678                    .insert(processed.sender(), processed.clone());
679                consensus_output.insert_dkg_processed_message(processed);
680            }
681        }
682
683        let messages: Vec<_> = self.processed_messages.values().cloned().collect();
684
685        match self.role.merge_messages(messages) {
686            Ok((conf, used_msgs)) => {
687                if let Some(conf) = &conf {
688                    info!(
689                        "random beacon: sending DKG Confirmation with {} complaints",
690                        conf.num_of_complaints()
691                    );
692                } else {
693                    info!(
694                        "random beacon: observer merged {} DKG messages",
695                        used_msgs
696                            .as_v1()
697                            .expect("expected V1 used processed messages")
698                            .0
699                            .len()
700                    );
701                }
702                if self.used_messages.set(used_msgs.clone()).is_err() {
703                    error!("BUG: used_messages should only ever be set once");
704                }
705                consensus_output.insert_dkg_used_messages(used_msgs);
706
707                if let Some(conf) = conf {
708                    let transaction = ConsensusTransaction::new_randomness_dkg_confirmation(
709                        epoch_store.name,
710                        &conf,
711                    );
712
713                    #[allow(unused_mut)]
714                    let mut fail_point_skip_sending = false;
715                    fail_point_if!("rb-dkg", || {
716                        // maybe skip sending in simtests
717                        fail_point_skip_sending = true;
718                    });
719                    if !fail_point_skip_sending {
720                        self.consensus_adapter
721                            .submit_to_consensus(&[transaction], epoch_store)?;
722                    }
723
724                    let elapsed = self.dkg_start_time.get().map(|t| t.elapsed().as_millis());
725                    if let Some(elapsed) = elapsed {
726                        epoch_store
727                            .metrics
728                            .epoch_random_beacon_dkg_confirmation_time_ms
729                            .set(elapsed as i64);
730                    }
731                }
732            }
733            Err(FastCryptoError::NotEnoughInputs) => (), // wait for more input
734            Err(e) => debug!("random beacon: error while merging DKG Messages: {e:?}"),
735        }
736
737        Ok(())
738    }
739
740    /// Attempts to complete DKG once enough Confirmations have been collected. For validators,
741    /// this produces the shared public key and private key shares. For observers, only the
742    /// shared public key is derived.
743    fn try_complete_dkg(
744        &mut self,
745        consensus_output: &mut ConsensusCommitOutput,
746        round: Round,
747        epoch_store: &Arc<AuthorityPerEpochStore>,
748    ) -> SuiResult {
749        if !self.dkg_output.initialized() && self.used_messages.initialized() {
750            let used_messages = self
751                .used_messages
752                .get()
753                .expect("checked above that `used_messages` is initialized");
754            let complete_result = self
755                .role
756                .complete_dkg(used_messages, self.confirmations.values());
757
758            let epoch = epoch_store.committee().epoch();
759            let num_confirmations = self.confirmations.len();
760            let num_messages = self.processed_messages.len();
761
762            match complete_result {
763                Ok(output) => {
764                    // Set the output now both internally and to consensus output
765                    self.dkg_output
766                        .set(Some(output.clone()))
767                        .expect("checked above that `dkg_output` is uninitialized");
768                    consensus_output.set_dkg_output(Some(output.clone()));
769
770                    self.randomness_receiver_handle
771                        .set_public_key(output.vss_pk.c0());
772
773                    let epoch_elapsed = epoch_store.epoch_open_time.elapsed().as_millis();
774                    epoch_store
775                        .metrics
776                        .epoch_random_beacon_dkg_epoch_start_completion_time_ms
777                        .set(epoch_elapsed as i64);
778                    epoch_store.metrics.epoch_random_beacon_dkg_failed.set(0);
779
780                    match self.role.as_ref() {
781                        DkgRole::Party(party) => {
782                            let num_shares =
783                                output.shares.as_ref().map_or(0, |shares| shares.len());
784                            let elapsed =
785                                self.dkg_start_time.get().map(|t| t.elapsed().as_millis());
786                            info!(
787                                "random beacon: DKG complete for Party epoch={epoch} commit_round={round} \
788                                 num_messages={num_messages} num_confirmations={num_confirmations} \
789                                 num_shares={num_shares} epoch_elapsed_ms={epoch_elapsed} dkg_elapsed_ms={elapsed:?}"
790                            );
791                            epoch_store
792                                .metrics
793                                .epoch_random_beacon_dkg_num_shares
794                                .set(num_shares as i64);
795
796                            if let Some(elapsed) = elapsed {
797                                epoch_store
798                                    .metrics
799                                    .epoch_random_beacon_dkg_completion_time_ms
800                                    .set(elapsed as i64);
801                            }
802
803                            self.network_handle.update_epoch(
804                                epoch_store.committee().epoch(),
805                                self.authority_info.clone(),
806                                output,
807                                party.t(),
808                                None,
809                            );
810                        }
811                        DkgRole::Observer(_) => {
812                            info!(
813                                "random beacon: DKG complete for Observer epoch={epoch} commit_round={round} \
814                                 num_messages={num_messages} num_confirmations={num_confirmations} \
815                                 epoch_elapsed_ms={epoch_elapsed}"
816                            );
817                        }
818                    }
819                }
820                Err(FastCryptoError::NotEnoughInputs) => (), // wait for more input
821                Err(e) => error!("random beacon: error while processing DKG Confirmations: {e:?}"),
822            }
823        }
824
825        Ok(())
826    }
827
828    /// Adds a received VersionedDkgMessage to the randomness DKG state machine.
829    pub fn add_message(
830        &mut self,
831        authority: &AuthorityName,
832        msg: VersionedDkgMessage,
833    ) -> SuiResult {
834        // message was received from other validators, so we need to ensure it uses a supported
835        // version before we call other functions that assume the version is correct
836        let dkg_version = self.epoch_store()?.protocol_config().dkg_version();
837        if !msg.is_valid_version(dkg_version) {
838            warn!("ignoring DKG Message from authority {authority:?} with unsupported version");
839            return Ok(());
840        }
841
842        if self.used_messages.initialized() || self.dkg_output.initialized() {
843            // We've already sent a `Confirmation`, so we can't add any more messages.
844            return Ok(());
845        }
846        let Some((_, party_id)) = self.authority_info.get(authority) else {
847            debug_fatal!(
848                "random beacon: received DKG Message from unknown authority: {authority:?}"
849            );
850            return Ok(());
851        };
852        if *party_id != msg.sender() {
853            warn!(
854                "ignoring equivocating DKG Message from authority {authority:?} pretending to be PartyId {party_id:?}"
855            );
856            return Ok(());
857        }
858        if self.enqueued_messages.contains_key(&msg.sender())
859            || self.processed_messages.contains_key(&msg.sender())
860        {
861            info!("ignoring duplicate DKG Message from authority {authority:?}");
862            return Ok(());
863        }
864
865        let sender = msg.sender();
866        let role = self.role.clone();
867        // TODO: Could save some CPU by not processing messages if we already have enough to merge.
868        let handle = tokio::task::spawn_blocking(move || match role.process_message(msg) {
869            Ok(processed) => Some(processed),
870            Err(err) => {
871                debug!("random beacon: error while processing DKG Message: {err:?}");
872                None
873            }
874        });
875        self.enqueued_messages.insert(sender, handle);
876        Ok(())
877    }
878
879    /// Adds a received dkg::Confirmation to the randomness DKG state machine.
880    pub(crate) fn add_confirmation(
881        &mut self,
882        output: &mut ConsensusCommitOutput,
883        authority: &AuthorityName,
884        conf: VersionedDkgConfirmation,
885    ) -> SuiResult {
886        // confirmation was received from other validators, so we need to ensure it uses a supported
887        // version before we call other functions that assume the version is correct
888        let dkg_version = self.epoch_store()?.protocol_config().dkg_version();
889        if !conf.is_valid_version(dkg_version) {
890            warn!(
891                "ignoring DKG Confirmation from authority {authority:?} with unsupported version"
892            );
893            return Ok(());
894        }
895
896        if self.dkg_output.initialized() {
897            // Once we have completed DKG, no more `Confirmation`s are needed.
898            return Ok(());
899        }
900        let Some((_, party_id)) = self.authority_info.get(authority) else {
901            error!(
902                "random beacon: received DKG Confirmation from unknown authority: {authority:?}"
903            );
904            return Ok(());
905        };
906        if *party_id != conf.sender() {
907            warn!(
908                "ignoring equivocating DKG Confirmation from authority {authority:?} pretending to be PartyId {party_id:?}"
909            );
910            return Ok(());
911        }
912        self.confirmations.insert(conf.sender(), conf.clone());
913        output.insert_dkg_confirmation(conf);
914        Ok(())
915    }
916
917    /// Reserves the next available round number for randomness generation if enough time has
918    /// elapsed, or returns None if not yet ready (based on ProtocolConfig setting). Once the given
919    /// batch is written, `generate_randomness` must be called to start the process. On restart,
920    /// any reserved rounds for which the batch was written will automatically be resumed.
921    pub(crate) fn reserve_next_randomness(
922        &mut self,
923        commit_timestamp: TimestampMs,
924        output: &mut ConsensusCommitOutput,
925    ) -> SuiResult<Option<RandomnessRound>> {
926        let epoch_store = self.epoch_store()?;
927
928        let last_round_timestamp = epoch_store
929            .get_randomness_last_round_timestamp()
930            .expect("read should not fail");
931
932        if let Some(last_round_timestamp) = last_round_timestamp
933            && commit_timestamp - last_round_timestamp
934                < epoch_store
935                    .protocol_config()
936                    .random_beacon_min_round_interval_ms()
937        {
938            return Ok(None);
939        }
940
941        let randomness_round = self.next_randomness_round;
942        self.next_randomness_round = self
943            .next_randomness_round
944            .checked_add(1)
945            .expect("RandomnessRound should not overflow");
946
947        output.reserve_next_randomness_round(self.next_randomness_round, commit_timestamp);
948
949        Ok(Some(randomness_round))
950    }
951
952    /// Starts the process of generating the given RandomnessRound (validators only).
953    pub fn generate_randomness(&self, epoch: EpochId, randomness_round: RandomnessRound) {
954        if self.role.is_party() {
955            self.network_handle
956                .send_partial_signatures(epoch, randomness_round);
957        }
958    }
959
960    pub fn dkg_status(&self) -> DkgStatus {
961        match self.dkg_output.get() {
962            Some(Some(_)) => DkgStatus::Successful,
963            Some(None) => DkgStatus::Failed,
964            None => DkgStatus::Pending,
965        }
966    }
967
968    /// Generates a new RandomnessReporter for reporting observed rounds to this RandomnessManager.
969    /// Returns None for observers (they don't generate partial signatures).
970    pub fn reporter(&self) -> Option<RandomnessReporter> {
971        if self.role.is_observer() {
972            return None;
973        }
974        Some(RandomnessReporter {
975            epoch_store: self.epoch_store.clone(),
976            epoch: self.epoch,
977            network_handle: self.network_handle.clone(),
978            highest_completed_round: self.highest_completed_round.clone(),
979        })
980    }
981
982    #[cfg(test)]
983    fn dkg_output(&self) -> Option<&dkg_v1::Output<PkG, EncG>> {
984        self.dkg_output.get().and_then(|o| o.as_ref())
985    }
986
987    fn epoch_store(&self) -> SuiResult<Arc<AuthorityPerEpochStore>> {
988        self.epoch_store
989            .upgrade()
990            .ok_or(SuiErrorKind::EpochEnded(self.epoch).into())
991    }
992
993    fn randomness_dkg_info_from_committee(
994        committee: &Committee,
995    ) -> Vec<(
996        u16,
997        AuthorityName,
998        fastcrypto_tbls::ecies_v1::PublicKey<bls12381::G2Element>,
999        StakeUnit,
1000    )> {
1001        committee
1002            .members()
1003            .map(|(name, stake)| {
1004                let index: u16 = committee
1005                    .authority_index(name)
1006                    .expect("lookup of known committee member should succeed")
1007                    .try_into()
1008                    .expect("authority index should fit in u16");
1009                let pk = bls12381::G2Element::from_byte_array(
1010                    committee
1011                        .public_key(name)
1012                        .expect("lookup of known committee member should succeed")
1013                        .as_bytes()
1014                        .try_into()
1015                        .expect("key length should match"),
1016                )
1017                .expect("should work to convert BLS key to G2Element");
1018                (
1019                    index,
1020                    *name,
1021                    fastcrypto_tbls::ecies_v1::PublicKey::from(pk),
1022                    *stake,
1023                )
1024            })
1025            .collect()
1026    }
1027}
1028
1029// Used by other components to notify the randomness system of observed randomness.
1030#[derive(Clone)]
1031pub struct RandomnessReporter {
1032    epoch_store: Weak<AuthorityPerEpochStore>,
1033    epoch: EpochId,
1034    network_handle: randomness::Handle,
1035    highest_completed_round: Arc<Mutex<Option<RandomnessRound>>>,
1036}
1037
1038impl RandomnessReporter {
1039    /// Notifies the associated randomness manager that randomness for the given round has been
1040    /// durably committed in a checkpoint. This completes the process of generating randomness for
1041    /// the round.
1042    pub fn notify_randomness_in_checkpoint(&self, round: RandomnessRound) -> SuiResult {
1043        let epoch_store = self
1044            .epoch_store
1045            .upgrade()
1046            .ok_or(SuiErrorKind::EpochEnded(self.epoch))?;
1047        let mut highest_completed_round = self.highest_completed_round.lock();
1048        if Some(round) > *highest_completed_round {
1049            *highest_completed_round = Some(round);
1050            epoch_store
1051                .tables()?
1052                .randomness_highest_completed_round
1053                .insert(&SINGLETON_KEY, &round)?;
1054            self.network_handle
1055                .complete_round(epoch_store.committee().epoch(), round);
1056        }
1057        Ok(())
1058    }
1059}
1060
1061#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1062pub enum DkgStatus {
1063    Pending,
1064    Failed,
1065    Successful,
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070    use crate::{
1071        authority::{
1072            authority_per_epoch_store::{ExecutionIndices, ExecutionIndicesWithStatsV2},
1073            test_authority_builder::TestAuthorityBuilder,
1074        },
1075        checkpoints::CheckpointStore,
1076        consensus_adapter::{ConsensusAdapter, ConsensusAdapterMetrics, MockConsensusClient},
1077        epoch::randomness::*,
1078        mock_consensus::with_block_status,
1079        randomness_round_receiver::RandomnessRoundReceiverHandle,
1080    };
1081    use consensus_core::BlockStatus;
1082    use consensus_types::block::BlockRef;
1083    use fastcrypto::groups::bls12381;
1084    use fastcrypto::serde_helpers::ToFromByteArray;
1085    use fastcrypto_tbls::{mocked_dkg, nodes};
1086    use std::num::NonZeroUsize;
1087    use sui_protocol_config::ProtocolConfig;
1088    use sui_protocol_config::{Chain, ProtocolVersion};
1089    use sui_types::{base_types::AuthorityName, messages_consensus::ConsensusTransactionKind};
1090    use tokio::sync::mpsc;
1091    use typed_store::Map;
1092
1093    use arc_swap::Guard;
1094
1095    /// Test harness that sets up validators (and optionally an observer) with mock consensus,
1096    /// ready for DKG message exchange.
1097    struct DkgTestSetup {
1098        network_config: sui_swarm_config::network_config::NetworkConfig,
1099        epoch_stores: Vec<Guard<Arc<AuthorityPerEpochStore>>>,
1100        randomness_managers: Vec<RandomnessManager>,
1101        tx_consensus: mpsc::Sender<Vec<ConsensusTransaction>>,
1102        rx_consensus: mpsc::Receiver<Vec<ConsensusTransaction>>,
1103        num_validators: usize,
1104    }
1105
1106    impl DkgTestSetup {
1107        async fn new(include_observer: bool) -> Self {
1108            let network_config =
1109                sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1110                    .committee_size(NonZeroUsize::new(4).unwrap())
1111                    .with_reference_gas_price(500)
1112                    .build();
1113
1114            let mut protocol_config =
1115                ProtocolConfig::get_for_version(ProtocolVersion::max(), Chain::Unknown);
1116            protocol_config.set_random_beacon_dkg_version_for_testing(1);
1117
1118            let num_validators = network_config.validator_configs.len();
1119            let mut epoch_stores = Vec::new();
1120            let mut randomness_managers = Vec::new();
1121            let (tx_consensus, rx_consensus) = mpsc::channel(100);
1122
1123            for validator in network_config.validator_configs.iter() {
1124                let mut mock_consensus_client = MockConsensusClient::new();
1125                let tx_consensus = tx_consensus.clone();
1126                mock_consensus_client
1127                    .expect_submit()
1128                    .withf(move |transactions: &[ConsensusTransaction], _epoch_store| {
1129                        tx_consensus.try_send(transactions.to_vec()).unwrap();
1130                        true
1131                    })
1132                    .returning(|_, _| {
1133                        Ok((
1134                            Vec::new(),
1135                            with_block_status(BlockStatus::Sequenced(BlockRef::MIN)),
1136                        ))
1137                    });
1138
1139                let state = TestAuthorityBuilder::new()
1140                    .with_protocol_config(protocol_config.clone())
1141                    .with_genesis_and_keypair(
1142                        &network_config.genesis,
1143                        validator.protocol_key_pair(),
1144                    )
1145                    .build()
1146                    .await;
1147                let consensus_adapter = Arc::new(ConsensusAdapter::new(
1148                    Arc::new(mock_consensus_client),
1149                    CheckpointStore::new_for_tests(),
1150                    state.name,
1151                    100_000,
1152                    100_000,
1153                    ConsensusAdapterMetrics::new_test(),
1154                    Arc::new(tokio::sync::Notify::new()),
1155                ));
1156                let epoch_store = state.epoch_store_for_testing();
1157                let randomness_manager = RandomnessManager::try_new(
1158                    Arc::downgrade(&epoch_store),
1159                    Box::new(consensus_adapter.clone()),
1160                    sui_network::randomness::Handle::new_stub(),
1161                    Some(validator.protocol_key_pair()),
1162                    RandomnessRoundReceiverHandle::new_for_testing(),
1163                )
1164                .await
1165                .unwrap();
1166
1167                epoch_stores.push(epoch_store);
1168                randomness_managers.push(randomness_manager);
1169            }
1170
1171            if include_observer {
1172                let observer_epoch_store = epoch_stores[0].clone();
1173                let mut mock_observer_consensus = MockConsensusClient::new();
1174                mock_observer_consensus
1175                    .expect_submit()
1176                    .returning(|_, _| panic!("observer should not submit to consensus"));
1177                let observer_adapter = Arc::new(ConsensusAdapter::new(
1178                    Arc::new(mock_observer_consensus),
1179                    CheckpointStore::new_for_tests(),
1180                    observer_epoch_store.name,
1181                    100_000,
1182                    100_000,
1183                    ConsensusAdapterMetrics::new_test(),
1184                    Arc::new(tokio::sync::Notify::new()),
1185                ));
1186                let observer_manager = RandomnessManager::try_new(
1187                    Arc::downgrade(&observer_epoch_store),
1188                    Box::new(observer_adapter),
1189                    sui_network::randomness::Handle::new_stub(),
1190                    None,
1191                    RandomnessRoundReceiverHandle::new_for_testing(),
1192                )
1193                .await
1194                .unwrap();
1195
1196                epoch_stores.push(observer_epoch_store.into());
1197                randomness_managers.push(observer_manager);
1198            }
1199
1200            Self {
1201                network_config,
1202                epoch_stores,
1203                randomness_managers,
1204                tx_consensus,
1205                rx_consensus,
1206                num_validators,
1207            }
1208        }
1209
1210        fn consensus_adapter(&self, authority: AuthorityName) -> Arc<ConsensusAdapter> {
1211            let mut mock_consensus_client = MockConsensusClient::new();
1212            let tx_consensus = self.tx_consensus.clone();
1213            mock_consensus_client
1214                .expect_submit()
1215                .withf(move |transactions: &[ConsensusTransaction], _epoch_store| {
1216                    tx_consensus.try_send(transactions.to_vec()).unwrap();
1217                    true
1218                })
1219                .returning(|_, _| {
1220                    Ok((
1221                        Vec::new(),
1222                        with_block_status(BlockStatus::Sequenced(BlockRef::MIN)),
1223                    ))
1224                });
1225
1226            Arc::new(ConsensusAdapter::new(
1227                Arc::new(mock_consensus_client),
1228                CheckpointStore::new_for_tests(),
1229                authority,
1230                100_000,
1231                100_000,
1232                ConsensusAdapterMetrics::new_test(),
1233                Arc::new(tokio::sync::Notify::new()),
1234            ))
1235        }
1236
1237        async fn recover_validator_randomness_managers(&mut self) -> usize {
1238            let mut recovered_randomness_managers = Vec::new();
1239            for (i, validator) in self
1240                .network_config
1241                .validator_configs
1242                .iter()
1243                .enumerate()
1244                .take(self.num_validators)
1245            {
1246                let consensus_adapter = self.consensus_adapter(self.epoch_stores[i].name);
1247                recovered_randomness_managers.push(
1248                    RandomnessManager::try_new(
1249                        Arc::downgrade(&self.epoch_stores[i]),
1250                        Box::new(consensus_adapter),
1251                        sui_network::randomness::Handle::new_stub(),
1252                        Some(validator.protocol_key_pair()),
1253                        RandomnessRoundReceiverHandle::new_for_testing(),
1254                    )
1255                    .await
1256                    .unwrap(),
1257                );
1258            }
1259            let recovered_count = recovered_randomness_managers.len();
1260            self.randomness_managers = recovered_randomness_managers;
1261            recovered_count
1262        }
1263
1264        /// Runs start_dkg on all managers and collects the DKG messages from validators.
1265        async fn start_dkg_and_collect_messages(&mut self) -> Vec<VersionedDkgMessage> {
1266            let mut dkg_messages = Vec::new();
1267            for randomness_manager in self.randomness_managers.iter_mut() {
1268                randomness_manager.start_dkg().await.unwrap();
1269            }
1270            for _ in 0..self.num_validators {
1271                let mut dkg_message = self.rx_consensus.recv().await.unwrap();
1272                assert!(dkg_message.len() == 1);
1273                match dkg_message.remove(0).kind {
1274                    ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
1275                        let msg: VersionedDkgMessage = bcs::from_bytes(&bytes)
1276                            .expect("DKG message deserialization should not fail");
1277                        dkg_messages.push(msg);
1278                    }
1279                    _ => panic!("wrong type of message sent"),
1280                }
1281            }
1282            dkg_messages
1283        }
1284
1285        /// Distributes DKG messages to all managers and advances DKG at the given round.
1286        async fn distribute_messages_and_advance(
1287            &mut self,
1288            dkg_messages: &[VersionedDkgMessage],
1289            advance_round: Round,
1290        ) {
1291            let indexed_messages: Vec<_> = dkg_messages.iter().cloned().enumerate().collect();
1292            self.distribute_indexed_messages_and_advance(&indexed_messages, advance_round)
1293                .await;
1294        }
1295
1296        async fn distribute_indexed_messages_and_advance(
1297            &mut self,
1298            dkg_messages: &[(usize, VersionedDkgMessage)],
1299            advance_round: Round,
1300        ) {
1301            for i in 0..self.randomness_managers.len() {
1302                let mut output = ConsensusCommitOutput::new(0);
1303                output.record_consensus_commit_stats(ExecutionIndicesWithStatsV2 {
1304                    index: ExecutionIndices {
1305                        last_committed_round: 0,
1306                        ..Default::default()
1307                    },
1308                    ..Default::default()
1309                });
1310                for (j, dkg_message) in dkg_messages.iter().cloned() {
1311                    self.randomness_managers[i]
1312                        .add_message(&self.epoch_stores[j].name, dkg_message)
1313                        .unwrap();
1314                }
1315                self.randomness_managers[i]
1316                    .advance_dkg(&mut output, advance_round)
1317                    .await
1318                    .unwrap();
1319                let mut batch = self.epoch_stores[i].db_batch_for_test();
1320                output
1321                    .write_to_batch(&self.epoch_stores[i], &mut batch)
1322                    .unwrap();
1323                batch.write().unwrap();
1324            }
1325        }
1326
1327        /// Collects DKG confirmations from validators and distributes them to all managers.
1328        async fn collect_and_distribute_confirmations(&mut self) {
1329            let mut dkg_confirmations = Vec::new();
1330            for _ in 0..self.num_validators {
1331                let mut dkg_confirmation = self.rx_consensus.recv().await.unwrap();
1332                assert!(dkg_confirmation.len() == 1);
1333                match dkg_confirmation.remove(0).kind {
1334                    ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
1335                        let msg: VersionedDkgConfirmation = bcs::from_bytes(&bytes)
1336                            .expect("DKG confirmation deserialization should not fail");
1337                        dkg_confirmations.push(msg);
1338                    }
1339                    _ => panic!("wrong type of message sent"),
1340                }
1341            }
1342            for i in 0..self.randomness_managers.len() {
1343                let mut output = ConsensusCommitOutput::new(0);
1344                output.record_consensus_commit_stats(ExecutionIndicesWithStatsV2 {
1345                    index: ExecutionIndices {
1346                        last_committed_round: 1,
1347                        ..Default::default()
1348                    },
1349                    ..Default::default()
1350                });
1351                for (j, dkg_confirmation) in dkg_confirmations.iter().cloned().enumerate() {
1352                    self.randomness_managers[i]
1353                        .add_confirmation(&mut output, &self.epoch_stores[j].name, dkg_confirmation)
1354                        .unwrap();
1355                }
1356                self.randomness_managers[i]
1357                    .advance_dkg(&mut output, 0)
1358                    .await
1359                    .unwrap();
1360                let mut batch = self.epoch_stores[i].db_batch_for_test();
1361                output
1362                    .write_to_batch(&self.epoch_stores[i], &mut batch)
1363                    .unwrap();
1364                batch.write().unwrap();
1365            }
1366        }
1367    }
1368
1369    #[tokio::test]
1370    async fn test_dkg() {
1371        telemetry_subscribers::init_for_testing();
1372
1373        let mut setup = DkgTestSetup::new(false).await;
1374        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1375        setup
1376            .distribute_messages_and_advance(&dkg_messages, 0)
1377            .await;
1378        setup.collect_and_distribute_confirmations().await;
1379
1380        for rm in &setup.randomness_managers {
1381            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1382        }
1383    }
1384
1385    #[tokio::test]
1386    async fn test_dkg_expiration() {
1387        telemetry_subscribers::init_for_testing();
1388
1389        let mut setup = DkgTestSetup::new(false).await;
1390        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1391        // Pass u64::MAX as round to trigger DKG timeout.
1392        setup
1393            .distribute_messages_and_advance(&dkg_messages, u64::MAX)
1394            .await;
1395
1396        for rm in &setup.randomness_managers {
1397            assert_eq!(DkgStatus::Failed, rm.dkg_status());
1398        }
1399
1400        let recovered_count = setup.recover_validator_randomness_managers().await;
1401        assert_eq!(setup.num_validators, recovered_count);
1402
1403        // Verify the failure is durably loaded on restart: RandomnessManagers reconstructed
1404        // from the same epoch stores must report DKG as failed (not pending), exercising the
1405        // `dkg_output_v2` failure-load path in `try_new`.
1406        for rm in &setup.randomness_managers {
1407            assert_eq!(DkgStatus::Failed, rm.dkg_status());
1408        }
1409    }
1410
1411    #[tokio::test]
1412    async fn test_dkg_recovers_processed_messages_after_restart() {
1413        telemetry_subscribers::init_for_testing();
1414
1415        let mut setup = DkgTestSetup::new(false).await;
1416        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1417        let processed_before_restart = 1;
1418        assert!(processed_before_restart > 0);
1419        assert!(processed_before_restart < setup.num_validators);
1420        let pre_restart_messages: Vec<_> = dkg_messages
1421            .iter()
1422            .cloned()
1423            .enumerate()
1424            .take(processed_before_restart)
1425            .collect();
1426        let post_restart_messages: Vec<_> = dkg_messages
1427            .iter()
1428            .cloned()
1429            .enumerate()
1430            .skip(processed_before_restart)
1431            .collect();
1432
1433        setup
1434            .distribute_indexed_messages_and_advance(&pre_restart_messages, 0)
1435            .await;
1436
1437        for rm in &setup.randomness_managers {
1438            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1439            assert_eq!(processed_before_restart, rm.processed_messages.len());
1440            assert!(!rm.used_messages.initialized());
1441        }
1442
1443        let recovered_count = setup.recover_validator_randomness_managers().await;
1444        assert_eq!(setup.num_validators, recovered_count);
1445        for rm in &setup.randomness_managers {
1446            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1447            assert_eq!(processed_before_restart, rm.processed_messages.len());
1448            assert!(!rm.used_messages.initialized());
1449        }
1450
1451        setup
1452            .distribute_indexed_messages_and_advance(&post_restart_messages, 0)
1453            .await;
1454
1455        for rm in &setup.randomness_managers {
1456            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1457            assert_eq!(setup.num_validators, rm.processed_messages.len());
1458            assert!(rm.used_messages.initialized());
1459        }
1460
1461        setup.collect_and_distribute_confirmations().await;
1462
1463        for rm in &setup.randomness_managers {
1464            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1465        }
1466    }
1467
1468    /// Verifies that an Observer completes DKG alongside validators and derives the same
1469    /// shared public key (vss_pk), but without receiving any private key shares.
1470    #[tokio::test]
1471    async fn test_dkg_observer() {
1472        telemetry_subscribers::init_for_testing();
1473
1474        let mut setup = DkgTestSetup::new(true).await;
1475        let observer_idx = setup.randomness_managers.len() - 1;
1476
1477        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1478        setup
1479            .distribute_messages_and_advance(&dkg_messages, 0)
1480            .await;
1481
1482        for rm in &setup.randomness_managers {
1483            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1484        }
1485
1486        setup.collect_and_distribute_confirmations().await;
1487
1488        for rm in &setup.randomness_managers {
1489            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1490        }
1491
1492        // Verify the observer derived the same vss_pk as validators, but without shares.
1493        let observer_output = setup.randomness_managers[observer_idx]
1494            .dkg_output()
1495            .expect("observer should have DKG output");
1496        let validator_output = setup.randomness_managers[0]
1497            .dkg_output()
1498            .expect("validator should have DKG output");
1499        assert_eq!(observer_output.vss_pk, validator_output.vss_pk);
1500        assert!(observer_output.shares.is_none());
1501
1502        for rm in &setup.randomness_managers[..setup.num_validators] {
1503            let output = rm.dkg_output().expect("validator should have DKG output");
1504            assert!(output.shares.is_some());
1505        }
1506    }
1507
1508    /// Builds a minimal set of DKG Nodes from a network config's validator key pairs.
1509    fn build_dkg_nodes(
1510        network_config: &sui_swarm_config::network_config::NetworkConfig,
1511    ) -> (nodes::Nodes<EncG>, u16) {
1512        let dkg_nodes: Vec<_> = network_config
1513            .validator_configs
1514            .iter()
1515            .enumerate()
1516            .map(|(i, v)| {
1517                let pk = bls12381::G2Element::from_byte_array(
1518                    v.protocol_key_pair()
1519                        .public()
1520                        .as_bytes()
1521                        .try_into()
1522                        .expect("key length should match"),
1523                )
1524                .expect("should work to convert BLS key to G2Element");
1525                nodes::Node::<EncG> {
1526                    id: i as u16,
1527                    pk: fastcrypto_tbls::ecies_v1::PublicKey::from(pk),
1528                    weight: 1,
1529                }
1530            })
1531            .collect();
1532        let num_nodes = dkg_nodes.len();
1533        // threshold = ceil(num_nodes / 3) works for a minimal committee
1534        let t = num_nodes.div_ceil(3) as u16;
1535        nodes::Nodes::new(dkg_nodes).map(|n| (n, t)).unwrap()
1536    }
1537
1538    #[test]
1539    fn test_dkg_role_try_new_party() {
1540        let network_config =
1541            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1542                .committee_size(NonZeroUsize::new(4).unwrap())
1543                .with_reference_gas_price(500)
1544                .build();
1545
1546        let (nodes, t) = build_dkg_nodes(&network_config);
1547        let random_oracle =
1548            fastcrypto_tbls::random_oracle::RandomOracle::new("test_dkg_role_party");
1549
1550        let role = DkgRole::try_new(
1551            Some(network_config.validator_configs[0].protocol_key_pair()),
1552            nodes,
1553            t,
1554            random_oracle,
1555        );
1556        assert!(role.is_some());
1557        assert!(role.unwrap().is_party());
1558    }
1559
1560    #[tokio::test]
1561    async fn test_randomness_manager_crash_recovery_v1() {
1562        telemetry_subscribers::init_for_testing();
1563
1564        let network_config =
1565            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1566                .committee_size(NonZeroUsize::new(4).unwrap())
1567                .with_reference_gas_price(500)
1568                .build();
1569
1570        let mut protocol_config =
1571            ProtocolConfig::get_for_version(ProtocolVersion::max(), Chain::Unknown);
1572        protocol_config.set_random_beacon_dkg_version_for_testing(1);
1573
1574        let validator = &network_config.validator_configs[0];
1575        let state = TestAuthorityBuilder::new()
1576            .with_protocol_config(protocol_config.clone())
1577            .with_genesis_and_keypair(&network_config.genesis, validator.protocol_key_pair())
1578            .build()
1579            .await;
1580        let epoch_store = state.epoch_store_for_testing();
1581
1582        let dkg_nodes = nodes::Nodes::new(
1583            RandomnessManager::randomness_dkg_info_from_committee(epoch_store.committee())
1584                .into_iter()
1585                .map(|(id, _, pk, stake)| nodes::Node::<bls12381::G2Element> {
1586                    id,
1587                    pk,
1588                    weight: stake.try_into().unwrap(),
1589                })
1590                .collect(),
1591        )
1592        .unwrap();
1593        let threshold = epoch_store
1594            .committee()
1595            .validity_threshold()
1596            .try_into()
1597            .unwrap();
1598        let party_id = epoch_store
1599            .committee()
1600            .authority_index(&epoch_store.name)
1601            .unwrap()
1602            .try_into()
1603            .unwrap();
1604        let expected_dkg_output = mocked_dkg::generate_mocked_output::<
1605            bls12381::G2Element,
1606            bls12381::G2Element,
1607        >(dkg_nodes, threshold, 0, party_id);
1608        let expected_dkg_output_bytes =
1609            bcs::to_bytes(&expected_dkg_output).expect("DKG output serialization should not fail");
1610        let expected_public_key = expected_dkg_output.vss_pk.c0();
1611
1612        let tables = epoch_store.tables().unwrap();
1613        tables
1614            .dkg_output_v2
1615            .insert(&SINGLETON_KEY, &Some(expected_dkg_output))
1616            .unwrap();
1617        tables
1618            .randomness_next_round
1619            .insert(&SINGLETON_KEY, &RandomnessRound(3))
1620            .unwrap();
1621        tables
1622            .randomness_highest_completed_round
1623            .insert(&SINGLETON_KEY, &RandomnessRound(1))
1624            .unwrap();
1625
1626        let consensus_adapter = Arc::new(ConsensusAdapter::new(
1627            Arc::new(MockConsensusClient::new()),
1628            CheckpointStore::new_for_tests(),
1629            epoch_store.name,
1630            100_000,
1631            100_000,
1632            ConsensusAdapterMetrics::new_test(),
1633            Arc::new(tokio::sync::Notify::new()),
1634        ));
1635        let recovered_receiver_handle = RandomnessRoundReceiverHandle::new_for_testing();
1636        assert!(recovered_receiver_handle.public_key_for_testing().is_none());
1637
1638        let recovered_randomness_manager = RandomnessManager::try_new(
1639            Arc::downgrade(&epoch_store),
1640            Box::new(consensus_adapter.clone()),
1641            sui_network::randomness::Handle::new_stub(),
1642            Some(validator.protocol_key_pair()),
1643            recovered_receiver_handle.clone(),
1644        )
1645        .await
1646        .unwrap();
1647
1648        assert_eq!(
1649            DkgStatus::Successful,
1650            recovered_randomness_manager.dkg_status()
1651        );
1652        let recovered_dkg_output = recovered_randomness_manager
1653            .dkg_output
1654            .get()
1655            .expect("recovered DKG output should be initialized")
1656            .as_ref()
1657            .expect("recovered DKG should be successful");
1658        assert_eq!(
1659            expected_dkg_output_bytes,
1660            bcs::to_bytes(recovered_dkg_output).expect("DKG output serialization should not fail")
1661        );
1662        assert_eq!(
1663            RandomnessRound(3),
1664            recovered_randomness_manager.next_randomness_round
1665        );
1666        assert_eq!(
1667            Some(RandomnessRound(1)),
1668            *recovered_randomness_manager.highest_completed_round.lock()
1669        );
1670        let recovered_public_key = recovered_receiver_handle
1671            .public_key_for_testing()
1672            .expect("public key should be restored on recovery");
1673        assert_eq!(
1674            expected_public_key.to_byte_array(),
1675            recovered_public_key.to_byte_array()
1676        );
1677    }
1678
1679    #[test]
1680    fn test_dkg_role_try_new_observer() {
1681        let network_config =
1682            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1683                .committee_size(NonZeroUsize::new(4).unwrap())
1684                .with_reference_gas_price(500)
1685                .build();
1686
1687        let (nodes, t) = build_dkg_nodes(&network_config);
1688        let random_oracle =
1689            fastcrypto_tbls::random_oracle::RandomOracle::new("test_dkg_role_observer");
1690
1691        let role = DkgRole::try_new(None, nodes, t, random_oracle);
1692        assert!(role.is_some());
1693        assert!(role.unwrap().is_observer());
1694    }
1695}