Skip to main content

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