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