Skip to main content

iota_core/epoch/
randomness.rs

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