sui_core/authority/
consensus_quarantine.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::authority::authority_per_epoch_store::{
5    AuthorityEpochTables, EncG, ExecutionIndicesWithStatsV2, LockDetails, LockDetailsWrapper, PkG,
6};
7use crate::authority::transaction_deferral::DeferralKey;
8use crate::checkpoints::BuilderCheckpointSummary;
9use crate::epoch::randomness::SINGLETON_KEY;
10use dashmap::DashMap;
11use fastcrypto_tbls::{dkg_v1, nodes::PartyId};
12use fastcrypto_zkp::bn254::zk_login::{JWK, JwkId};
13use moka::policy::EvictionPolicy;
14use moka::sync::SegmentedCache as MokaCache;
15use mysten_common::ZipDebugEqIteratorExt;
16use mysten_common::fatal;
17use mysten_common::random_util::randomize_cache_capacity_in_tests;
18use parking_lot::Mutex;
19use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque, hash_map};
20use sui_types::authenticator_state::ActiveJwk;
21use sui_types::base_types::{AuthorityName, ObjectRef, SequenceNumber};
22use sui_types::crypto::RandomnessRound;
23use sui_types::error::SuiResult;
24use sui_types::executable_transaction::{
25    TrustedExecutableTransactionWithAliases, VerifiedExecutableTransactionWithAliases,
26};
27use sui_types::execution::ExecutionTimeObservationKey;
28use sui_types::messages_checkpoint::CheckpointSequenceNumber;
29use sui_types::messages_consensus::AuthorityIndex;
30use sui_types::{
31    base_types::{ConsensusObjectSequenceKey, ObjectID},
32    digests::TransactionDigest,
33    messages_consensus::{Round, TimestampMs, VersionedDkgConfirmation},
34    signature::GenericSignature,
35};
36use tracing::debug;
37use typed_store::Map;
38use typed_store::rocks::DBBatch;
39
40use crate::{
41    authority::{
42        authority_per_epoch_store::AuthorityPerEpochStore,
43        shared_object_congestion_tracker::CongestionPerObjectDebt,
44    },
45    checkpoints::{CheckpointHeight, PendingCheckpoint},
46    consensus_handler::SequencedConsensusTransactionKey,
47    epoch::{
48        randomness::{VersionedProcessedMessage, VersionedUsedProcessedMessages},
49        reconfiguration::ReconfigState,
50    },
51};
52
53use super::*;
54
55#[derive(Default)]
56#[allow(clippy::type_complexity)]
57pub(crate) struct ConsensusCommitOutput {
58    // Consensus and reconfig state
59    consensus_round: Round,
60    // Keeps all the processed consensus messages. It also includes transactions that have been dropped and not scheduled
61    // for execution after failing to acquire the required locks.
62    consensus_messages_processed: BTreeSet<SequencedConsensusTransactionKey>,
63    end_of_publish: BTreeSet<AuthorityName>,
64    reconfig_state: Option<ReconfigState>,
65    consensus_commit_stats: Option<ExecutionIndicesWithStatsV2>,
66
67    // transaction scheduling state
68    next_shared_object_versions: Option<HashMap<ConsensusObjectSequenceKey, SequenceNumber>>,
69
70    deferred_txns: Vec<(DeferralKey, Vec<VerifiedExecutableTransactionWithAliases>)>,
71    deleted_deferred_txns: BTreeSet<DeferralKey>,
72
73    // checkpoint state
74    pending_checkpoints: Vec<PendingCheckpoint>,
75
76    // random beacon state
77    next_randomness_round: Option<(RandomnessRound, TimestampMs)>,
78
79    dkg_confirmations: BTreeMap<PartyId, VersionedDkgConfirmation>,
80    dkg_processed_messages: BTreeMap<PartyId, VersionedProcessedMessage>,
81    dkg_used_message: Option<VersionedUsedProcessedMessages>,
82    dkg_output: Option<Option<dkg_v1::Output<PkG, EncG>>>,
83
84    // jwk state
85    pending_jwks: BTreeSet<(AuthorityName, JwkId, JWK)>,
86    active_jwks: BTreeSet<(u64, (JwkId, JWK))>,
87
88    // congestion control state
89    congestion_control_object_debts: Vec<(ObjectID, u64)>,
90    congestion_control_randomness_object_debts: Vec<(ObjectID, u64)>,
91    execution_time_observations: Vec<(
92        AuthorityIndex,
93        u64, /* generation */
94        Vec<(ExecutionTimeObservationKey, Duration)>,
95    )>,
96
97    // Owned object locks acquired post-consensus.
98    owned_object_locks: HashMap<ObjectRef, LockDetails>,
99
100    // True when the checkpoint queue had no pending roots after this commit's flush.
101    // Used by quarantine to determine safe commit boundaries on restart.
102    checkpoint_queue_drained: bool,
103}
104
105impl ConsensusCommitOutput {
106    pub fn new(consensus_round: Round) -> Self {
107        Self {
108            consensus_round,
109            ..Default::default()
110        }
111    }
112
113    pub fn get_deleted_deferred_txn_keys(&self) -> impl Iterator<Item = DeferralKey> + use<'_> {
114        self.deleted_deferred_txns.iter().cloned()
115    }
116
117    pub fn has_deferred_transactions(&self) -> bool {
118        !self.deferred_txns.is_empty()
119    }
120
121    fn get_randomness_last_round_timestamp(&self) -> Option<TimestampMs> {
122        self.next_randomness_round.as_ref().map(|(_, ts)| *ts)
123    }
124
125    fn get_highest_pending_checkpoint_height(&self) -> Option<CheckpointHeight> {
126        self.pending_checkpoints.last().map(|cp| cp.height())
127    }
128
129    fn get_pending_checkpoints(
130        &self,
131        last: Option<CheckpointHeight>,
132    ) -> impl Iterator<Item = &PendingCheckpoint> {
133        self.pending_checkpoints.iter().filter(move |cp| {
134            if let Some(last) = last {
135                cp.height() > last
136            } else {
137                true
138            }
139        })
140    }
141
142    fn pending_checkpoint_exists(&self, index: &CheckpointHeight) -> bool {
143        self.pending_checkpoints
144            .iter()
145            .any(|cp| cp.height() == *index)
146    }
147
148    fn get_round(&self) -> Option<u64> {
149        self.consensus_commit_stats
150            .as_ref()
151            .map(|stats| stats.index.last_committed_round)
152    }
153
154    pub fn insert_end_of_publish(&mut self, authority: AuthorityName) {
155        self.end_of_publish.insert(authority);
156    }
157
158    pub fn insert_execution_time_observation(
159        &mut self,
160        source: AuthorityIndex,
161        generation: u64,
162        estimates: Vec<(ExecutionTimeObservationKey, Duration)>,
163    ) {
164        self.execution_time_observations
165            .push((source, generation, estimates));
166    }
167
168    pub(crate) fn record_consensus_commit_stats(&mut self, stats: ExecutionIndicesWithStatsV2) {
169        self.consensus_commit_stats = Some(stats);
170    }
171
172    // in testing code we often need to write to the db outside of a consensus commit
173    pub(crate) fn set_default_commit_stats_for_testing(&mut self) {
174        self.record_consensus_commit_stats(Default::default());
175    }
176
177    pub fn store_reconfig_state(&mut self, state: ReconfigState) {
178        self.reconfig_state = Some(state);
179    }
180
181    pub fn record_consensus_message_processed(&mut self, key: SequencedConsensusTransactionKey) {
182        self.consensus_messages_processed.insert(key);
183    }
184
185    pub fn get_consensus_messages_processed(
186        &self,
187    ) -> impl Iterator<Item = &SequencedConsensusTransactionKey> {
188        self.consensus_messages_processed.iter()
189    }
190
191    pub fn set_next_shared_object_versions(
192        &mut self,
193        next_versions: HashMap<ConsensusObjectSequenceKey, SequenceNumber>,
194    ) {
195        assert!(self.next_shared_object_versions.is_none());
196        self.next_shared_object_versions = Some(next_versions);
197    }
198
199    pub fn defer_transactions(
200        &mut self,
201        key: DeferralKey,
202        transactions: Vec<VerifiedExecutableTransactionWithAliases>,
203    ) {
204        self.deferred_txns.push((key, transactions));
205    }
206
207    pub fn delete_loaded_deferred_transactions(&mut self, deferral_keys: &[DeferralKey]) {
208        self.deleted_deferred_txns
209            .extend(deferral_keys.iter().cloned());
210    }
211
212    pub fn insert_pending_checkpoint(&mut self, checkpoint: PendingCheckpoint) {
213        self.pending_checkpoints.push(checkpoint);
214    }
215
216    pub fn reserve_next_randomness_round(
217        &mut self,
218        next_randomness_round: RandomnessRound,
219        commit_timestamp: TimestampMs,
220    ) {
221        assert!(self.next_randomness_round.is_none());
222        self.next_randomness_round = Some((next_randomness_round, commit_timestamp));
223    }
224
225    pub fn insert_dkg_confirmation(&mut self, conf: VersionedDkgConfirmation) {
226        self.dkg_confirmations.insert(conf.sender(), conf);
227    }
228
229    pub fn insert_dkg_processed_message(&mut self, message: VersionedProcessedMessage) {
230        self.dkg_processed_messages
231            .insert(message.sender(), message);
232    }
233
234    pub fn insert_dkg_used_messages(&mut self, used_messages: VersionedUsedProcessedMessages) {
235        self.dkg_used_message = Some(used_messages);
236    }
237
238    pub fn set_dkg_output(&mut self, output: Option<dkg_v1::Output<PkG, EncG>>) {
239        self.dkg_output = Some(output);
240    }
241
242    pub fn insert_pending_jwk(&mut self, authority: AuthorityName, id: JwkId, jwk: JWK) {
243        self.pending_jwks.insert((authority, id, jwk));
244    }
245
246    pub fn insert_active_jwk(&mut self, round: u64, key: (JwkId, JWK)) {
247        self.active_jwks.insert((round, key));
248    }
249
250    pub fn set_congestion_control_object_debts(&mut self, object_debts: Vec<(ObjectID, u64)>) {
251        self.congestion_control_object_debts = object_debts;
252    }
253
254    pub fn set_congestion_control_randomness_object_debts(
255        &mut self,
256        object_debts: Vec<(ObjectID, u64)>,
257    ) {
258        self.congestion_control_randomness_object_debts = object_debts;
259    }
260
261    pub fn set_checkpoint_queue_drained(&mut self, drained: bool) {
262        self.checkpoint_queue_drained = drained;
263    }
264
265    pub fn set_owned_object_locks(&mut self, locks: HashMap<ObjectRef, LockDetails>) {
266        assert!(self.owned_object_locks.is_empty());
267        self.owned_object_locks = locks;
268    }
269
270    pub fn write_to_batch(
271        self,
272        epoch_store: &AuthorityPerEpochStore,
273        batch: &mut DBBatch,
274    ) -> SuiResult {
275        let tables = epoch_store.tables()?;
276        batch.insert_batch(
277            &tables.consensus_message_processed,
278            self.consensus_messages_processed
279                .iter()
280                .map(|key| (key, true)),
281        )?;
282
283        batch.insert_batch(
284            &tables.end_of_publish,
285            self.end_of_publish.iter().map(|authority| (authority, ())),
286        )?;
287
288        if let Some(reconfig_state) = &self.reconfig_state {
289            batch.insert_batch(
290                &tables.reconfig_state,
291                [(RECONFIG_STATE_INDEX, reconfig_state)],
292            )?;
293        }
294
295        let consensus_commit_stats = self
296            .consensus_commit_stats
297            .expect("consensus_commit_stats must be set");
298        let round = consensus_commit_stats.index.last_committed_round;
299
300        batch.insert_batch(
301            &tables.last_consensus_stats_v2,
302            [(LAST_CONSENSUS_STATS_ADDR, consensus_commit_stats)],
303        )?;
304
305        if let Some(next_versions) = self.next_shared_object_versions {
306            batch.insert_batch(&tables.next_shared_object_versions_v2, next_versions)?;
307        }
308
309        if !self.owned_object_locks.is_empty() {
310            batch.insert_batch(
311                &tables.owned_object_locked_transactions,
312                self.owned_object_locks
313                    .into_iter()
314                    .map(|(obj_ref, lock)| (obj_ref, LockDetailsWrapper::from(lock))),
315            )?;
316        }
317
318        batch.delete_batch(
319            &tables.deferred_transactions_with_aliases_v3,
320            &self.deleted_deferred_txns,
321        )?;
322
323        batch.insert_batch(
324            &tables.deferred_transactions_with_aliases_v3,
325            self.deferred_txns.into_iter().map(|(key, txs)| {
326                (
327                    key,
328                    txs.into_iter()
329                        .map(|tx| {
330                            let tx: TrustedExecutableTransactionWithAliases = tx.serializable();
331                            tx
332                        })
333                        .collect::<Vec<_>>(),
334                )
335            }),
336        )?;
337
338        if let Some((round, commit_timestamp)) = self.next_randomness_round {
339            batch.insert_batch(&tables.randomness_next_round, [(SINGLETON_KEY, round)])?;
340            batch.insert_batch(
341                &tables.randomness_last_round_timestamp,
342                [(SINGLETON_KEY, commit_timestamp)],
343            )?;
344        }
345
346        batch.insert_batch(&tables.dkg_confirmations_v2, self.dkg_confirmations)?;
347        batch.insert_batch(
348            &tables.dkg_processed_messages_v2,
349            self.dkg_processed_messages,
350        )?;
351        batch.insert_batch(
352            &tables.dkg_used_messages_v2,
353            // using Option as iter
354            self.dkg_used_message
355                .into_iter()
356                .map(|used_msgs| (SINGLETON_KEY, used_msgs)),
357        )?;
358        if let Some(output) = self.dkg_output {
359            batch.insert_batch(&tables.dkg_output_v2, [(SINGLETON_KEY, output)])?;
360        }
361
362        batch.insert_batch(
363            &tables.pending_jwks,
364            self.pending_jwks.into_iter().map(|j| (j, ())),
365        )?;
366        batch.insert_batch(
367            &tables.active_jwks,
368            self.active_jwks.into_iter().map(|j| {
369                // TODO: we don't need to store the round in this map if it is invariant
370                assert_eq!(j.0, round);
371                (j, ())
372            }),
373        )?;
374
375        batch.insert_batch(
376            &tables.congestion_control_object_debts,
377            self.congestion_control_object_debts
378                .into_iter()
379                .map(|(object_id, debt)| {
380                    (
381                        object_id,
382                        CongestionPerObjectDebt::new(self.consensus_round, debt),
383                    )
384                }),
385        )?;
386        batch.insert_batch(
387            &tables.congestion_control_randomness_object_debts,
388            self.congestion_control_randomness_object_debts
389                .into_iter()
390                .map(|(object_id, debt)| {
391                    (
392                        object_id,
393                        CongestionPerObjectDebt::new(self.consensus_round, debt),
394                    )
395                }),
396        )?;
397
398        batch.insert_batch(
399            &tables.execution_time_observations,
400            self.execution_time_observations
401                .into_iter()
402                .map(|(authority, generation, estimates)| ((generation, authority), estimates)),
403        )?;
404
405        Ok(())
406    }
407}
408
409/// ConsensusOutputCache holds outputs of consensus processing that do not need to be committed to disk.
410/// Data quarantining guarantees that all of this data will be used (e.g. for building checkpoints)
411/// before the consensus commit from which it originated is marked as processed. Therefore we can rely
412/// on replay of consensus commits to recover this data.
413pub(crate) struct ConsensusOutputCache {
414    // deferred transactions is only used by consensus handler so there should never be lock contention
415    // - hence no need for a DashMap.
416    pub(crate) deferred_transactions:
417        Mutex<BTreeMap<DeferralKey, Vec<VerifiedExecutableTransactionWithAliases>>>,
418
419    // user_signatures_for_checkpoints is written to by consensus handler and read from by checkpoint builder
420    // The critical sections are small in both cases so a DashMap is probably not helpful.
421    #[allow(clippy::type_complexity)]
422    pub(crate) user_signatures_for_checkpoints:
423        Mutex<HashMap<TransactionDigest, Vec<(GenericSignature, Option<SequenceNumber>)>>>,
424
425    executed_in_epoch: RwLock<DashMap<TransactionDigest, ()>>,
426    executed_in_epoch_cache: MokaCache<TransactionDigest, ()>,
427}
428
429impl ConsensusOutputCache {
430    pub(crate) fn new(tables: &AuthorityEpochTables) -> Self {
431        let deferred_transactions = tables
432            .get_all_deferred_transactions()
433            .expect("load deferred transactions cannot fail");
434
435        let executed_in_epoch_cache_capacity = 50_000;
436
437        Self {
438            deferred_transactions: Mutex::new(deferred_transactions),
439            user_signatures_for_checkpoints: Default::default(),
440            executed_in_epoch: RwLock::new(DashMap::with_shard_amount(2048)),
441            executed_in_epoch_cache: MokaCache::builder(8)
442                // most queries should be for recent transactions
443                .max_capacity(randomize_cache_capacity_in_tests(
444                    executed_in_epoch_cache_capacity,
445                ))
446                .eviction_policy(EvictionPolicy::lru())
447                .build(),
448        }
449    }
450
451    pub fn executed_in_current_epoch(&self, digest: &TransactionDigest) -> bool {
452        self.executed_in_epoch
453            .read()
454            .contains_key(digest) ||
455            // we use get instead of contains key to mark the entry as read
456            self.executed_in_epoch_cache.get(digest).is_some()
457    }
458
459    // Called by execution
460    pub fn insert_executed_in_epoch(&self, tx_digest: TransactionDigest) {
461        assert!(
462            self.executed_in_epoch
463                .read()
464                .insert(tx_digest, ())
465                .is_none(),
466            "transaction already executed"
467        );
468        self.executed_in_epoch_cache.insert(tx_digest, ());
469    }
470
471    // CheckpointExecutor calls this (indirectly) in order to prune the in-memory cache of executed
472    // transactions. By the time this is called, the transaction digests will have been committed to
473    // the `executed_transactions_to_checkpoint` table.
474    pub fn remove_executed_in_epoch(&self, tx_digests: &[TransactionDigest]) {
475        let executed_in_epoch = self.executed_in_epoch.read();
476        for tx_digest in tx_digests {
477            executed_in_epoch.remove(tx_digest);
478        }
479    }
480}
481
482/// ConsensusOutputQuarantine holds outputs of consensus processing in memory until the checkpoints
483/// for the commit have been certified.
484pub(crate) struct ConsensusOutputQuarantine {
485    // Output from consensus handler
486    output_queue: VecDeque<ConsensusCommitOutput>,
487
488    // Highest known certified checkpoint sequence number
489    highest_executed_checkpoint: CheckpointSequenceNumber,
490
491    // Checkpoint Builder output
492    builder_checkpoint_summary: BTreeMap<CheckpointSequenceNumber, BuilderCheckpointSummary>,
493
494    // Any un-committed next versions are stored here.
495    shared_object_next_versions: RefCountedHashMap<ConsensusObjectSequenceKey, SequenceNumber>,
496
497    // The most recent congestion control debts for objects. Uses a ref-count to track
498    // which objects still exist in some element of output_queue.
499    congestion_control_randomness_object_debts:
500        RefCountedHashMap<ObjectID, CongestionPerObjectDebt>,
501    congestion_control_object_debts: RefCountedHashMap<ObjectID, CongestionPerObjectDebt>,
502
503    processed_consensus_messages: RefCountedHashMap<SequencedConsensusTransactionKey, ()>,
504
505    // Owned object locks acquired post-consensus.
506    owned_object_locks: HashMap<ObjectRef, LockDetails>,
507
508    metrics: Arc<EpochMetrics>,
509}
510
511impl ConsensusOutputQuarantine {
512    pub(super) fn new(
513        highest_executed_checkpoint: CheckpointSequenceNumber,
514        authority_metrics: Arc<EpochMetrics>,
515    ) -> Self {
516        Self {
517            highest_executed_checkpoint,
518
519            output_queue: VecDeque::new(),
520            builder_checkpoint_summary: BTreeMap::new(),
521            shared_object_next_versions: RefCountedHashMap::new(),
522            processed_consensus_messages: RefCountedHashMap::new(),
523            congestion_control_randomness_object_debts: RefCountedHashMap::new(),
524            congestion_control_object_debts: RefCountedHashMap::new(),
525            owned_object_locks: HashMap::new(),
526            metrics: authority_metrics,
527        }
528    }
529}
530
531// Write methods - all methods in this block insert new data into the quarantine.
532// There are only two sources! ConsensusHandler and CheckpointBuilder.
533impl ConsensusOutputQuarantine {
534    // Push all data gathered from a consensus commit into the quarantine.
535    pub(crate) fn push_consensus_output(
536        &mut self,
537        output: ConsensusCommitOutput,
538        epoch_store: &AuthorityPerEpochStore,
539    ) -> SuiResult {
540        self.insert_shared_object_next_versions(&output);
541        self.insert_congestion_control_debts(&output);
542        self.insert_processed_consensus_messages(&output);
543        self.insert_owned_object_locks(&output);
544        self.output_queue.push_back(output);
545
546        self.metrics
547            .consensus_quarantine_queue_size
548            .set(self.output_queue.len() as i64);
549
550        // we may already have observed the certified checkpoint for this round, if state sync is running
551        // ahead of consensus, so there may be data to commit right away.
552        self.commit(epoch_store)
553    }
554
555    // Record a newly built checkpoint.
556    pub(super) fn insert_builder_summary(
557        &mut self,
558        sequence_number: CheckpointSequenceNumber,
559        summary: BuilderCheckpointSummary,
560    ) {
561        debug!(?sequence_number, "inserting builder summary {:?}", summary);
562        self.builder_checkpoint_summary
563            .insert(sequence_number, summary);
564    }
565}
566
567// Commit methods.
568impl ConsensusOutputQuarantine {
569    /// Update the highest executed checkpoint and commit any data which is now
570    /// below the watermark.
571    pub(super) fn update_highest_executed_checkpoint(
572        &mut self,
573        checkpoint: CheckpointSequenceNumber,
574        epoch_store: &AuthorityPerEpochStore,
575        batch: &mut DBBatch,
576    ) -> SuiResult {
577        self.highest_executed_checkpoint = checkpoint;
578        self.commit_with_batch(epoch_store, batch)
579    }
580
581    pub(super) fn commit(&mut self, epoch_store: &AuthorityPerEpochStore) -> SuiResult {
582        let mut batch = epoch_store.db_batch()?;
583        self.commit_with_batch(epoch_store, &mut batch)?;
584        batch.write()?;
585        Ok(())
586    }
587
588    /// Commit all data below the watermark.
589    fn commit_with_batch(
590        &mut self,
591        epoch_store: &AuthorityPerEpochStore,
592        batch: &mut DBBatch,
593    ) -> SuiResult {
594        // The commit algorithm is simple:
595        // 1. First commit all checkpoint builder state which is below the watermark.
596        // 2. Determine the consensus commit height that corresponds to the highest committed
597        //    checkpoint.
598        // 3. Commit all consensus output at that height or below.
599
600        let tables = epoch_store.tables()?;
601
602        let mut highest_committed_height = None;
603
604        while self
605            .builder_checkpoint_summary
606            .first_key_value()
607            .map(|(seq, _)| *seq <= self.highest_executed_checkpoint)
608            == Some(true)
609        {
610            let (seq, builder_summary) = self.builder_checkpoint_summary.pop_first().unwrap();
611
612            batch.insert_batch(
613                &tables.builder_checkpoint_summary_v2,
614                [(seq, &builder_summary)],
615            )?;
616
617            let checkpoint_height = builder_summary
618                .checkpoint_height
619                .expect("non-genesis checkpoint must have height");
620            if let Some(highest) = highest_committed_height {
621                assert!(
622                    checkpoint_height >= highest,
623                    "current checkpoint height {} must be no less than highest committed height {}",
624                    checkpoint_height,
625                    highest
626                );
627            }
628
629            highest_committed_height = Some(checkpoint_height);
630        }
631
632        let Some(highest_committed_height) = highest_committed_height else {
633            return Ok(());
634        };
635
636        // Only commit outputs up to the last one where the checkpoint queue
637        // was fully drained (no pending roots). If the queue is empty after an
638        // output, there are no roots that could be lost on restart. Any outputs
639        // after the last drain point stay in the quarantine and get full-replayed
640        // on restart with correct root reconstruction.
641        let mut last_drain_idx = None;
642        for (i, output) in self.output_queue.iter().enumerate() {
643            let stats = output
644                .consensus_commit_stats
645                .as_ref()
646                .expect("consensus_commit_stats must be set");
647            if stats.height > highest_committed_height {
648                break;
649            }
650            if output.checkpoint_queue_drained {
651                last_drain_idx = Some(i);
652            }
653        }
654        if let Some(idx) = last_drain_idx {
655            for _ in 0..=idx {
656                let output = self.output_queue.pop_front().unwrap();
657                self.remove_shared_object_next_versions(&output);
658                self.remove_processed_consensus_messages(&output);
659                self.remove_congestion_control_debts(&output);
660                self.remove_owned_object_locks(&output);
661                output.write_to_batch(epoch_store, batch)?;
662            }
663        }
664
665        self.metrics
666            .consensus_quarantine_queue_size
667            .set(self.output_queue.len() as i64);
668
669        Ok(())
670    }
671}
672
673impl ConsensusOutputQuarantine {
674    fn insert_shared_object_next_versions(&mut self, output: &ConsensusCommitOutput) {
675        if let Some(next_versions) = output.next_shared_object_versions.as_ref() {
676            for (object_id, next_version) in next_versions {
677                self.shared_object_next_versions
678                    .insert(*object_id, *next_version);
679            }
680        }
681    }
682
683    fn insert_congestion_control_debts(&mut self, output: &ConsensusCommitOutput) {
684        let current_round = output.consensus_round;
685
686        for (object_id, debt) in output.congestion_control_object_debts.iter() {
687            self.congestion_control_object_debts.insert(
688                *object_id,
689                CongestionPerObjectDebt::new(current_round, *debt),
690            );
691        }
692
693        for (object_id, debt) in output.congestion_control_randomness_object_debts.iter() {
694            self.congestion_control_randomness_object_debts.insert(
695                *object_id,
696                CongestionPerObjectDebt::new(current_round, *debt),
697            );
698        }
699    }
700
701    fn remove_congestion_control_debts(&mut self, output: &ConsensusCommitOutput) {
702        for (object_id, _) in output.congestion_control_object_debts.iter() {
703            self.congestion_control_object_debts.remove(object_id);
704        }
705        for (object_id, _) in output.congestion_control_randomness_object_debts.iter() {
706            self.congestion_control_randomness_object_debts
707                .remove(object_id);
708        }
709    }
710
711    fn insert_processed_consensus_messages(&mut self, output: &ConsensusCommitOutput) {
712        for tx_key in output.consensus_messages_processed.iter() {
713            self.processed_consensus_messages.insert(tx_key.clone(), ());
714        }
715    }
716
717    fn remove_processed_consensus_messages(&mut self, output: &ConsensusCommitOutput) {
718        for tx_key in output.consensus_messages_processed.iter() {
719            self.processed_consensus_messages.remove(tx_key);
720        }
721    }
722
723    fn remove_shared_object_next_versions(&mut self, output: &ConsensusCommitOutput) {
724        if let Some(next_versions) = output.next_shared_object_versions.as_ref() {
725            for object_id in next_versions.keys() {
726                if !self.shared_object_next_versions.remove(object_id) {
727                    fatal!(
728                        "Shared object next version not found in quarantine: {:?}",
729                        object_id
730                    );
731                }
732            }
733        }
734    }
735
736    fn insert_owned_object_locks(&mut self, output: &ConsensusCommitOutput) {
737        for (obj_ref, lock) in &output.owned_object_locks {
738            self.owned_object_locks.insert(*obj_ref, *lock);
739        }
740    }
741
742    fn remove_owned_object_locks(&mut self, output: &ConsensusCommitOutput) {
743        for obj_ref in output.owned_object_locks.keys() {
744            self.owned_object_locks.remove(obj_ref);
745        }
746    }
747}
748
749// Read methods - all methods in this block return data from the quarantine which would otherwise
750// be found in the database.
751impl ConsensusOutputQuarantine {
752    pub(super) fn last_built_summary(&self) -> Option<&BuilderCheckpointSummary> {
753        self.builder_checkpoint_summary.values().last()
754    }
755
756    pub(super) fn get_built_summary(
757        &self,
758        sequence: CheckpointSequenceNumber,
759    ) -> Option<&BuilderCheckpointSummary> {
760        self.builder_checkpoint_summary.get(&sequence)
761    }
762
763    pub(super) fn is_consensus_message_processed(
764        &self,
765        key: &SequencedConsensusTransactionKey,
766    ) -> bool {
767        self.processed_consensus_messages.contains_key(key)
768    }
769
770    pub(super) fn is_empty(&self) -> bool {
771        self.output_queue.is_empty()
772    }
773
774    pub(super) fn get_next_shared_object_versions(
775        &self,
776        tables: &AuthorityEpochTables,
777        objects_to_init: &[ConsensusObjectSequenceKey],
778    ) -> SuiResult<Vec<Option<SequenceNumber>>> {
779        Ok(do_fallback_lookup(
780            objects_to_init,
781            |object_key| {
782                if let Some(next_version) = self.shared_object_next_versions.get(object_key) {
783                    CacheResult::Hit(Some(*next_version))
784                } else {
785                    CacheResult::Miss
786                }
787            },
788            |object_keys| {
789                tables
790                    .next_shared_object_versions_v2
791                    .multi_get(object_keys)
792                    .expect("db error")
793            },
794        ))
795    }
796
797    /// Gets owned object locks, checking quarantine first then falling back to DB.
798    /// After crash recovery, quarantine is empty so we naturally fall back to DB.
799    pub(super) fn get_owned_object_locks(
800        &self,
801        tables: &AuthorityEpochTables,
802        obj_refs: &[ObjectRef],
803    ) -> SuiResult<Vec<Option<LockDetails>>> {
804        Ok(do_fallback_lookup(
805            obj_refs,
806            |obj_ref| {
807                if let Some(lock) = self.owned_object_locks.get(obj_ref) {
808                    CacheResult::Hit(Some(*lock))
809                } else {
810                    CacheResult::Miss
811                }
812            },
813            |obj_refs| {
814                tables
815                    .multi_get_locked_transactions(obj_refs)
816                    .expect("db error")
817            },
818        ))
819    }
820
821    pub(super) fn get_highest_pending_checkpoint_height(&self) -> Option<CheckpointHeight> {
822        self.output_queue
823            .back()
824            .and_then(|output| output.get_highest_pending_checkpoint_height())
825    }
826
827    pub(super) fn get_pending_checkpoints(
828        &self,
829        last: Option<CheckpointHeight>,
830    ) -> Vec<(CheckpointHeight, PendingCheckpoint)> {
831        let mut checkpoints = Vec::new();
832        for output in &self.output_queue {
833            checkpoints.extend(
834                output
835                    .get_pending_checkpoints(last)
836                    .map(|cp| (cp.height(), cp.clone())),
837            );
838        }
839        if cfg!(debug_assertions) {
840            let mut prev = None;
841            for (height, _) in &checkpoints {
842                if let Some(prev) = prev {
843                    assert!(prev < *height);
844                }
845                prev = Some(*height);
846            }
847        }
848        checkpoints
849    }
850
851    pub(super) fn pending_checkpoint_exists(&self, index: &CheckpointHeight) -> bool {
852        self.output_queue
853            .iter()
854            .any(|output| output.pending_checkpoint_exists(index))
855    }
856
857    pub(super) fn get_new_jwks(
858        &self,
859        epoch_store: &AuthorityPerEpochStore,
860        round: u64,
861    ) -> SuiResult<Vec<ActiveJwk>> {
862        let epoch = epoch_store.epoch();
863
864        // Check if the requested round is in memory
865        for output in self.output_queue.iter().rev() {
866            // unwrap safe because output will always have last consensus stats set before being added
867            // to the quarantine
868            let output_round = output.get_round().unwrap();
869            if round == output_round {
870                return Ok(output
871                    .active_jwks
872                    .iter()
873                    .map(|(_, (jwk_id, jwk))| ActiveJwk {
874                        jwk_id: jwk_id.clone(),
875                        jwk: jwk.clone(),
876                        epoch,
877                    })
878                    .collect());
879            }
880        }
881
882        // Fall back to reading from database
883        let empty_jwk_id = JwkId::new(String::new(), String::new());
884        let empty_jwk = JWK {
885            kty: String::new(),
886            e: String::new(),
887            n: String::new(),
888            alg: String::new(),
889        };
890
891        let start = (round, (empty_jwk_id.clone(), empty_jwk.clone()));
892        let end = (round + 1, (empty_jwk_id, empty_jwk));
893
894        Ok(epoch_store
895            .tables()?
896            .active_jwks
897            .safe_iter_with_bounds(Some(start), Some(end))
898            .map_ok(|((r, (jwk_id, jwk)), _)| {
899                debug_assert!(round == r);
900                ActiveJwk { jwk_id, jwk, epoch }
901            })
902            .collect::<Result<Vec<_>, _>>()?)
903    }
904
905    pub(super) fn get_randomness_last_round_timestamp(&self) -> Option<TimestampMs> {
906        self.output_queue
907            .iter()
908            .rev()
909            .filter_map(|output| output.get_randomness_last_round_timestamp())
910            .next()
911    }
912
913    pub(crate) fn load_initial_object_debts(
914        &self,
915        epoch_store: &AuthorityPerEpochStore,
916        current_round: Round,
917        for_randomness: bool,
918        transactions: &[VerifiedExecutableTransactionWithAliases],
919    ) -> SuiResult<impl IntoIterator<Item = (ObjectID, u64)>> {
920        let protocol_config = epoch_store.protocol_config();
921        let tables = epoch_store.tables()?;
922        let default_per_commit_budget = protocol_config
923            .max_accumulated_txn_cost_per_object_in_mysticeti_commit_as_option()
924            .unwrap_or(0);
925        let (hash_table, db_table, per_commit_budget) = if for_randomness {
926            (
927                &self.congestion_control_randomness_object_debts,
928                &tables.congestion_control_randomness_object_debts,
929                protocol_config
930                    .max_accumulated_randomness_txn_cost_per_object_in_mysticeti_commit_as_option()
931                    .unwrap_or(default_per_commit_budget),
932            )
933        } else {
934            (
935                &self.congestion_control_object_debts,
936                &tables.congestion_control_object_debts,
937                default_per_commit_budget,
938            )
939        };
940        let mut shared_input_object_ids: Vec<_> = transactions
941            .iter()
942            .flat_map(|tx| tx.tx().shared_input_objects().map(|obj| obj.id))
943            .collect();
944        shared_input_object_ids.sort();
945        shared_input_object_ids.dedup();
946
947        let results = do_fallback_lookup(
948            &shared_input_object_ids,
949            |object_id| {
950                if let Some(debt) = hash_table.get(object_id) {
951                    CacheResult::Hit(Some(debt.into_v1()))
952                } else {
953                    CacheResult::Miss
954                }
955            },
956            |object_ids| {
957                db_table
958                    .multi_get(object_ids)
959                    .expect("db error")
960                    .into_iter()
961                    .map(|debt| debt.map(|debt| debt.into_v1()))
962                    .collect()
963            },
964        );
965
966        Ok(results
967            .into_iter()
968            .zip_debug_eq(shared_input_object_ids)
969            .filter_map(|(debt, object_id)| debt.map(|debt| (debt, object_id)))
970            .map(move |((round, debt), object_id)| {
971                // Stored debts already account for the budget of the round in which
972                // they were accumulated. Application of budget from future rounds to
973                // the debt is handled here.
974                assert!(current_round > round);
975                let num_rounds = current_round - round - 1;
976                let debt = debt.saturating_sub(per_commit_budget * num_rounds);
977                (object_id, debt)
978            }))
979    }
980}
981
982// A wrapper around HashMap that uses refcounts to keep entries alive until
983// they are no longer needed.
984//
985// If there are N inserts for the same key, the key will not be removed until
986// there are N removes.
987//
988// It is intended to track the *latest* value for a given key, so duplicate
989// inserts are intended to overwrite any prior value.
990#[derive(Debug, Default)]
991struct RefCountedHashMap<K, V> {
992    map: HashMap<K, (usize, V)>,
993}
994
995impl<K, V> RefCountedHashMap<K, V>
996where
997    K: Clone + Eq + std::hash::Hash,
998{
999    pub fn new() -> Self {
1000        Self {
1001            map: HashMap::new(),
1002        }
1003    }
1004
1005    pub fn insert(&mut self, key: K, value: V) {
1006        let entry = self.map.entry(key);
1007        match entry {
1008            hash_map::Entry::Occupied(mut entry) => {
1009                let (ref_count, v) = entry.get_mut();
1010                *ref_count += 1;
1011                *v = value;
1012            }
1013            hash_map::Entry::Vacant(entry) => {
1014                entry.insert((1, value));
1015            }
1016        }
1017    }
1018
1019    // Returns true if the key was present, false otherwise.
1020    // Note that the key may not be removed if present, as it may have a refcount > 1.
1021    pub fn remove(&mut self, key: &K) -> bool {
1022        let entry = self.map.entry(key.clone());
1023        match entry {
1024            hash_map::Entry::Occupied(mut entry) => {
1025                let (ref_count, _) = entry.get_mut();
1026                *ref_count -= 1;
1027                if *ref_count == 0 {
1028                    entry.remove();
1029                }
1030                true
1031            }
1032            hash_map::Entry::Vacant(_) => false,
1033        }
1034    }
1035
1036    pub fn get(&self, key: &K) -> Option<&V> {
1037        self.map.get(key).map(|(_, v)| v)
1038    }
1039
1040    pub fn contains_key(&self, key: &K) -> bool {
1041        self.map.contains_key(key)
1042    }
1043}
1044
1045#[cfg(test)]
1046impl ConsensusOutputQuarantine {
1047    fn output_queue_len_for_testing(&self) -> usize {
1048        self.output_queue.len()
1049    }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    use super::*;
1055    use crate::authority::test_authority_builder::TestAuthorityBuilder;
1056    use sui_types::base_types::ExecutionDigests;
1057    use sui_types::gas::GasCostSummary;
1058    use sui_types::messages_checkpoint::CheckpointContents;
1059
1060    fn make_output(height: u64, round: u64, drained: bool) -> ConsensusCommitOutput {
1061        let mut output = ConsensusCommitOutput::new(round);
1062        output.record_consensus_commit_stats(ExecutionIndicesWithStatsV2 {
1063            height,
1064            ..Default::default()
1065        });
1066        output.set_checkpoint_queue_drained(drained);
1067        output
1068    }
1069
1070    fn make_builder_summary(
1071        seq: CheckpointSequenceNumber,
1072        height: CheckpointHeight,
1073        protocol_config: &ProtocolConfig,
1074    ) -> BuilderCheckpointSummary {
1075        let contents =
1076            CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
1077        let summary = CheckpointSummary::new(
1078            protocol_config,
1079            0,
1080            seq,
1081            0,
1082            &contents,
1083            None,
1084            GasCostSummary::default(),
1085            None,
1086            0,
1087            vec![],
1088            vec![],
1089        );
1090        BuilderCheckpointSummary {
1091            summary,
1092            checkpoint_height: Some(height),
1093            position_in_commit: 0,
1094        }
1095    }
1096
1097    #[tokio::test]
1098    async fn test_drain_boundary_prevents_premature_commit() {
1099        let state = TestAuthorityBuilder::new().build().await;
1100        let epoch_store = state.epoch_store_for_testing();
1101
1102        let metrics = epoch_store.metrics.clone();
1103        let mut quarantine = ConsensusOutputQuarantine::new(0, metrics);
1104
1105        // Output C: height=4, not drained
1106        let c = make_output(4, 1, false);
1107        quarantine.push_consensus_output(c, &epoch_store).unwrap();
1108
1109        // Output C2: height=5, drained
1110        let c2 = make_output(5, 2, true);
1111        quarantine.push_consensus_output(c2, &epoch_store).unwrap();
1112
1113        assert_eq!(quarantine.output_queue_len_for_testing(), 2);
1114
1115        // Insert builder summaries for checkpoints 1-4 with checkpoint_height = seq
1116        let pc = epoch_store.protocol_config();
1117        for seq in 1..=4 {
1118            let summary = make_builder_summary(seq, seq, pc);
1119            quarantine.insert_builder_summary(seq, summary);
1120        }
1121
1122        // Certify up to checkpoint 4
1123        let mut batch = epoch_store.db_batch_for_test();
1124        quarantine
1125            .update_highest_executed_checkpoint(4, &epoch_store, &mut batch)
1126            .unwrap();
1127        batch.write().unwrap();
1128
1129        // C has height=4 which is <= 4 but checkpoint_queue_drained=false.
1130        // C2 has height=5 which is > 4, so it's skipped.
1131        // No drain boundary found => nothing drained.
1132        assert_eq!(quarantine.output_queue_len_for_testing(), 2);
1133    }
1134
1135    #[tokio::test]
1136    async fn test_drain_boundary_commits_at_safe_point() {
1137        let state = TestAuthorityBuilder::new().build().await;
1138        let epoch_store = state.epoch_store_for_testing();
1139
1140        let metrics = epoch_store.metrics.clone();
1141        let mut quarantine = ConsensusOutputQuarantine::new(0, metrics);
1142
1143        let c = make_output(4, 1, false);
1144        quarantine.push_consensus_output(c, &epoch_store).unwrap();
1145
1146        let c2 = make_output(5, 2, true);
1147        quarantine.push_consensus_output(c2, &epoch_store).unwrap();
1148
1149        assert_eq!(quarantine.output_queue_len_for_testing(), 2);
1150
1151        // Insert builder summaries for checkpoints 1-5 with checkpoint_height = seq
1152        let pc = epoch_store.protocol_config();
1153        for seq in 1..=5 {
1154            let summary = make_builder_summary(seq, seq, pc);
1155            quarantine.insert_builder_summary(seq, summary);
1156        }
1157
1158        // Certify up to checkpoint 5
1159        let mut batch = epoch_store.db_batch_for_test();
1160        quarantine
1161            .update_highest_executed_checkpoint(5, &epoch_store, &mut batch)
1162            .unwrap();
1163        batch.write().unwrap();
1164
1165        // C has height=4 <= 5, drained=false.
1166        // C2 has height=5 <= 5, drained=true => drain boundary at index 1.
1167        // Both outputs drained.
1168        assert_eq!(quarantine.output_queue_len_for_testing(), 0);
1169    }
1170}