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, 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_tbls::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_tbls::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_tbls::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_tbls::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_tbls::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_tbls::{mocked_dkg, nodes};
1210    use std::num::NonZeroUsize;
1211    use sui_protocol_config::ProtocolConfig;
1212    use sui_protocol_config::{Chain, ProtocolVersion};
1213    use sui_types::{base_types::AuthorityName, messages_consensus::ConsensusTransactionKind};
1214    use tokio::sync::mpsc;
1215    use typed_store::Map;
1216
1217    use arc_swap::Guard;
1218
1219    /// Test harness that sets up validators (and optionally an observer) with mock consensus,
1220    /// ready for DKG message exchange.
1221    struct DkgTestSetup {
1222        network_config: sui_swarm_config::network_config::NetworkConfig,
1223        epoch_stores: Vec<Guard<Arc<AuthorityPerEpochStore>>>,
1224        randomness_managers: Vec<RandomnessManager>,
1225        tx_consensus: mpsc::Sender<Vec<ConsensusTransaction>>,
1226        rx_consensus: mpsc::Receiver<Vec<ConsensusTransaction>>,
1227        num_validators: usize,
1228    }
1229
1230    impl DkgTestSetup {
1231        async fn new(include_observer: bool) -> Self {
1232            let mut protocol_config =
1233                ProtocolConfig::get_for_version(ProtocolVersion::max(), Chain::Unknown);
1234            protocol_config.set_random_beacon_dkg_version_for_testing(1);
1235            Self::new_with_protocol_config(include_observer, protocol_config).await
1236        }
1237
1238        async fn new_with_protocol_config(
1239            include_observer: bool,
1240            protocol_config: ProtocolConfig,
1241        ) -> Self {
1242            let network_config =
1243                sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1244                    .committee_size(NonZeroUsize::new(4).unwrap())
1245                    .with_reference_gas_price(500)
1246                    .build();
1247
1248            let num_validators = network_config.validator_configs.len();
1249            let mut epoch_stores = Vec::new();
1250            let mut randomness_managers = Vec::new();
1251            let (tx_consensus, rx_consensus) = mpsc::channel(100);
1252
1253            for validator in network_config.validator_configs.iter() {
1254                let mut mock_consensus_client = MockConsensusClient::new();
1255                let tx_consensus = tx_consensus.clone();
1256                mock_consensus_client
1257                    .expect_submit()
1258                    .withf(move |transactions: &[ConsensusTransaction], _epoch_store| {
1259                        tx_consensus.try_send(transactions.to_vec()).unwrap();
1260                        true
1261                    })
1262                    .returning(|_, _| {
1263                        Ok((
1264                            Vec::new(),
1265                            with_block_status(BlockStatus::Sequenced(BlockRef::MIN)),
1266                        ))
1267                    });
1268
1269                let state = TestAuthorityBuilder::new()
1270                    .with_protocol_config(protocol_config.clone())
1271                    .with_genesis_and_keypair(
1272                        &network_config.genesis,
1273                        validator.protocol_key_pair(),
1274                    )
1275                    .build()
1276                    .await;
1277                let consensus_adapter = Arc::new(ConsensusAdapter::new(
1278                    Arc::new(mock_consensus_client),
1279                    CheckpointStore::new_for_tests(),
1280                    state.name,
1281                    100_000,
1282                    100_000,
1283                    ConsensusAdapterMetrics::new_test(),
1284                    Arc::new(tokio::sync::Notify::new()),
1285                ));
1286                let epoch_store = state.epoch_store_for_testing();
1287                let randomness_manager = RandomnessManager::try_new(
1288                    Arc::downgrade(&epoch_store),
1289                    Box::new(consensus_adapter.clone()),
1290                    sui_network::randomness::Handle::new_stub(),
1291                    Some(validator.protocol_key_pair()),
1292                    RandomnessRoundReceiverHandle::new_for_testing(),
1293                )
1294                .await
1295                .unwrap();
1296
1297                epoch_stores.push(epoch_store);
1298                randomness_managers.push(randomness_manager);
1299            }
1300
1301            if include_observer {
1302                let observer_epoch_store = epoch_stores[0].clone();
1303                let mut mock_observer_consensus = MockConsensusClient::new();
1304                mock_observer_consensus
1305                    .expect_submit()
1306                    .returning(|_, _| panic!("observer should not submit to consensus"));
1307                let observer_adapter = Arc::new(ConsensusAdapter::new(
1308                    Arc::new(mock_observer_consensus),
1309                    CheckpointStore::new_for_tests(),
1310                    observer_epoch_store.name,
1311                    100_000,
1312                    100_000,
1313                    ConsensusAdapterMetrics::new_test(),
1314                    Arc::new(tokio::sync::Notify::new()),
1315                ));
1316                let observer_manager = RandomnessManager::try_new(
1317                    Arc::downgrade(&observer_epoch_store),
1318                    Box::new(observer_adapter),
1319                    sui_network::randomness::Handle::new_stub(),
1320                    None,
1321                    RandomnessRoundReceiverHandle::new_for_testing(),
1322                )
1323                .await
1324                .unwrap();
1325
1326                epoch_stores.push(observer_epoch_store.into());
1327                randomness_managers.push(observer_manager);
1328            }
1329
1330            Self {
1331                network_config,
1332                epoch_stores,
1333                randomness_managers,
1334                tx_consensus,
1335                rx_consensus,
1336                num_validators,
1337            }
1338        }
1339
1340        fn consensus_adapter(&self, authority: AuthorityName) -> Arc<ConsensusAdapter> {
1341            let mut mock_consensus_client = MockConsensusClient::new();
1342            let tx_consensus = self.tx_consensus.clone();
1343            mock_consensus_client
1344                .expect_submit()
1345                .withf(move |transactions: &[ConsensusTransaction], _epoch_store| {
1346                    tx_consensus.try_send(transactions.to_vec()).unwrap();
1347                    true
1348                })
1349                .returning(|_, _| {
1350                    Ok((
1351                        Vec::new(),
1352                        with_block_status(BlockStatus::Sequenced(BlockRef::MIN)),
1353                    ))
1354                });
1355
1356            Arc::new(ConsensusAdapter::new(
1357                Arc::new(mock_consensus_client),
1358                CheckpointStore::new_for_tests(),
1359                authority,
1360                100_000,
1361                100_000,
1362                ConsensusAdapterMetrics::new_test(),
1363                Arc::new(tokio::sync::Notify::new()),
1364            ))
1365        }
1366
1367        async fn recover_validator_randomness_managers(&mut self) -> usize {
1368            let mut recovered_randomness_managers = Vec::new();
1369            for (i, validator) in self
1370                .network_config
1371                .validator_configs
1372                .iter()
1373                .enumerate()
1374                .take(self.num_validators)
1375            {
1376                let consensus_adapter = self.consensus_adapter(self.epoch_stores[i].name);
1377                recovered_randomness_managers.push(
1378                    RandomnessManager::try_new(
1379                        Arc::downgrade(&self.epoch_stores[i]),
1380                        Box::new(consensus_adapter),
1381                        sui_network::randomness::Handle::new_stub(),
1382                        Some(validator.protocol_key_pair()),
1383                        RandomnessRoundReceiverHandle::new_for_testing(),
1384                    )
1385                    .await
1386                    .unwrap(),
1387                );
1388            }
1389            let recovered_count = recovered_randomness_managers.len();
1390            self.randomness_managers = recovered_randomness_managers;
1391            recovered_count
1392        }
1393
1394        /// Runs start_dkg on all managers and collects the DKG messages from validators.
1395        async fn start_dkg_and_collect_messages(&mut self) -> Vec<VersionedDkgMessage> {
1396            let mut dkg_messages = Vec::new();
1397            for randomness_manager in self.randomness_managers.iter_mut() {
1398                randomness_manager.start_dkg().await.unwrap();
1399            }
1400            for _ in 0..self.num_validators {
1401                let mut dkg_message = self.rx_consensus.recv().await.unwrap();
1402                assert!(dkg_message.len() == 1);
1403                match dkg_message.remove(0).kind {
1404                    ConsensusTransactionKind::RandomnessDkgMessage(_, bytes) => {
1405                        let msg: VersionedDkgMessage = bcs::from_bytes(&bytes)
1406                            .expect("DKG message deserialization should not fail");
1407                        dkg_messages.push(msg);
1408                    }
1409                    _ => panic!("wrong type of message sent"),
1410                }
1411            }
1412            dkg_messages
1413        }
1414
1415        /// Distributes DKG messages to all managers and advances DKG at the given round.
1416        async fn distribute_messages_and_advance(
1417            &mut self,
1418            dkg_messages: &[VersionedDkgMessage],
1419            advance_round: Round,
1420        ) {
1421            let indexed_messages: Vec<_> = dkg_messages.iter().cloned().enumerate().collect();
1422            self.distribute_indexed_messages_and_advance(&indexed_messages, advance_round)
1423                .await;
1424        }
1425
1426        async fn distribute_indexed_messages_and_advance(
1427            &mut self,
1428            dkg_messages: &[(usize, VersionedDkgMessage)],
1429            advance_round: Round,
1430        ) {
1431            for i in 0..self.randomness_managers.len() {
1432                let mut output = ConsensusCommitOutput::new(0);
1433                output.record_consensus_commit_stats(ExecutionIndicesWithStatsV2 {
1434                    index: ExecutionIndices {
1435                        last_committed_round: 0,
1436                        ..Default::default()
1437                    },
1438                    ..Default::default()
1439                });
1440                for (j, dkg_message) in dkg_messages.iter().cloned() {
1441                    self.randomness_managers[i]
1442                        .add_message(&self.epoch_stores[j].name, dkg_message)
1443                        .unwrap();
1444                }
1445                self.randomness_managers[i]
1446                    .advance_dkg(&mut output, advance_round)
1447                    .await
1448                    .unwrap();
1449                let mut batch = self.epoch_stores[i].db_batch_for_test();
1450                output
1451                    .write_to_batch(&self.epoch_stores[i], &mut batch)
1452                    .unwrap();
1453                batch.write().unwrap();
1454            }
1455        }
1456
1457        async fn collect_confirmations(&mut self) -> Vec<VersionedDkgConfirmation> {
1458            let mut dkg_confirmations = Vec::new();
1459            for _ in 0..self.num_validators {
1460                let mut dkg_confirmation = self.rx_consensus.recv().await.unwrap();
1461                assert!(dkg_confirmation.len() == 1);
1462                match dkg_confirmation.remove(0).kind {
1463                    ConsensusTransactionKind::RandomnessDkgConfirmation(_, bytes) => {
1464                        let msg: VersionedDkgConfirmation = bcs::from_bytes(&bytes)
1465                            .expect("DKG confirmation deserialization should not fail");
1466                        dkg_confirmations.push(msg);
1467                    }
1468                    _ => panic!("wrong type of message sent"),
1469                }
1470            }
1471            dkg_confirmations
1472        }
1473
1474        async fn distribute_confirmations_and_advance(
1475            &mut self,
1476            dkg_confirmations: &[VersionedDkgConfirmation],
1477            advance_round: Round,
1478        ) {
1479            for i in 0..self.randomness_managers.len() {
1480                let mut output = ConsensusCommitOutput::new(0);
1481                output.record_consensus_commit_stats(ExecutionIndicesWithStatsV2 {
1482                    index: ExecutionIndices {
1483                        last_committed_round: 1,
1484                        ..Default::default()
1485                    },
1486                    ..Default::default()
1487                });
1488                for (j, dkg_confirmation) in dkg_confirmations.iter().cloned().enumerate() {
1489                    self.randomness_managers[i]
1490                        .add_confirmation(&mut output, &self.epoch_stores[j].name, dkg_confirmation)
1491                        .unwrap();
1492                }
1493                self.randomness_managers[i]
1494                    .advance_dkg(&mut output, advance_round)
1495                    .await
1496                    .unwrap();
1497                let mut batch = self.epoch_stores[i].db_batch_for_test();
1498                output
1499                    .write_to_batch(&self.epoch_stores[i], &mut batch)
1500                    .unwrap();
1501                batch.write().unwrap();
1502            }
1503        }
1504
1505        /// Collects DKG confirmations from validators and distributes them to all managers.
1506        async fn collect_and_distribute_confirmations(&mut self, advance_round: Round) {
1507            let dkg_confirmations = self.collect_confirmations().await;
1508            self.distribute_confirmations_and_advance(&dkg_confirmations, advance_round)
1509                .await;
1510        }
1511    }
1512
1513    #[tokio::test]
1514    async fn test_dkg() {
1515        telemetry_subscribers::init_for_testing();
1516
1517        let mut setup = DkgTestSetup::new(false).await;
1518        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1519        setup
1520            .distribute_messages_and_advance(&dkg_messages, 0)
1521            .await;
1522        setup.collect_and_distribute_confirmations(0).await;
1523
1524        for rm in &setup.randomness_managers {
1525            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1526        }
1527    }
1528
1529    #[tokio::test]
1530    async fn test_dkg_expiration() {
1531        telemetry_subscribers::init_for_testing();
1532
1533        let mut protocol_config =
1534            ProtocolConfig::get_for_version(ProtocolVersion::max(), Chain::Unknown);
1535        protocol_config.set_random_beacon_dkg_version_for_testing(1);
1536        protocol_config.set_allow_dkg_completion_after_timeout_for_testing(false);
1537        let mut setup = DkgTestSetup::new_with_protocol_config(false, protocol_config).await;
1538        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1539
1540        // With late completion disabled, `dkg_status_for_commit_round` passes through the raw
1541        // status; timeout failure is only marked by `advance_dkg`.
1542        assert_eq!(
1543            DkgStatus::Pending,
1544            setup.randomness_managers[0].dkg_status_for_commit_round(u64::MAX)
1545        );
1546
1547        // Pass u64::MAX as round to trigger DKG timeout.
1548        setup
1549            .distribute_messages_and_advance(&dkg_messages, u64::MAX)
1550            .await;
1551
1552        for rm in &setup.randomness_managers {
1553            assert_eq!(DkgStatus::TimedOut, rm.dkg_status());
1554        }
1555
1556        let recovered_count = setup.recover_validator_randomness_managers().await;
1557        assert_eq!(setup.num_validators, recovered_count);
1558
1559        // Verify the failure is durably loaded on restart: RandomnessManagers reconstructed
1560        // from the same epoch stores must report DKG as failed (not pending), exercising the
1561        // `dkg_output_v2` failure-load path in `try_new`.
1562        for rm in &setup.randomness_managers {
1563            assert_eq!(DkgStatus::TimedOut, rm.dkg_status());
1564        }
1565    }
1566
1567    #[tokio::test]
1568    async fn test_dkg_completes_after_timeout() {
1569        telemetry_subscribers::init_for_testing();
1570
1571        let mut setup = DkgTestSetup::new(false).await;
1572        let timeout: Round = setup.epoch_stores[0]
1573            .protocol_config()
1574            .random_beacon_dkg_timeout_round()
1575            .into();
1576        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1577        setup
1578            .distribute_messages_and_advance(&dkg_messages, timeout + 1)
1579            .await;
1580
1581        for rm in &mut setup.randomness_managers {
1582            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1583            assert_eq!(
1584                DkgStatus::TimedOut,
1585                rm.dkg_status_for_commit_round(timeout + 1)
1586            );
1587        }
1588
1589        setup
1590            .collect_and_distribute_confirmations(timeout + 2)
1591            .await;
1592        for rm in &setup.randomness_managers {
1593            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1594        }
1595    }
1596
1597    #[tokio::test]
1598    async fn test_dkg_timeout_then_restart_then_complete() {
1599        telemetry_subscribers::init_for_testing();
1600
1601        let mut setup = DkgTestSetup::new(false).await;
1602        let timeout: Round = setup.epoch_stores[0]
1603            .protocol_config()
1604            .random_beacon_dkg_timeout_round()
1605            .into();
1606        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1607        setup
1608            .distribute_messages_and_advance(&dkg_messages, timeout + 1)
1609            .await;
1610
1611        let recovered_count = setup.recover_validator_randomness_managers().await;
1612        assert_eq!(setup.num_validators, recovered_count);
1613        for rm in &mut setup.randomness_managers {
1614            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1615            assert!(rm.used_messages.initialized());
1616            assert_eq!(
1617                DkgStatus::TimedOut,
1618                rm.dkg_status_for_commit_round(timeout + 1)
1619            );
1620        }
1621
1622        setup
1623            .collect_and_distribute_confirmations(timeout + 2)
1624            .await;
1625        for rm in &setup.randomness_managers {
1626            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1627        }
1628    }
1629
1630    #[tokio::test]
1631    async fn test_dkg_confirmation_resent_after_restart() {
1632        telemetry_subscribers::init_for_testing();
1633
1634        let mut setup = DkgTestSetup::new(false).await;
1635        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1636        setup
1637            .distribute_messages_and_advance(&dkg_messages, 0)
1638            .await;
1639        let original_confirmations = setup.collect_confirmations().await;
1640        assert_eq!(setup.num_validators, original_confirmations.len());
1641
1642        let recovered_count = setup.recover_validator_randomness_managers().await;
1643        assert_eq!(setup.num_validators, recovered_count);
1644        for randomness_manager in &mut setup.randomness_managers {
1645            randomness_manager.start_dkg().await.unwrap();
1646        }
1647
1648        let resent_confirmations = setup.collect_confirmations().await;
1649        assert_eq!(setup.num_validators, resent_confirmations.len());
1650        // The persisted copy must be identical to the originally-sent confirmation.
1651        assert_eq!(original_confirmations, resent_confirmations);
1652        setup
1653            .distribute_confirmations_and_advance(&resent_confirmations, 0)
1654            .await;
1655        for rm in &setup.randomness_managers {
1656            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1657        }
1658    }
1659
1660    #[tokio::test]
1661    async fn test_dkg_status_for_commit_round() {
1662        telemetry_subscribers::init_for_testing();
1663
1664        let mut setup = DkgTestSetup::new(false).await;
1665        let timeout: Round = setup.epoch_stores[0]
1666            .protocol_config()
1667            .random_beacon_dkg_timeout_round()
1668            .into();
1669        let rm = &mut setup.randomness_managers[0];
1670        assert_eq!(DkgStatus::Pending, rm.dkg_status_for_commit_round(timeout));
1671        assert_eq!(
1672            DkgStatus::TimedOut,
1673            rm.dkg_status_for_commit_round(timeout + 1)
1674        );
1675        rm.dkg_output.set(None).unwrap();
1676        assert_eq!(DkgStatus::TimedOut, rm.dkg_status_for_commit_round(0));
1677        // Flag-off passthrough is covered by test_dkg_expiration.
1678    }
1679
1680    #[tokio::test]
1681    async fn test_dkg_recovers_processed_messages_after_restart() {
1682        telemetry_subscribers::init_for_testing();
1683
1684        let mut setup = DkgTestSetup::new(false).await;
1685        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1686        let processed_before_restart = 1;
1687        assert!(processed_before_restart > 0);
1688        assert!(processed_before_restart < setup.num_validators);
1689        let pre_restart_messages: Vec<_> = dkg_messages
1690            .iter()
1691            .cloned()
1692            .enumerate()
1693            .take(processed_before_restart)
1694            .collect();
1695        let post_restart_messages: Vec<_> = dkg_messages
1696            .iter()
1697            .cloned()
1698            .enumerate()
1699            .skip(processed_before_restart)
1700            .collect();
1701
1702        setup
1703            .distribute_indexed_messages_and_advance(&pre_restart_messages, 0)
1704            .await;
1705
1706        for rm in &setup.randomness_managers {
1707            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1708            assert_eq!(processed_before_restart, rm.processed_messages.len());
1709            assert!(!rm.used_messages.initialized());
1710        }
1711
1712        let recovered_count = setup.recover_validator_randomness_managers().await;
1713        assert_eq!(setup.num_validators, recovered_count);
1714        for rm in &setup.randomness_managers {
1715            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1716            assert_eq!(processed_before_restart, rm.processed_messages.len());
1717            assert!(!rm.used_messages.initialized());
1718        }
1719
1720        setup
1721            .distribute_indexed_messages_and_advance(&post_restart_messages, 0)
1722            .await;
1723
1724        for rm in &setup.randomness_managers {
1725            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1726            assert_eq!(setup.num_validators, rm.processed_messages.len());
1727            assert!(rm.used_messages.initialized());
1728        }
1729
1730        setup.collect_and_distribute_confirmations(0).await;
1731
1732        for rm in &setup.randomness_managers {
1733            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1734        }
1735    }
1736
1737    /// Verifies that an Observer completes DKG alongside validators and derives the same
1738    /// shared public key (vss_pk), but without receiving any private key shares.
1739    #[tokio::test]
1740    async fn test_dkg_observer() {
1741        telemetry_subscribers::init_for_testing();
1742
1743        let mut setup = DkgTestSetup::new(true).await;
1744        let observer_idx = setup.randomness_managers.len() - 1;
1745
1746        let dkg_messages = setup.start_dkg_and_collect_messages().await;
1747        setup
1748            .distribute_messages_and_advance(&dkg_messages, 0)
1749            .await;
1750
1751        for rm in &setup.randomness_managers {
1752            assert_eq!(DkgStatus::Pending, rm.dkg_status());
1753        }
1754
1755        setup.collect_and_distribute_confirmations(0).await;
1756
1757        for rm in &setup.randomness_managers {
1758            assert_eq!(DkgStatus::Successful, rm.dkg_status());
1759        }
1760
1761        // Verify the observer derived the same vss_pk as validators, but without shares.
1762        let observer_output = setup.randomness_managers[observer_idx]
1763            .dkg_output()
1764            .expect("observer should have DKG output");
1765        let validator_output = setup.randomness_managers[0]
1766            .dkg_output()
1767            .expect("validator should have DKG output");
1768        assert_eq!(observer_output.vss_pk, validator_output.vss_pk);
1769        assert!(observer_output.shares.is_none());
1770
1771        for rm in &setup.randomness_managers[..setup.num_validators] {
1772            let output = rm.dkg_output().expect("validator should have DKG output");
1773            assert!(output.shares.is_some());
1774        }
1775    }
1776
1777    /// Builds a minimal set of DKG Nodes from a network config's validator key pairs.
1778    fn build_dkg_nodes(
1779        network_config: &sui_swarm_config::network_config::NetworkConfig,
1780    ) -> (nodes::Nodes<EncG>, u16) {
1781        let dkg_nodes: Vec<_> = network_config
1782            .validator_configs
1783            .iter()
1784            .enumerate()
1785            .map(|(i, v)| {
1786                let pk = bls12381::G2Element::from_byte_array(
1787                    v.protocol_key_pair()
1788                        .public()
1789                        .as_bytes()
1790                        .try_into()
1791                        .expect("key length should match"),
1792                )
1793                .expect("should work to convert BLS key to G2Element");
1794                nodes::Node::<EncG> {
1795                    id: i as u16,
1796                    pk: fastcrypto_tbls::ecies_v1::PublicKey::from(pk),
1797                    weight: 1,
1798                }
1799            })
1800            .collect();
1801        let num_nodes = dkg_nodes.len();
1802        // threshold = ceil(num_nodes / 3) works for a minimal committee
1803        let t = num_nodes.div_ceil(3) as u16;
1804        nodes::Nodes::new(dkg_nodes).map(|n| (n, t)).unwrap()
1805    }
1806
1807    #[test]
1808    fn test_dkg_role_try_new_party() {
1809        let network_config =
1810            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1811                .committee_size(NonZeroUsize::new(4).unwrap())
1812                .with_reference_gas_price(500)
1813                .build();
1814
1815        let (nodes, t) = build_dkg_nodes(&network_config);
1816        let random_oracle =
1817            fastcrypto_tbls::random_oracle::RandomOracle::new("test_dkg_role_party");
1818
1819        let role = DkgRole::try_new(
1820            Some(network_config.validator_configs[0].protocol_key_pair()),
1821            nodes,
1822            t,
1823            random_oracle,
1824        );
1825        assert!(role.is_some());
1826        assert!(role.unwrap().is_party());
1827    }
1828
1829    #[tokio::test]
1830    async fn test_randomness_manager_crash_recovery_v1() {
1831        telemetry_subscribers::init_for_testing();
1832
1833        let network_config =
1834            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1835                .committee_size(NonZeroUsize::new(4).unwrap())
1836                .with_reference_gas_price(500)
1837                .build();
1838
1839        let mut protocol_config =
1840            ProtocolConfig::get_for_version(ProtocolVersion::max(), Chain::Unknown);
1841        protocol_config.set_random_beacon_dkg_version_for_testing(1);
1842
1843        let validator = &network_config.validator_configs[0];
1844        let state = TestAuthorityBuilder::new()
1845            .with_protocol_config(protocol_config.clone())
1846            .with_genesis_and_keypair(&network_config.genesis, validator.protocol_key_pair())
1847            .build()
1848            .await;
1849        let epoch_store = state.epoch_store_for_testing();
1850
1851        let dkg_nodes = nodes::Nodes::new(
1852            RandomnessManager::randomness_dkg_info_from_committee(epoch_store.committee())
1853                .into_iter()
1854                .map(|(id, _, pk, stake)| nodes::Node::<bls12381::G2Element> {
1855                    id,
1856                    pk,
1857                    weight: stake.try_into().unwrap(),
1858                })
1859                .collect(),
1860        )
1861        .unwrap();
1862        let threshold = epoch_store
1863            .committee()
1864            .validity_threshold()
1865            .try_into()
1866            .unwrap();
1867        let party_id = epoch_store
1868            .committee()
1869            .authority_index(&epoch_store.name)
1870            .unwrap()
1871            .try_into()
1872            .unwrap();
1873        let expected_dkg_output = mocked_dkg::generate_mocked_output::<
1874            bls12381::G2Element,
1875            bls12381::G2Element,
1876        >(dkg_nodes, threshold, 0, party_id);
1877        let expected_dkg_output_bytes =
1878            bcs::to_bytes(&expected_dkg_output).expect("DKG output serialization should not fail");
1879        let expected_public_key = expected_dkg_output.vss_pk.c0();
1880
1881        let tables = epoch_store.tables();
1882        tables
1883            .dkg_output_v2
1884            .insert(&SINGLETON_KEY, &Some(expected_dkg_output))
1885            .unwrap();
1886        tables
1887            .randomness_next_round
1888            .insert(&SINGLETON_KEY, &RandomnessRound(3))
1889            .unwrap();
1890        tables
1891            .randomness_highest_completed_round
1892            .insert(&SINGLETON_KEY, &RandomnessRound(1))
1893            .unwrap();
1894
1895        let consensus_adapter = Arc::new(ConsensusAdapter::new(
1896            Arc::new(MockConsensusClient::new()),
1897            CheckpointStore::new_for_tests(),
1898            epoch_store.name,
1899            100_000,
1900            100_000,
1901            ConsensusAdapterMetrics::new_test(),
1902            Arc::new(tokio::sync::Notify::new()),
1903        ));
1904        let recovered_receiver_handle = RandomnessRoundReceiverHandle::new_for_testing();
1905        assert!(recovered_receiver_handle.public_key_for_testing().is_none());
1906
1907        let recovered_randomness_manager = RandomnessManager::try_new(
1908            Arc::downgrade(&epoch_store),
1909            Box::new(consensus_adapter.clone()),
1910            sui_network::randomness::Handle::new_stub(),
1911            Some(validator.protocol_key_pair()),
1912            recovered_receiver_handle.clone(),
1913        )
1914        .await
1915        .unwrap();
1916
1917        assert_eq!(
1918            DkgStatus::Successful,
1919            recovered_randomness_manager.dkg_status()
1920        );
1921        let recovered_dkg_output = recovered_randomness_manager
1922            .dkg_output
1923            .get()
1924            .expect("recovered DKG output should be initialized")
1925            .as_ref()
1926            .expect("recovered DKG should be successful");
1927        assert_eq!(
1928            expected_dkg_output_bytes,
1929            bcs::to_bytes(recovered_dkg_output).expect("DKG output serialization should not fail")
1930        );
1931        assert_eq!(
1932            RandomnessRound(3),
1933            recovered_randomness_manager.next_randomness_round
1934        );
1935        assert_eq!(
1936            Some(RandomnessRound(1)),
1937            *recovered_randomness_manager.highest_completed_round.lock()
1938        );
1939        let recovered_public_key = recovered_receiver_handle
1940            .public_key_for_testing()
1941            .expect("public key should be restored on recovery");
1942        assert_eq!(
1943            expected_public_key.to_byte_array(),
1944            recovered_public_key.to_byte_array()
1945        );
1946    }
1947
1948    #[test]
1949    fn test_dkg_role_try_new_observer() {
1950        let network_config =
1951            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
1952                .committee_size(NonZeroUsize::new(4).unwrap())
1953                .with_reference_gas_price(500)
1954                .build();
1955
1956        let (nodes, t) = build_dkg_nodes(&network_config);
1957        let random_oracle =
1958            fastcrypto_tbls::random_oracle::RandomOracle::new("test_dkg_role_observer");
1959
1960        let role = DkgRole::try_new(None, nodes, t, random_oracle);
1961        assert!(role.is_some());
1962        assert!(role.unwrap().is_observer());
1963    }
1964}