Skip to main content

sui_core/
consensus_handler.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
6    hash::Hash,
7    num::NonZeroUsize,
8    sync::{Arc, Mutex},
9    time::{Duration, SystemTime, UNIX_EPOCH},
10};
11
12use consensus_config::Committee as ConsensusCommittee;
13use consensus_core::{CommitConsumerMonitor, CommitIndex, CommitRef};
14use consensus_types::block::BlockRef;
15use consensus_types::block::TransactionIndex;
16use fastcrypto_zkp::bn254::zk_login::{JWK, JwkId};
17use lru::LruCache;
18use mysten_common::{
19    assert_reachable, assert_sometimes, debug_fatal, random_util::randomize_cache_capacity_in_tests,
20};
21use mysten_metrics::{
22    monitored_future,
23    monitored_mpsc::{self, UnboundedReceiver},
24    monitored_scope, spawn_monitored_task,
25};
26use parking_lot::RwLockWriteGuard;
27use serde::{Deserialize, Serialize};
28use sui_config::node::CongestionLogConfig;
29use sui_macros::{fail_point, fail_point_arg, fail_point_if};
30use sui_protocol_config::{Chain, PerObjectCongestionControlMode, ProtocolConfig};
31use sui_types::{
32    authenticator_state::ActiveJwk,
33    base_types::{
34        AuthorityName, ConciseableName, ConsensusObjectSequenceKey, ObjectID, ObjectRef,
35        SequenceNumber, TransactionDigest,
36    },
37    crypto::RandomnessRound,
38    digests::{AdditionalConsensusStateDigest, ConsensusCommitDigest, Digest},
39    executable_transaction::{
40        TrustedExecutableTransaction, VerifiedExecutableTransaction,
41        VerifiedExecutableTransactionWithAliases,
42    },
43    messages_checkpoint::{
44        CheckpointSequenceNumber, CheckpointSignatureMessage, CheckpointTimestamp,
45    },
46    messages_consensus::{
47        AuthorityCapabilitiesV2, AuthorityIndex, ConsensusDeterminedVersionAssignments,
48        ConsensusPosition, ConsensusTransaction, ConsensusTransactionKey, ConsensusTransactionKind,
49        ExecutionTimeObservation, SharedTransactionDenyConfig,
50    },
51    sui_system_state::epoch_start_sui_system_state::EpochStartSystemStateTrait,
52    transaction::{
53        InputObjectKind, PlainTransactionWithClaims, SenderSignedData, TransactionDataAPI,
54        TransactionKey, VerifiedTransaction, WithAliases,
55    },
56};
57use tokio::task::JoinSet;
58use tracing::{debug, error, info, instrument, trace, warn};
59
60use crate::{
61    authority::{
62        AuthorityMetrics, AuthorityState, ExecutionEnv,
63        authority_per_epoch_store::{
64            AuthorityPerEpochStore, CancelConsensusCertificateReason, ConsensusStats,
65            ConsensusStatsAPI, ExecutionIndices, ExecutionIndicesWithStatsV2,
66            consensus_quarantine::ConsensusCommitOutput,
67        },
68        backpressure::{BackpressureManager, BackpressureSubscriber},
69        congestion_log::CongestionCommitLogger,
70        consensus_tx_status_cache::ConsensusTxStatus,
71        execution_time_estimator::ExecutionTimeEstimator,
72        shared_object_congestion_tracker::SharedObjectCongestionTracker,
73        shared_object_version_manager::{AssignedTxAndVersions, AssignedVersions, Schedulable},
74        transaction_deferral::{DeferralKey, DeferralReason, transaction_deferral_within_limit},
75    },
76    checkpoints::{
77        CheckpointHeight, CheckpointRoots, CheckpointService, CheckpointServiceNotify,
78        PendingCheckpoint, PendingCheckpointInfo,
79    },
80    consensus_throughput_calculator::ConsensusThroughputCalculator,
81    consensus_types::consensus_output_api::{ConsensusCommitAPI, ParsedTransaction},
82    epoch::{
83        randomness::{DkgStatus, RandomnessManager},
84        reconfiguration::ReconfigState,
85    },
86    execution_cache::ObjectCacheRead,
87    execution_scheduler::{SettlementBatchInfo, SettlementScheduler},
88    gasless_rate_limiter::ConsensusGaslessCounter,
89    post_consensus_tx_reorder::PostConsensusTxReorder,
90    traffic_controller::{TrafficController, policies::TrafficTally},
91    transaction_deny_config_manager::TransactionDenyConfigManager,
92};
93
94/// Tracks the nature of intra-commit owned object lock conflicts for a winning transaction.
95#[derive(Default)]
96struct ConflictInfo {
97    /// Number of conflicts on gas payment objects.
98    gas_object_conflicts: u64,
99    /// Number of conflicts on non-gas owned objects.
100    non_gas_object_conflicts: u64,
101    /// Index of the block authority that sequenced the winning (lock holder) transaction.
102    winner_author: usize,
103}
104
105/// Output from filtering consensus transactions.
106/// Contains the filtered transactions and any owned object locks acquired post-consensus.
107struct FilteredConsensusOutput {
108    transactions: Vec<(SequencedConsensusTransactionKind, u32)>,
109    owned_object_locks: HashMap<ObjectRef, TransactionDigest>,
110    dropped_transaction_keys: Vec<ConsensusTransactionKey>,
111    // When multiple transactions in the same commit try to lock the same owned object, the transaction
112    // that managed to lock it first is tracked here with the conflict info.
113    contested_transaction_digests: HashMap<TransactionDigest, ConflictInfo>,
114}
115
116pub struct ConsensusHandlerInitializer {
117    state: Arc<AuthorityState>,
118    checkpoint_service: Arc<CheckpointService>,
119    epoch_store: Arc<AuthorityPerEpochStore>,
120    throughput_calculator: Arc<ConsensusThroughputCalculator>,
121    backpressure_manager: Arc<BackpressureManager>,
122    congestion_logger: Option<Arc<Mutex<CongestionCommitLogger>>>,
123    consensus_gasless_counter: Arc<ConsensusGaslessCounter>,
124}
125
126impl ConsensusHandlerInitializer {
127    pub fn new(
128        state: Arc<AuthorityState>,
129        checkpoint_service: Arc<CheckpointService>,
130        epoch_store: Arc<AuthorityPerEpochStore>,
131        throughput_calculator: Arc<ConsensusThroughputCalculator>,
132        backpressure_manager: Arc<BackpressureManager>,
133        congestion_log_config: Option<CongestionLogConfig>,
134    ) -> Self {
135        let congestion_logger =
136            congestion_log_config.and_then(|config| match CongestionCommitLogger::new(&config) {
137                Ok(logger) => Some(Arc::new(Mutex::new(logger))),
138                Err(e) => {
139                    debug_fatal!("Failed to create congestion logger: {e}");
140                    None
141                }
142            });
143        let consensus_gasless_counter = state.consensus_gasless_counter.clone();
144        Self {
145            state,
146            checkpoint_service,
147            epoch_store,
148            throughput_calculator,
149            backpressure_manager,
150            congestion_logger,
151            consensus_gasless_counter,
152        }
153    }
154
155    #[cfg(test)]
156    pub(crate) fn new_for_testing(
157        state: Arc<AuthorityState>,
158        checkpoint_service: Arc<CheckpointService>,
159    ) -> Self {
160        let backpressure_manager = BackpressureManager::new_for_tests();
161        let consensus_gasless_counter = state.consensus_gasless_counter.clone();
162        Self {
163            state: state.clone(),
164            checkpoint_service,
165            epoch_store: state.epoch_store_for_testing().clone(),
166            throughput_calculator: Arc::new(ConsensusThroughputCalculator::new(
167                None,
168                state.metrics.clone(),
169            )),
170            backpressure_manager,
171            congestion_logger: None,
172            consensus_gasless_counter,
173        }
174    }
175
176    pub(crate) fn new_consensus_handler(&self) -> ConsensusHandler<CheckpointService> {
177        let new_epoch_start_state = self.epoch_store.epoch_start_state();
178        let consensus_committee = new_epoch_start_state.get_consensus_committee();
179
180        let settlement_scheduler = SettlementScheduler::new(
181            self.state.execution_scheduler().as_ref().clone(),
182            self.state.get_transaction_cache_reader().clone(),
183            self.state.metrics.clone(),
184        );
185        ConsensusHandler::new(
186            self.epoch_store.clone(),
187            self.checkpoint_service.clone(),
188            settlement_scheduler,
189            self.state.get_object_cache_reader().clone(),
190            consensus_committee,
191            self.state.metrics.clone(),
192            self.throughput_calculator.clone(),
193            self.backpressure_manager.subscribe(),
194            self.state.traffic_controller.clone(),
195            self.congestion_logger.clone(),
196            self.consensus_gasless_counter.clone(),
197            self.state.transaction_deny_config_manager().clone(),
198        )
199    }
200}
201
202mod additional_consensus_state {
203    use std::marker::PhantomData;
204
205    use consensus_core::CommitRef;
206    use fastcrypto::hash::HashFunction as _;
207    use sui_types::{crypto::DefaultHash, digests::Digest};
208
209    use super::*;
210    /// AdditionalConsensusState tracks any in-memory state that is retained by ConsensusHandler
211    /// between consensus commits. Because of crash recovery, using such data is inherently risky.
212    /// In order to do this safely, we must store data from a fixed number of previous commits.
213    /// Then, at start-up, that same fixed number of already processed commits is replayed to
214    /// reconstruct the state.
215    ///
216    /// To make sure that bugs in this process appear immediately, we record the digest of this
217    /// state in ConsensusCommitPrologue, so that any deviation causes an immediate fork.
218    #[derive(Serialize, Deserialize)]
219    pub(super) struct AdditionalConsensusState {
220        commit_interval_observer: CommitIntervalObserver,
221    }
222
223    impl AdditionalConsensusState {
224        pub fn new(additional_consensus_state_window_size: u32) -> Self {
225            Self {
226                commit_interval_observer: CommitIntervalObserver::new(
227                    additional_consensus_state_window_size,
228                ),
229            }
230        }
231
232        /// Update all internal state based on the new commit
233        pub(crate) fn observe_commit(
234            &mut self,
235            protocol_config: &ProtocolConfig,
236            epoch_start_time: u64,
237            consensus_commit: &impl ConsensusCommitAPI,
238        ) -> ConsensusCommitInfo {
239            self.commit_interval_observer
240                .observe_commit_time(consensus_commit);
241
242            let estimated_commit_period = self
243                .commit_interval_observer
244                .commit_interval_estimate()
245                .unwrap_or(Duration::from_millis(
246                    protocol_config.min_checkpoint_interval_ms(),
247                ));
248
249            info!("estimated commit rate: {:?}", estimated_commit_period);
250
251            self.commit_info_impl(
252                epoch_start_time,
253                consensus_commit,
254                Some(estimated_commit_period),
255            )
256        }
257
258        fn commit_info_impl(
259            &self,
260            epoch_start_time: u64,
261            consensus_commit: &impl ConsensusCommitAPI,
262            estimated_commit_period: Option<Duration>,
263        ) -> ConsensusCommitInfo {
264            let leader_author = consensus_commit.leader_author_index();
265            let timestamp = consensus_commit.commit_timestamp_ms();
266
267            let timestamp = if timestamp < epoch_start_time {
268                error!(
269                    "Unexpected commit timestamp {timestamp} less then epoch start time {epoch_start_time}, author {leader_author:?}"
270                );
271                epoch_start_time
272            } else {
273                timestamp
274            };
275
276            ConsensusCommitInfo {
277                _phantom: PhantomData,
278                round: consensus_commit.leader_round(),
279                timestamp,
280                leader_author,
281                consensus_commit_ref: consensus_commit.commit_ref(),
282                rejected_transactions_digest: consensus_commit.rejected_transactions_digest(),
283                additional_state_digest: Some(self.digest()),
284                estimated_commit_period,
285                skip_consensus_commit_prologue_in_test: false,
286            }
287        }
288
289        /// Get the digest of the current state.
290        fn digest(&self) -> AdditionalConsensusStateDigest {
291            let mut hash = DefaultHash::new();
292            bcs::serialize_into(&mut hash, self).unwrap();
293            AdditionalConsensusStateDigest::new(hash.finalize().into())
294        }
295    }
296
297    pub struct ConsensusCommitInfo {
298        // prevent public construction
299        _phantom: PhantomData<()>,
300
301        pub round: u64,
302        pub timestamp: u64,
303        pub leader_author: AuthorityIndex,
304        pub consensus_commit_ref: CommitRef,
305        pub rejected_transactions_digest: Digest,
306
307        additional_state_digest: Option<AdditionalConsensusStateDigest>,
308        estimated_commit_period: Option<Duration>,
309
310        pub skip_consensus_commit_prologue_in_test: bool,
311    }
312
313    impl ConsensusCommitInfo {
314        pub fn new_for_test(
315            commit_round: u64,
316            commit_timestamp: u64,
317            estimated_commit_period: Option<Duration>,
318            skip_consensus_commit_prologue_in_test: bool,
319        ) -> Self {
320            Self {
321                _phantom: PhantomData,
322                round: commit_round,
323                timestamp: commit_timestamp,
324                leader_author: 0,
325                consensus_commit_ref: CommitRef::default(),
326                rejected_transactions_digest: Digest::default(),
327                additional_state_digest: Some(AdditionalConsensusStateDigest::ZERO),
328                estimated_commit_period,
329                skip_consensus_commit_prologue_in_test,
330            }
331        }
332
333        pub fn new_for_congestion_test(
334            commit_round: u64,
335            commit_timestamp: u64,
336            estimated_commit_period: Duration,
337        ) -> Self {
338            Self::new_for_test(
339                commit_round,
340                commit_timestamp,
341                Some(estimated_commit_period),
342                true,
343            )
344        }
345
346        pub fn additional_state_digest(&self) -> AdditionalConsensusStateDigest {
347            // this method cannot be called if stateless_commit_info is used
348            self.additional_state_digest
349                .expect("additional_state_digest is not available")
350        }
351
352        pub fn estimated_commit_period(&self) -> Duration {
353            // this method cannot be called if stateless_commit_info is used
354            self.estimated_commit_period
355                .expect("estimated commit period is not available")
356        }
357
358        fn consensus_commit_digest(&self) -> ConsensusCommitDigest {
359            ConsensusCommitDigest::new(self.consensus_commit_ref.digest.into_inner())
360        }
361
362        fn consensus_commit_prologue_v4_transaction(
363            &self,
364            epoch: u64,
365            consensus_determined_version_assignments: ConsensusDeterminedVersionAssignments,
366            additional_state_digest: AdditionalConsensusStateDigest,
367        ) -> VerifiedExecutableTransaction {
368            let transaction = VerifiedTransaction::new_consensus_commit_prologue_v4(
369                epoch,
370                self.round,
371                self.timestamp,
372                self.consensus_commit_digest(),
373                consensus_determined_version_assignments,
374                additional_state_digest,
375            );
376            VerifiedExecutableTransaction::new_system(transaction, epoch)
377        }
378
379        pub fn create_consensus_commit_prologue_transaction(
380            &self,
381            epoch: u64,
382            cancelled_txn_version_assignment: Vec<(
383                TransactionDigest,
384                Vec<(ConsensusObjectSequenceKey, SequenceNumber)>,
385            )>,
386            indirect_state_observer: IndirectStateObserver,
387        ) -> VerifiedExecutableTransaction {
388            let version_assignments =
389                ConsensusDeterminedVersionAssignments::CancelledTransactionsV2(
390                    cancelled_txn_version_assignment,
391                );
392            let additional_state_digest =
393                indirect_state_observer.fold_with(self.additional_state_digest());
394
395            self.consensus_commit_prologue_v4_transaction(
396                epoch,
397                version_assignments,
398                additional_state_digest,
399            )
400        }
401    }
402
403    #[derive(Default)]
404    pub struct IndirectStateObserver {
405        hash: DefaultHash,
406    }
407
408    impl IndirectStateObserver {
409        pub fn new() -> Self {
410            Self::default()
411        }
412
413        pub fn observe_indirect_state<T: Serialize>(&mut self, state: &T) {
414            bcs::serialize_into(&mut self.hash, state).unwrap();
415        }
416
417        pub fn fold_with(
418            self,
419            d1: AdditionalConsensusStateDigest,
420        ) -> AdditionalConsensusStateDigest {
421            let hash = self.hash.finalize();
422            let d2 = AdditionalConsensusStateDigest::new(hash.into());
423
424            let mut hasher = DefaultHash::new();
425            bcs::serialize_into(&mut hasher, &d1).unwrap();
426            bcs::serialize_into(&mut hasher, &d2).unwrap();
427            AdditionalConsensusStateDigest::new(hasher.finalize().into())
428        }
429    }
430
431    #[test]
432    fn test_additional_consensus_state() {
433        use crate::consensus_test_utils::TestConsensusCommit;
434
435        fn observe(state: &mut AdditionalConsensusState, round: u64, timestamp: u64) {
436            let protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
437            state.observe_commit(
438                &protocol_config,
439                100,
440                &TestConsensusCommit::empty(round, timestamp, 0),
441            );
442        }
443
444        let mut s1 = AdditionalConsensusState::new(3);
445        observe(&mut s1, 1, 1000);
446        observe(&mut s1, 2, 2000);
447        observe(&mut s1, 3, 3000);
448        observe(&mut s1, 4, 4000);
449
450        let mut s2 = AdditionalConsensusState::new(3);
451        // Because state uses a ring buffer, we should get the same digest
452        // even though we only added the 3 latest observations.
453        observe(&mut s2, 2, 2000);
454        observe(&mut s2, 3, 3000);
455        observe(&mut s2, 4, 4000);
456
457        assert_eq!(s1.digest(), s2.digest());
458
459        observe(&mut s1, 5, 5000);
460        observe(&mut s2, 5, 5000);
461
462        assert_eq!(s1.digest(), s2.digest());
463    }
464}
465use additional_consensus_state::AdditionalConsensusState;
466pub(crate) use additional_consensus_state::{ConsensusCommitInfo, IndirectStateObserver};
467
468struct QueuedCheckpointRoots {
469    roots: CheckpointRoots,
470    timestamp: CheckpointTimestamp,
471    consensus_commit_ref: CommitRef,
472    rejected_transactions_digest: Digest,
473}
474
475struct Chunk<
476    T: crate::authority::shared_object_version_manager::AsTx = VerifiedExecutableTransaction,
477> {
478    schedulables: Vec<Schedulable<T>>,
479    settlement: Option<Schedulable<T>>,
480    height: CheckpointHeight,
481}
482
483impl<T: crate::authority::shared_object_version_manager::AsTx + Clone> Chunk<T> {
484    fn all_schedulables(&self) -> impl Iterator<Item = &Schedulable<T>> + Clone {
485        self.schedulables.iter().chain(self.settlement.iter())
486    }
487
488    fn all_schedulables_from(chunks: &[Self]) -> impl Iterator<Item = &Schedulable<T>> + Clone {
489        chunks.iter().flat_map(|c| c.all_schedulables())
490    }
491
492    fn to_checkpoint_roots(&self) -> CheckpointRoots {
493        let tx_roots: Vec<_> = self.schedulables.iter().map(|s| s.key()).collect();
494        let settlement_root = self.settlement.as_ref().map(|s| s.key());
495        CheckpointRoots {
496            tx_roots,
497            settlement_root,
498            height: self.height,
499        }
500    }
501}
502
503impl From<Chunk<VerifiedExecutableTransactionWithAliases>> for Chunk {
504    fn from(chunk: Chunk<VerifiedExecutableTransactionWithAliases>) -> Self {
505        Chunk {
506            schedulables: chunk.schedulables.into_iter().map(|s| s.into()).collect(),
507            settlement: chunk.settlement.map(|s| s.into()),
508            height: chunk.height,
509        }
510    }
511}
512
513// Accumulates checkpoint roots from consensus and flushes them into PendingCheckpoints.
514// Roots are buffered in `pending_roots` until a flush is triggered (by max_tx overflow or
515// time interval). A flush always drains all buffered roots into a single PendingCheckpoint,
516// so the queue only ever holds roots for one pending checkpoint at a time.
517//
518// Owns an ExecutionSchedulerSender so push_chunk can send schedulables and settlement info
519// for execution in one shot.
520pub(crate) struct CheckpointQueue {
521    last_built_timestamp: CheckpointTimestamp,
522    pending_roots: VecDeque<QueuedCheckpointRoots>,
523    height: u64,
524    pending_tx_count: usize,
525    current_checkpoint_seq: CheckpointSequenceNumber,
526    max_tx: usize,
527    min_checkpoint_interval_ms: u64,
528    execution_scheduler_sender: ExecutionSchedulerSender,
529}
530
531impl CheckpointQueue {
532    pub(crate) fn new(
533        last_built_timestamp: CheckpointTimestamp,
534        checkpoint_height: u64,
535        next_checkpoint_seq: CheckpointSequenceNumber,
536        max_tx: usize,
537        min_checkpoint_interval_ms: u64,
538        execution_scheduler_sender: ExecutionSchedulerSender,
539    ) -> Self {
540        Self {
541            last_built_timestamp,
542            pending_roots: VecDeque::new(),
543            height: checkpoint_height,
544            pending_tx_count: 0,
545            current_checkpoint_seq: next_checkpoint_seq,
546            max_tx,
547            min_checkpoint_interval_ms,
548            execution_scheduler_sender,
549        }
550    }
551
552    #[cfg(test)]
553    fn new_for_testing(
554        last_built_timestamp: CheckpointTimestamp,
555        checkpoint_height: u64,
556        next_checkpoint_seq: CheckpointSequenceNumber,
557        max_tx: usize,
558        min_checkpoint_interval_ms: u64,
559    ) -> Self {
560        let (sender, _receiver) = monitored_mpsc::unbounded_channel("test_checkpoint_queue_sender");
561        Self {
562            last_built_timestamp,
563            pending_roots: VecDeque::new(),
564            height: checkpoint_height,
565            pending_tx_count: 0,
566            current_checkpoint_seq: next_checkpoint_seq,
567            max_tx,
568            min_checkpoint_interval_ms,
569            execution_scheduler_sender: ExecutionSchedulerSender::new_for_testing(sender),
570        }
571    }
572
573    #[cfg(test)]
574    fn new_for_testing_with_sender(
575        last_built_timestamp: CheckpointTimestamp,
576        checkpoint_height: u64,
577        next_checkpoint_seq: CheckpointSequenceNumber,
578        max_tx: usize,
579        min_checkpoint_interval_ms: u64,
580        sender: monitored_mpsc::UnboundedSender<SchedulerMessage>,
581    ) -> Self {
582        Self {
583            last_built_timestamp,
584            pending_roots: VecDeque::new(),
585            height: checkpoint_height,
586            pending_tx_count: 0,
587            current_checkpoint_seq: next_checkpoint_seq,
588            max_tx,
589            min_checkpoint_interval_ms,
590            execution_scheduler_sender: ExecutionSchedulerSender::new_for_testing(sender),
591        }
592    }
593
594    pub(crate) fn last_built_timestamp(&self) -> CheckpointTimestamp {
595        self.last_built_timestamp
596    }
597
598    pub(crate) fn is_empty(&self) -> bool {
599        self.pending_roots.is_empty()
600    }
601
602    fn next_height(&mut self) -> u64 {
603        self.height += 1;
604        self.height
605    }
606
607    fn push_chunk(
608        &mut self,
609        chunk: Chunk,
610        assigned_versions: &HashMap<TransactionKey, AssignedVersions>,
611        timestamp: CheckpointTimestamp,
612        consensus_commit_ref: CommitRef,
613        rejected_transactions_digest: Digest,
614    ) -> Vec<PendingCheckpoint> {
615        let max_tx = self.max_tx;
616        let user_tx_count = chunk.schedulables.len();
617
618        let roots = chunk.to_checkpoint_roots();
619
620        let schedulables: Vec<_> = chunk
621            .schedulables
622            .into_iter()
623            .map(|s| {
624                let versions = assigned_versions
625                    .get(&s.key())
626                    .cloned()
627                    .unwrap_or_else(AssignedVersions::empty);
628                (s, versions)
629            })
630            .collect();
631
632        let mut flushed_checkpoints = Vec::new();
633
634        if self.pending_tx_count > 0
635            && self.pending_tx_count + user_tx_count > max_tx
636            && let Some(checkpoint) = self.flush_forced()
637        {
638            flushed_checkpoints.push(checkpoint);
639        }
640
641        let settlement_info = chunk.settlement.as_ref().map(|s| {
642            let settlement_key = s.key();
643            let tx_keys: Vec<_> = schedulables.iter().map(|(s, _)| s.key()).collect();
644            SettlementBatchInfo {
645                settlement_key,
646                tx_keys,
647                checkpoint_height: chunk.height,
648                checkpoint_seq: self.current_checkpoint_seq,
649                assigned_versions: assigned_versions
650                    .get(&settlement_key)
651                    .cloned()
652                    .unwrap_or_else(AssignedVersions::empty),
653            }
654        });
655
656        self.execution_scheduler_sender
657            .send(schedulables, settlement_info);
658
659        self.pending_tx_count += user_tx_count;
660        self.pending_roots.push_back(QueuedCheckpointRoots {
661            roots,
662            timestamp,
663            consensus_commit_ref,
664            rejected_transactions_digest,
665        });
666
667        flushed_checkpoints
668    }
669
670    pub(crate) fn flush(
671        &mut self,
672        current_timestamp: CheckpointTimestamp,
673        force: bool,
674    ) -> Option<PendingCheckpoint> {
675        if !force && current_timestamp < self.last_built_timestamp + self.min_checkpoint_interval_ms
676        {
677            return None;
678        }
679        self.flush_forced()
680    }
681
682    fn flush_forced(&mut self) -> Option<PendingCheckpoint> {
683        if self.pending_roots.is_empty() {
684            return None;
685        }
686
687        let to_flush: Vec<_> = self.pending_roots.drain(..).collect();
688        let last_root = to_flush.last().unwrap();
689
690        let checkpoint = PendingCheckpoint {
691            roots: to_flush.iter().map(|q| q.roots.clone()).collect(),
692            details: PendingCheckpointInfo {
693                timestamp_ms: last_root.timestamp,
694                last_of_epoch: false,
695                checkpoint_height: last_root.roots.height,
696                consensus_commit_ref: last_root.consensus_commit_ref,
697                rejected_transactions_digest: last_root.rejected_transactions_digest,
698                checkpoint_seq: self.current_checkpoint_seq,
699            },
700        };
701
702        self.last_built_timestamp = last_root.timestamp;
703        self.pending_tx_count = 0;
704        self.current_checkpoint_seq += 1;
705
706        Some(checkpoint)
707    }
708
709    pub(crate) fn checkpoint_seq(&self) -> CheckpointSequenceNumber {
710        self.current_checkpoint_seq
711            .checked_sub(1)
712            .expect("checkpoint_seq called before any checkpoint was assigned")
713    }
714}
715
716pub struct ConsensusHandler<C> {
717    /// A store created for each epoch. ConsensusHandler is recreated each epoch, with the
718    /// corresponding store. This store is also used to get the current epoch ID.
719    epoch_store: Arc<AuthorityPerEpochStore>,
720    /// Holds the indices, hash and stats after the last consensus commit
721    /// It is used for avoiding replaying already processed transactions,
722    /// checking chain consistency, and accumulating per-epoch consensus output stats.
723    last_consensus_stats: ExecutionIndicesWithStatsV2,
724    checkpoint_service: Arc<C>,
725    /// cache reader is needed when determining the next version to assign for shared objects.
726    cache_reader: Arc<dyn ObjectCacheRead>,
727    /// The consensus committee used to do stake computations for deciding set of low scoring authorities
728    committee: ConsensusCommittee,
729    // TODO: ConsensusHandler doesn't really share metrics with AuthorityState. We could define
730    // a new metrics type here if we want to.
731    metrics: Arc<AuthorityMetrics>,
732    /// Lru cache to quickly discard transactions processed by consensus
733    processed_cache: LruCache<SequencedConsensusTransactionKey, ()>,
734    /// Using the throughput calculator to record the current consensus throughput
735    throughput_calculator: Arc<ConsensusThroughputCalculator>,
736
737    additional_consensus_state: AdditionalConsensusState,
738
739    backpressure_subscriber: BackpressureSubscriber,
740
741    traffic_controller: Option<Arc<TrafficController>>,
742
743    congestion_logger: Option<Arc<Mutex<CongestionCommitLogger>>>,
744
745    consensus_gasless_counter: Arc<ConsensusGaslessCounter>,
746
747    transaction_deny_config_manager: Arc<TransactionDenyConfigManager>,
748
749    checkpoint_queue: Mutex<CheckpointQueue>,
750}
751
752const PROCESSED_CACHE_CAP: usize = 1024 * 1024;
753
754fn assert_supported_protocol_config(protocol_config: &ProtocolConfig) {
755    assert!(
756        matches!(
757            protocol_config.per_object_congestion_control_mode(),
758            PerObjectCongestionControlMode::ExecutionTimeEstimate(_)
759        ),
760        "support for congestion control modes other than PerObjectCongestionControlMode::ExecutionTimeEstimate has been removed"
761    );
762    assert!(
763        protocol_config.split_checkpoints_in_consensus_handler(),
764        "support for splitting checkpoints outside of consensus handler has been removed"
765    );
766    assert!(protocol_config.ignore_execution_time_observations_after_certs_closed());
767    assert!(protocol_config.record_time_estimate_processed());
768    assert!(protocol_config.prepend_prologue_tx_in_consensus_commit_in_checkpoints());
769    assert!(protocol_config.consensus_checkpoint_signature_key_includes_digest());
770    assert!(protocol_config.authority_capabilities_v2());
771    assert!(protocol_config.cancel_for_failed_dkg_early());
772    assert!(protocol_config.record_consensus_determined_version_assignments_in_prologue_v2());
773    assert!(protocol_config.record_additional_state_digest_in_prologue());
774    assert!(protocol_config.additional_consensus_digest_indirect_state());
775    assert!(protocol_config.include_cancelled_randomness_txns_in_prologue());
776    assert!(protocol_config.fix_checkpoint_signature_mapping());
777    assert!(protocol_config.merge_randomness_into_checkpoint());
778    assert!(
779        protocol_config.timestamp_based_epoch_close(),
780        "support for non-timestamp-based epoch close has been removed"
781    );
782}
783
784impl<C> ConsensusHandler<C> {
785    pub(crate) fn new(
786        epoch_store: Arc<AuthorityPerEpochStore>,
787        checkpoint_service: Arc<C>,
788        settlement_scheduler: SettlementScheduler,
789        cache_reader: Arc<dyn ObjectCacheRead>,
790        committee: ConsensusCommittee,
791        metrics: Arc<AuthorityMetrics>,
792        throughput_calculator: Arc<ConsensusThroughputCalculator>,
793        backpressure_subscriber: BackpressureSubscriber,
794        traffic_controller: Option<Arc<TrafficController>>,
795        congestion_logger: Option<Arc<Mutex<CongestionCommitLogger>>>,
796        consensus_gasless_counter: Arc<ConsensusGaslessCounter>,
797        transaction_deny_config_manager: Arc<TransactionDenyConfigManager>,
798    ) -> Self {
799        assert_supported_protocol_config(epoch_store.protocol_config());
800
801        // Recover last_consensus_stats so it is consistent across validators.
802        let mut last_consensus_stats = epoch_store
803            .get_last_consensus_stats()
804            .expect("Should be able to read last consensus index");
805        // stats is empty at the beginning of epoch.
806        if !last_consensus_stats.stats.is_initialized() {
807            last_consensus_stats.stats = ConsensusStats::new(committee.size());
808            last_consensus_stats.checkpoint_seq = epoch_store.previous_epoch_last_checkpoint();
809        }
810        let max_tx = epoch_store
811            .protocol_config()
812            .max_transactions_per_checkpoint() as usize;
813        let min_checkpoint_interval_ms = epoch_store
814            .protocol_config()
815            .min_checkpoint_interval_ms_as_option()
816            .unwrap_or_default();
817        let execution_scheduler_sender =
818            ExecutionSchedulerSender::start(settlement_scheduler, epoch_store.clone());
819        let commit_rate_estimate_window_size = epoch_store
820            .protocol_config()
821            .get_consensus_commit_rate_estimation_window_size();
822        let last_built_timestamp = last_consensus_stats.last_checkpoint_flush_timestamp;
823        let checkpoint_height = last_consensus_stats.height;
824        let next_checkpoint_seq = last_consensus_stats.checkpoint_seq + 1;
825        Self {
826            epoch_store,
827            last_consensus_stats,
828            checkpoint_service,
829            cache_reader,
830            committee,
831            metrics,
832            processed_cache: LruCache::new(
833                NonZeroUsize::new(randomize_cache_capacity_in_tests(PROCESSED_CACHE_CAP)).unwrap(),
834            ),
835            throughput_calculator,
836            additional_consensus_state: AdditionalConsensusState::new(
837                commit_rate_estimate_window_size,
838            ),
839            backpressure_subscriber,
840            traffic_controller,
841            congestion_logger,
842            consensus_gasless_counter,
843            transaction_deny_config_manager,
844            checkpoint_queue: Mutex::new(CheckpointQueue::new(
845                last_built_timestamp,
846                checkpoint_height,
847                next_checkpoint_seq,
848                max_tx,
849                min_checkpoint_interval_ms,
850                execution_scheduler_sender,
851            )),
852        }
853    }
854
855    /// Returns the last subdag index processed by the handler.
856    pub(crate) fn last_processed_subdag_index(&self) -> u64 {
857        self.last_consensus_stats.index.sub_dag_index
858    }
859
860    pub(crate) fn new_for_testing(
861        epoch_store: Arc<AuthorityPerEpochStore>,
862        checkpoint_service: Arc<C>,
863        execution_scheduler_sender: ExecutionSchedulerSender,
864        cache_reader: Arc<dyn ObjectCacheRead>,
865        committee: ConsensusCommittee,
866        metrics: Arc<AuthorityMetrics>,
867        throughput_calculator: Arc<ConsensusThroughputCalculator>,
868        backpressure_subscriber: BackpressureSubscriber,
869        traffic_controller: Option<Arc<TrafficController>>,
870        transaction_deny_config_manager: Arc<TransactionDenyConfigManager>,
871        last_consensus_stats: ExecutionIndicesWithStatsV2,
872    ) -> Self {
873        assert_supported_protocol_config(epoch_store.protocol_config());
874
875        let commit_rate_estimate_window_size = epoch_store
876            .protocol_config()
877            .get_consensus_commit_rate_estimation_window_size();
878        let max_tx = epoch_store
879            .protocol_config()
880            .max_transactions_per_checkpoint() as usize;
881        let min_checkpoint_interval_ms = epoch_store
882            .protocol_config()
883            .min_checkpoint_interval_ms_as_option()
884            .unwrap_or_default();
885        let last_built_timestamp = last_consensus_stats.last_checkpoint_flush_timestamp;
886        let checkpoint_height = last_consensus_stats.height;
887        Self {
888            epoch_store,
889            last_consensus_stats,
890            checkpoint_service,
891            cache_reader,
892            committee,
893            metrics,
894            processed_cache: LruCache::new(
895                NonZeroUsize::new(randomize_cache_capacity_in_tests(PROCESSED_CACHE_CAP)).unwrap(),
896            ),
897            throughput_calculator,
898            additional_consensus_state: AdditionalConsensusState::new(
899                commit_rate_estimate_window_size,
900            ),
901            backpressure_subscriber,
902            traffic_controller,
903            congestion_logger: None,
904            consensus_gasless_counter: Arc::new(ConsensusGaslessCounter::default()),
905            transaction_deny_config_manager,
906            checkpoint_queue: Mutex::new(CheckpointQueue::new(
907                last_built_timestamp,
908                checkpoint_height,
909                0,
910                max_tx,
911                min_checkpoint_interval_ms,
912                execution_scheduler_sender,
913            )),
914        }
915    }
916}
917
918#[derive(Default)]
919struct CommitHandlerInput {
920    user_transactions: Vec<VerifiedExecutableTransactionWithAliases>,
921    capability_notifications: Vec<AuthorityCapabilitiesV2>,
922    execution_time_observations: Vec<ExecutionTimeObservation>,
923    checkpoint_signature_messages: Vec<CheckpointSignatureMessage>,
924    randomness_dkg_messages: Vec<(AuthorityName, Vec<u8>)>,
925    randomness_dkg_confirmations: Vec<(AuthorityName, Vec<u8>)>,
926    end_of_publish_transactions: Vec<AuthorityName>,
927    new_jwks: Vec<(AuthorityName, JwkId, JWK)>,
928    transaction_deny_config_updates: Vec<(AuthorityName, SharedTransactionDenyConfig)>,
929}
930
931struct CommitHandlerState {
932    dkg_failed: bool,
933    randomness_round: Option<RandomnessRound>,
934    output: ConsensusCommitOutput,
935    indirect_state_observer: Option<IndirectStateObserver>,
936    initial_reconfig_state: ReconfigState,
937    // Occurrence counts for user transactions, used for unpaid amplification detection.
938    occurrence_counts: HashMap<TransactionDigest, u32>,
939    // Transactions involved in same commit owned object lock contention (double-spend),
940    // mapped to conflict info (gas vs non-gas breakdown).
941    contested_transaction_digests: HashMap<TransactionDigest, ConflictInfo>,
942}
943
944impl CommitHandlerState {
945    fn new(epoch_store: &AuthorityPerEpochStore, consensus_round: u64) -> Self {
946        Self {
947            output: ConsensusCommitOutput::new(consensus_round),
948            dkg_failed: false,
949            randomness_round: None,
950            indirect_state_observer: Some(IndirectStateObserver::new()),
951            initial_reconfig_state: epoch_store.get_reconfig_state_read_lock_guard().clone(),
952            occurrence_counts: HashMap::new(),
953            contested_transaction_digests: HashMap::new(),
954        }
955    }
956
957    fn get_notifications(&self) -> Vec<SequencedConsensusTransactionKey> {
958        self.output
959            .get_consensus_messages_processed()
960            .cloned()
961            .collect()
962    }
963
964    fn init_randomness<'a, 'epoch>(
965        &'a mut self,
966        epoch_store: &'epoch AuthorityPerEpochStore,
967        commit_info: &'a ConsensusCommitInfo,
968    ) -> Option<tokio::sync::MutexGuard<'epoch, RandomnessManager>> {
969        let mut randomness_manager = epoch_store.randomness_manager.get().map(|rm| {
970            rm.try_lock()
971                .expect("should only ever be called from the commit handler thread")
972        });
973
974        let mut dkg_failed = false;
975        let randomness_round = if epoch_store.randomness_state_enabled() {
976            let randomness_manager = randomness_manager
977                .as_mut()
978                .expect("randomness manager should exist if randomness is enabled");
979            match randomness_manager.dkg_status() {
980                DkgStatus::Pending => None,
981                DkgStatus::Failed => {
982                    dkg_failed = true;
983                    None
984                }
985                DkgStatus::Successful => {
986                    // Generate randomness for this commit if DKG is successful and we are still
987                    // accepting certs.
988                    if self.initial_reconfig_state.should_accept_tx() {
989                        randomness_manager
990                            // TODO: make infallible
991                            .reserve_next_randomness(commit_info.timestamp, &mut self.output)
992                            .expect("epoch ended")
993                    } else {
994                        None
995                    }
996                }
997            }
998        } else {
999            None
1000        };
1001
1002        if randomness_round.is_some() {
1003            assert!(!dkg_failed); // invariant check
1004        }
1005
1006        self.randomness_round = randomness_round;
1007        self.dkg_failed = dkg_failed;
1008
1009        randomness_manager
1010    }
1011}
1012
1013/// Deferred transactions abandoned because the epoch close deadline forced the epoch
1014/// closed while they were still unscheduled.
1015struct AbandonedDeferredTxns {
1016    count: usize,
1017    // At most 10 (key, digest) pairs for logging.
1018    sample: Vec<(DeferralKey, TransactionDigest)>,
1019}
1020
1021impl<C: CheckpointServiceNotify + Send + Sync> ConsensusHandler<C> {
1022    /// Called during startup to allow us to observe commits we previously processed, for crash recovery.
1023    /// Any state computed here must be a pure function of the commits observed, it cannot depend on any
1024    /// state recorded in the epoch db.
1025    fn handle_prior_consensus_commit(&mut self, consensus_commit: impl ConsensusCommitAPI) {
1026        assert!(
1027            self.epoch_store
1028                .protocol_config()
1029                .record_additional_state_digest_in_prologue()
1030        );
1031        let protocol_config = self.epoch_store.protocol_config();
1032        let epoch_start_time = self
1033            .epoch_store
1034            .epoch_start_config()
1035            .epoch_start_timestamp_ms();
1036
1037        self.additional_consensus_state.observe_commit(
1038            protocol_config,
1039            epoch_start_time,
1040            &consensus_commit,
1041        );
1042    }
1043
1044    #[cfg(test)]
1045    pub(crate) async fn handle_consensus_commit_for_test(
1046        &mut self,
1047        consensus_commit: impl ConsensusCommitAPI,
1048    ) {
1049        let transactions = consensus_commit.transactions();
1050        self.handle_consensus_commit(consensus_commit, transactions)
1051            .await;
1052    }
1053
1054    #[instrument(level = "debug", skip_all, fields(epoch = self.epoch_store.epoch(), round = consensus_commit.leader_round()))]
1055    pub(crate) async fn handle_consensus_commit(
1056        &mut self,
1057        consensus_commit: impl ConsensusCommitAPI,
1058        transactions: ParsedConsensusTransactions,
1059    ) {
1060        // This may block until one of two conditions happens:
1061        // - Number of uncommitted transactions in the writeback cache goes below the
1062        //   backpressure threshold.
1063        // - The highest executed checkpoint catches up to the highest certified checkpoint.
1064        self.backpressure_subscriber.await_no_backpressure().await;
1065
1066        let epoch = self.epoch_store.epoch();
1067
1068        let _scope = monitored_scope("ConsensusCommitHandler::handle_consensus_commit");
1069
1070        let last_committed_round = self.last_consensus_stats.index.last_committed_round;
1071
1072        self.epoch_store
1073            .consensus_tx_status_cache
1074            .update_last_committed_leader_round(last_committed_round as u32);
1075        self.epoch_store
1076            .tx_reject_reason_cache
1077            .set_last_committed_leader_round(last_committed_round as u32);
1078
1079        let commit_info = self.additional_consensus_state.observe_commit(
1080            self.epoch_store.protocol_config(),
1081            self.epoch_store
1082                .epoch_start_config()
1083                .epoch_start_timestamp_ms(),
1084            &consensus_commit,
1085        );
1086        assert!(commit_info.round > last_committed_round);
1087
1088        let (timestamp, leader_author, commit_sub_dag_index) =
1089            self.gather_commit_metadata(&consensus_commit);
1090
1091        info!(
1092            %consensus_commit,
1093            "Received consensus output {}. Rejected transactions {}",
1094            consensus_commit.commit_ref(),
1095            consensus_commit.rejected_transactions_debug_string(),
1096        );
1097
1098        self.last_consensus_stats.index = ExecutionIndices {
1099            last_committed_round: commit_info.round,
1100            sub_dag_index: commit_sub_dag_index,
1101            transaction_index: 0_u64,
1102        };
1103
1104        self.metrics
1105            .consensus_committed_subdags
1106            .with_label_values(&[&leader_author.to_string()])
1107            .inc();
1108
1109        let mut state = CommitHandlerState::new(&self.epoch_store, commit_info.round);
1110
1111        let FilteredConsensusOutput {
1112            transactions,
1113            owned_object_locks,
1114            dropped_transaction_keys,
1115            contested_transaction_digests,
1116        } = self.filter_consensus_txns(
1117            state.initial_reconfig_state.clone(),
1118            &commit_info,
1119            transactions,
1120        );
1121        state.contested_transaction_digests = contested_transaction_digests;
1122        // Buffer owned object locks for batch write.
1123        if !owned_object_locks.is_empty() {
1124            state.output.set_owned_object_locks(owned_object_locks);
1125        }
1126
1127        // Still record the dropped transactions as consensus message processed.
1128        for key in dropped_transaction_keys {
1129            state.output.record_consensus_message_processed(
1130                SequencedConsensusTransactionKey::External(key),
1131            );
1132        }
1133        let transactions = self.deduplicate_consensus_txns(&mut state, &commit_info, transactions);
1134
1135        let mut randomness_manager = state.init_randomness(&self.epoch_store, &commit_info);
1136
1137        let CommitHandlerInput {
1138            user_transactions,
1139            capability_notifications,
1140            execution_time_observations,
1141            checkpoint_signature_messages,
1142            randomness_dkg_messages,
1143            randomness_dkg_confirmations,
1144            end_of_publish_transactions,
1145            new_jwks,
1146            transaction_deny_config_updates,
1147        } = self.build_commit_handler_input(transactions);
1148
1149        self.process_gasless_transactions(&commit_info, &user_transactions);
1150        self.process_jwks(&mut state, &commit_info, new_jwks);
1151        self.process_capability_notifications(capability_notifications);
1152        self.process_transaction_deny_config_updates(transaction_deny_config_updates);
1153        self.process_execution_time_observations(&mut state, execution_time_observations);
1154        self.process_checkpoint_signature_messages(checkpoint_signature_messages);
1155
1156        self.process_dkg_updates(
1157            &mut state,
1158            &commit_info,
1159            randomness_manager.as_deref_mut(),
1160            randomness_dkg_messages,
1161            randomness_dkg_confirmations,
1162        )
1163        .await;
1164
1165        let mut execution_time_estimator = self
1166            .epoch_store
1167            .execution_time_estimator
1168            .try_lock()
1169            .expect("should only ever be called from the commit handler thread");
1170
1171        let authenticator_state_update_transaction =
1172            self.create_authenticator_state_update(last_committed_round, &commit_info);
1173
1174        let (
1175            transactions_to_schedule,
1176            randomness_transactions_to_schedule,
1177            cancelled_txns,
1178            randomness_state_update_transaction,
1179        ) = self.collect_transactions_to_schedule(
1180            &mut state,
1181            &mut execution_time_estimator,
1182            &commit_info,
1183            user_transactions,
1184        );
1185
1186        let (should_accept_tx, lock, final_round, abandoned_deferred_txns) =
1187            self.handle_close_epoch(&mut state, &commit_info, end_of_publish_transactions);
1188
1189        let make_checkpoint = should_accept_tx || final_round;
1190        if !make_checkpoint {
1191            // No need for any further processing
1192            return;
1193        }
1194
1195        // If this is the final round, record execution time observations for storage in the
1196        // end-of-epoch tx.
1197        if final_round {
1198            self.record_end_of_epoch_execution_time_observations(&mut execution_time_estimator);
1199        }
1200
1201        let consensus_commit_prologue = (!commit_info.skip_consensus_commit_prologue_in_test)
1202            .then_some(Schedulable::ConsensusCommitPrologue(
1203                epoch,
1204                commit_info.round,
1205                commit_info.consensus_commit_ref.index,
1206            ));
1207
1208        let schedulables: Vec<_> = itertools::chain!(
1209            consensus_commit_prologue.into_iter(),
1210            authenticator_state_update_transaction
1211                .into_iter()
1212                .map(Schedulable::Transaction),
1213            transactions_to_schedule
1214                .into_iter()
1215                .map(Schedulable::Transaction),
1216        )
1217        .collect();
1218
1219        let randomness_schedulables: Vec<_> = randomness_state_update_transaction
1220            .into_iter()
1221            .chain(
1222                randomness_transactions_to_schedule
1223                    .into_iter()
1224                    .map(Schedulable::Transaction),
1225            )
1226            .collect();
1227
1228        let num_schedulables = schedulables.len();
1229        let checkpoint_height = self.create_pending_checkpoints(
1230            &mut state,
1231            &commit_info,
1232            schedulables,
1233            randomness_schedulables,
1234            &cancelled_txns,
1235            final_round,
1236        );
1237
1238        let notifications = state.get_notifications();
1239
1240        let mut stats_to_record = self.last_consensus_stats.clone();
1241        stats_to_record.height = checkpoint_height;
1242        {
1243            let queue = self.checkpoint_queue.lock().unwrap();
1244            stats_to_record.last_checkpoint_flush_timestamp = queue.last_built_timestamp();
1245            stats_to_record.checkpoint_seq = queue.checkpoint_seq();
1246        }
1247
1248        state.output.record_consensus_commit_stats(stats_to_record);
1249
1250        self.record_deferral_deletion(&mut state);
1251
1252        self.epoch_store
1253            .consensus_quarantine
1254            .write()
1255            .push_consensus_output(state.output, &self.epoch_store)
1256            .expect("push_consensus_output should not fail");
1257
1258        debug!(
1259            ?commit_info.round,
1260            "Notifying checkpoint service about new pending checkpoint(s)",
1261        );
1262        self.checkpoint_service
1263            .notify_checkpoint()
1264            .expect("failed to notify checkpoint service");
1265
1266        if let Some(randomness_round) = state.randomness_round {
1267            randomness_manager
1268                .as_ref()
1269                .expect("randomness manager should exist if randomness round is provided")
1270                .generate_randomness(epoch, randomness_round);
1271        }
1272
1273        self.epoch_store.process_notifications(notifications.iter());
1274
1275        // pass lock by value to ensure that it is held until this point
1276        self.log_final_round(lock, final_round);
1277
1278        // Report abandoned transactions only after the commit output has been pushed
1279        // and all notifications delivered: in configurations where debug_fatal panics, firing
1280        // it mid-commit would abort processing of the very commit that closes the epoch.
1281        if let Some(AbandonedDeferredTxns { count, sample }) = abandoned_deferred_txns {
1282            self.metrics
1283                .consensus_handler_dropped_transactions
1284                .with_label_values(&["epoch_close_deadline"])
1285                .inc_by(count as u64);
1286            debug_fatal!(
1287                "Epoch close deadline reached with unscheduled deferred transactions: count={}, sample={:?}",
1288                count,
1289                sample
1290            );
1291        }
1292
1293        // update the calculated throughput
1294        self.throughput_calculator
1295            .add_transactions(timestamp, num_schedulables as u64);
1296
1297        fail_point_if!("correlated-crash-after-consensus-commit-boundary", || {
1298            let key = [commit_sub_dag_index, epoch];
1299            if sui_simulator::random::deterministic_probability_once(&key, 0.01) {
1300                sui_simulator::task::kill_current_node(None);
1301            }
1302        });
1303
1304        fail_point!("crash");
1305    }
1306
1307    fn handle_close_epoch(
1308        &self,
1309        state: &mut CommitHandlerState,
1310        commit_info: &ConsensusCommitInfo,
1311        end_of_publish_transactions: Vec<AuthorityName>,
1312    ) -> (
1313        bool,
1314        Option<RwLockWriteGuard<'_, ReconfigState>>,
1315        bool,
1316        Option<AbandonedDeferredTxns>,
1317    ) {
1318        let timestamp_triggered =
1319            commit_info.timestamp >= self.epoch_store.next_reconfiguration_timestamp_ms();
1320        let deadline_reached = self
1321            .epoch_store
1322            .protocol_config()
1323            .epoch_close_deadline_ms_as_option()
1324            .is_some_and(|deadline_ms| {
1325                commit_info.timestamp
1326                    >= self
1327                        .epoch_store
1328                        .next_reconfiguration_timestamp_ms()
1329                        .saturating_add(deadline_ms)
1330            });
1331        let collected_eop_quorum =
1332            self.process_end_of_publish_transactions(state, end_of_publish_transactions);
1333        if timestamp_triggered || collected_eop_quorum {
1334            let (lock, final_round, abandoned_deferred_txns) =
1335                self.advance_end_of_epoch_state_machine(state, deadline_reached);
1336            (
1337                lock.should_accept_tx(),
1338                Some(lock),
1339                final_round,
1340                abandoned_deferred_txns,
1341            )
1342        } else {
1343            (true, None, false, None)
1344        }
1345    }
1346
1347    fn record_end_of_epoch_execution_time_observations(
1348        &self,
1349        estimator: &mut ExecutionTimeEstimator,
1350    ) {
1351        self.epoch_store
1352            .end_of_epoch_execution_time_observations
1353            .set(estimator.take_observations())
1354            .expect("`stored_execution_time_observations` should only be set once at end of epoch");
1355    }
1356
1357    fn record_deferral_deletion(&self, state: &mut CommitHandlerState) {
1358        let mut deferred_transactions = self
1359            .epoch_store
1360            .consensus_output_cache
1361            .deferred_transactions
1362            .lock();
1363        for deleted_deferred_key in state.output.get_deleted_deferred_txn_keys() {
1364            deferred_transactions.remove(&deleted_deferred_key);
1365        }
1366    }
1367
1368    fn log_final_round(&self, lock: Option<RwLockWriteGuard<ReconfigState>>, final_round: bool) {
1369        if final_round {
1370            let epoch = self.epoch_store.epoch();
1371            info!(
1372                ?epoch,
1373                lock=?lock.as_ref(),
1374                final_round=?final_round,
1375                "Notified last checkpoint"
1376            );
1377            self.epoch_store.record_end_of_message_quorum_time_metric();
1378        }
1379    }
1380
1381    #[allow(clippy::type_complexity)]
1382    fn collect_transactions_to_schedule(
1383        &self,
1384        state: &mut CommitHandlerState,
1385        execution_time_estimator: &mut ExecutionTimeEstimator,
1386        commit_info: &ConsensusCommitInfo,
1387        user_transactions: Vec<VerifiedExecutableTransactionWithAliases>,
1388    ) -> (
1389        Vec<VerifiedExecutableTransactionWithAliases>,
1390        Vec<VerifiedExecutableTransactionWithAliases>,
1391        BTreeMap<TransactionDigest, CancelConsensusCertificateReason>,
1392        Option<Schedulable<VerifiedExecutableTransactionWithAliases>>,
1393    ) {
1394        let _scope = monitored_scope("ConsensusCommitHandler::collect_transactions_to_schedule");
1395        let protocol_config = self.epoch_store.protocol_config();
1396        let epoch = self.epoch_store.epoch();
1397
1398        let (ordered_txns, ordered_randomness_txns, previously_deferred_tx_digests) =
1399            self.merge_and_reorder_transactions(state, commit_info, user_transactions);
1400
1401        let mut shared_object_congestion_tracker =
1402            self.init_congestion_tracker(commit_info, false, &ordered_txns);
1403        let mut shared_object_using_randomness_congestion_tracker =
1404            self.init_congestion_tracker(commit_info, true, &ordered_randomness_txns);
1405
1406        let randomness_state_update_transaction = state
1407            .randomness_round
1408            .map(|round| Schedulable::RandomnessStateUpdate(epoch, round));
1409        debug!(
1410            "Randomness state update transaction: {:?}",
1411            randomness_state_update_transaction
1412                .as_ref()
1413                .map(|t| t.key())
1414        );
1415
1416        let mut transactions_to_schedule = Vec::with_capacity(ordered_txns.len());
1417        let mut randomness_transactions_to_schedule =
1418            Vec::with_capacity(ordered_randomness_txns.len());
1419        let mut deferred_txns = BTreeMap::new();
1420        let mut cancelled_txns = BTreeMap::new();
1421
1422        for transaction in ordered_txns {
1423            self.handle_deferral_and_cancellation(
1424                state,
1425                &mut cancelled_txns,
1426                &mut deferred_txns,
1427                &mut transactions_to_schedule,
1428                protocol_config,
1429                commit_info,
1430                transaction,
1431                &mut shared_object_congestion_tracker,
1432                &previously_deferred_tx_digests,
1433                execution_time_estimator,
1434            );
1435        }
1436
1437        for transaction in ordered_randomness_txns {
1438            if state.dkg_failed {
1439                debug!(
1440                    "Canceling randomness-using transaction {:?} because DKG failed",
1441                    transaction.tx().digest(),
1442                );
1443                cancelled_txns.insert(
1444                    *transaction.tx().digest(),
1445                    CancelConsensusCertificateReason::DkgFailed,
1446                );
1447                randomness_transactions_to_schedule.push(transaction);
1448                continue;
1449            }
1450            self.handle_deferral_and_cancellation(
1451                state,
1452                &mut cancelled_txns,
1453                &mut deferred_txns,
1454                &mut randomness_transactions_to_schedule,
1455                protocol_config,
1456                commit_info,
1457                transaction,
1458                &mut shared_object_using_randomness_congestion_tracker,
1459                &previously_deferred_tx_digests,
1460                execution_time_estimator,
1461            );
1462        }
1463
1464        let mut total_deferred_txns = 0;
1465        {
1466            let mut deferred_transactions = self
1467                .epoch_store
1468                .consensus_output_cache
1469                .deferred_transactions
1470                .lock();
1471            for (key, txns) in deferred_txns.into_iter() {
1472                total_deferred_txns += txns.len();
1473                deferred_transactions.insert(key, txns.clone());
1474                state.output.defer_transactions(key, txns);
1475            }
1476        }
1477
1478        self.metrics
1479            .consensus_handler_deferred_transactions
1480            .inc_by(total_deferred_txns as u64);
1481        self.metrics
1482            .consensus_handler_cancelled_transactions
1483            .inc_by(cancelled_txns.len() as u64);
1484        self.metrics
1485            .consensus_handler_max_object_costs
1486            .with_label_values(&["regular_commit"])
1487            .set(shared_object_congestion_tracker.max_cost() as i64);
1488        self.metrics
1489            .consensus_handler_max_object_costs
1490            .with_label_values(&["randomness_commit"])
1491            .set(shared_object_using_randomness_congestion_tracker.max_cost() as i64);
1492
1493        let congestion_commit_data = shared_object_congestion_tracker.finish_commit(commit_info);
1494        let randomness_congestion_commit_data =
1495            shared_object_using_randomness_congestion_tracker.finish_commit(commit_info);
1496
1497        if let Some(logger) = &self.congestion_logger {
1498            let epoch = self.epoch_store.epoch();
1499            let mut logger = logger.lock().unwrap();
1500            logger.write_commit_log(epoch, commit_info, false, &congestion_commit_data);
1501            logger.write_commit_log(epoch, commit_info, true, &randomness_congestion_commit_data);
1502        }
1503
1504        if let Some(tx_object_debts) = self.epoch_store.tx_object_debts.get()
1505            && let Err(e) = tx_object_debts.try_send(
1506                congestion_commit_data
1507                    .accumulated_debts
1508                    .iter()
1509                    .chain(randomness_congestion_commit_data.accumulated_debts.iter())
1510                    .map(|(id, _)| *id)
1511                    .collect(),
1512            )
1513        {
1514            info!("failed to send updated object debts to ExecutionTimeObserver: {e:?}");
1515        }
1516
1517        state
1518            .output
1519            .set_congestion_control_object_debts(congestion_commit_data.accumulated_debts);
1520        state.output.set_congestion_control_randomness_object_debts(
1521            randomness_congestion_commit_data.accumulated_debts,
1522        );
1523
1524        (
1525            transactions_to_schedule,
1526            randomness_transactions_to_schedule,
1527            cancelled_txns,
1528            randomness_state_update_transaction,
1529        )
1530    }
1531
1532    #[allow(clippy::type_complexity)]
1533    fn create_pending_checkpoints(
1534        &self,
1535        state: &mut CommitHandlerState,
1536        commit_info: &ConsensusCommitInfo,
1537        schedulables: Vec<Schedulable<VerifiedExecutableTransactionWithAliases>>,
1538        randomness_schedulables: Vec<Schedulable<VerifiedExecutableTransactionWithAliases>>,
1539        cancelled_txns: &BTreeMap<TransactionDigest, CancelConsensusCertificateReason>,
1540        final_round: bool,
1541    ) -> CheckpointHeight {
1542        let _scope = monitored_scope("ConsensusCommitHandler::create_pending_checkpoints");
1543        let protocol_config = self.epoch_store.protocol_config();
1544        let epoch = self.epoch_store.epoch();
1545        let accumulators_enabled = self.epoch_store.accumulators_enabled();
1546        let max_transactions_per_checkpoint =
1547            protocol_config.max_transactions_per_checkpoint() as usize;
1548
1549        let should_write_random_checkpoint = state.randomness_round.is_some()
1550            || (state.dkg_failed && !randomness_schedulables.is_empty());
1551
1552        let mut checkpoint_queue = self.checkpoint_queue.lock().unwrap();
1553
1554        let build_chunks =
1555            |schedulables: Vec<Schedulable<VerifiedExecutableTransactionWithAliases>>,
1556             queue: &mut CheckpointQueue|
1557             -> Vec<Chunk<VerifiedExecutableTransactionWithAliases>> {
1558                schedulables
1559                    .chunks(max_transactions_per_checkpoint)
1560                    .map(|chunk| {
1561                        let height = queue.next_height();
1562                        let schedulables: Vec<_> = chunk.to_vec();
1563                        let settlement = if accumulators_enabled {
1564                            Some(Schedulable::AccumulatorSettlement(epoch, height))
1565                        } else {
1566                            None
1567                        };
1568                        Chunk {
1569                            schedulables,
1570                            settlement,
1571                            height,
1572                        }
1573                    })
1574                    .collect()
1575            };
1576
1577        let num_schedulables = schedulables.len();
1578        let chunked_schedulables = build_chunks(schedulables, &mut checkpoint_queue);
1579        if chunked_schedulables.len() > 1 {
1580            info!(
1581                "Splitting transactions into {} checkpoint chunks (num_schedulables={}, max_tx={})",
1582                chunked_schedulables.len(),
1583                num_schedulables,
1584                max_transactions_per_checkpoint
1585            );
1586            assert_reachable!("checkpoint split due to transaction limit");
1587        }
1588        let chunked_randomness_schedulables = if should_write_random_checkpoint {
1589            build_chunks(randomness_schedulables, &mut checkpoint_queue)
1590        } else {
1591            vec![]
1592        };
1593
1594        let schedulables_for_version_assignment =
1595            Chunk::all_schedulables_from(&chunked_schedulables);
1596        let randomness_schedulables_for_version_assignment =
1597            Chunk::all_schedulables_from(&chunked_randomness_schedulables);
1598
1599        let assigned_versions = self
1600            .epoch_store
1601            .process_consensus_transaction_shared_object_versions(
1602                self.cache_reader.as_ref(),
1603                schedulables_for_version_assignment,
1604                randomness_schedulables_for_version_assignment,
1605                cancelled_txns,
1606                &mut state.output,
1607            )
1608            .expect("failed to assign shared object versions");
1609
1610        let consensus_commit_prologue =
1611            self.add_consensus_commit_prologue_transaction(state, commit_info, &assigned_versions);
1612
1613        let mut chunked_schedulables = chunked_schedulables;
1614        let mut assigned_versions = assigned_versions;
1615        if let Some(consensus_commit_prologue) = consensus_commit_prologue {
1616            assert!(matches!(
1617                chunked_schedulables[0].schedulables[0],
1618                Schedulable::ConsensusCommitPrologue(..)
1619            ));
1620            assert!(matches!(
1621                assigned_versions.0[0].0,
1622                TransactionKey::ConsensusCommitPrologue(..)
1623            ));
1624            assigned_versions.0[0].0 =
1625                TransactionKey::Digest(*consensus_commit_prologue.tx().digest());
1626            chunked_schedulables[0].schedulables[0] =
1627                Schedulable::Transaction(consensus_commit_prologue);
1628        }
1629
1630        let assigned_versions = assigned_versions.into_map();
1631
1632        self.epoch_store.process_user_signatures(
1633            chunked_schedulables
1634                .iter()
1635                .flat_map(|c| c.all_schedulables())
1636                .chain(
1637                    chunked_randomness_schedulables
1638                        .iter()
1639                        .flat_map(|c| c.all_schedulables()),
1640                ),
1641        );
1642
1643        let commit_height = chunked_randomness_schedulables
1644            .last()
1645            .or(chunked_schedulables.last())
1646            .map(|c| c.height)
1647            .expect("at least one checkpoint root must be created per commit");
1648
1649        let mut pending_checkpoints = Vec::new();
1650        for chunk in chunked_schedulables {
1651            pending_checkpoints.extend(checkpoint_queue.push_chunk(
1652                chunk.into(),
1653                &assigned_versions,
1654                commit_info.timestamp,
1655                commit_info.consensus_commit_ref,
1656                commit_info.rejected_transactions_digest,
1657            ));
1658        }
1659
1660        // We don't want to block checkpoint formation for non-randomness schedulables
1661        // on randomness state update. Therefore, we include randomness chunks in the
1662        // subsequent checkpoint. First we flush the queue, then enqueue randomness
1663        // for merging into the subsequent commit.
1664        pending_checkpoints.extend(checkpoint_queue.flush(commit_info.timestamp, final_round));
1665
1666        if should_write_random_checkpoint {
1667            for chunk in chunked_randomness_schedulables {
1668                pending_checkpoints.extend(checkpoint_queue.push_chunk(
1669                    chunk.into(),
1670                    &assigned_versions,
1671                    commit_info.timestamp,
1672                    commit_info.consensus_commit_ref,
1673                    commit_info.rejected_transactions_digest,
1674                ));
1675            }
1676            if final_round {
1677                pending_checkpoints.extend(checkpoint_queue.flush(commit_info.timestamp, true));
1678            }
1679        }
1680
1681        if final_round && let Some(last) = pending_checkpoints.last_mut() {
1682            last.details.last_of_epoch = true;
1683        }
1684
1685        let queue_drained = checkpoint_queue.is_empty();
1686        drop(checkpoint_queue);
1687
1688        for pending_checkpoint in pending_checkpoints {
1689            debug!(
1690                checkpoint_height = pending_checkpoint.details.checkpoint_height,
1691                roots_count = pending_checkpoint.num_roots(),
1692                "Writing pending checkpoint",
1693            );
1694            self.epoch_store
1695                .write_pending_checkpoint(&mut state.output, &pending_checkpoint)
1696                .expect("failed to write pending checkpoint");
1697        }
1698
1699        state.output.set_checkpoint_queue_drained(queue_drained);
1700
1701        commit_height
1702    }
1703
1704    // Adds the consensus commit prologue transaction to the beginning of input `transactions` to update
1705    // the system clock used in all transactions in the current consensus commit.
1706    // Returns the root of the consensus commit prologue transaction if it was added to the input.
1707    fn add_consensus_commit_prologue_transaction<'a>(
1708        &'a self,
1709        state: &'a mut CommitHandlerState,
1710        commit_info: &'a ConsensusCommitInfo,
1711        assigned_versions: &AssignedTxAndVersions,
1712    ) -> Option<VerifiedExecutableTransactionWithAliases> {
1713        {
1714            if commit_info.skip_consensus_commit_prologue_in_test {
1715                return None;
1716            }
1717        }
1718
1719        let mut cancelled_txn_version_assignment = Vec::new();
1720
1721        for (txn_key, assigned_versions) in assigned_versions.0.iter() {
1722            let Some(d) = txn_key.as_digest() else {
1723                continue;
1724            };
1725
1726            if assigned_versions
1727                .shared_object_versions
1728                .iter()
1729                .any(|(_, version)| version.is_cancelled())
1730            {
1731                assert_reachable!("cancelled transactions");
1732                cancelled_txn_version_assignment
1733                    .push((*d, assigned_versions.shared_object_versions.clone()));
1734            }
1735        }
1736
1737        fail_point_arg!(
1738            "additional_cancelled_txns_for_tests",
1739            |additional_cancelled_txns: Vec<(
1740                TransactionDigest,
1741                Vec<(ConsensusObjectSequenceKey, SequenceNumber)>
1742            )>| {
1743                cancelled_txn_version_assignment.extend(additional_cancelled_txns);
1744            }
1745        );
1746
1747        let transaction = commit_info.create_consensus_commit_prologue_transaction(
1748            self.epoch_store.epoch(),
1749            cancelled_txn_version_assignment,
1750            state.indirect_state_observer.take().unwrap(),
1751        );
1752        Some(VerifiedExecutableTransactionWithAliases::no_aliases(
1753            transaction,
1754        ))
1755    }
1756
1757    fn handle_deferral_and_cancellation(
1758        &self,
1759        state: &mut CommitHandlerState,
1760        cancelled_txns: &mut BTreeMap<TransactionDigest, CancelConsensusCertificateReason>,
1761        deferred_txns: &mut BTreeMap<DeferralKey, Vec<VerifiedExecutableTransactionWithAliases>>,
1762        scheduled_txns: &mut Vec<VerifiedExecutableTransactionWithAliases>,
1763        protocol_config: &ProtocolConfig,
1764        commit_info: &ConsensusCommitInfo,
1765        transaction: VerifiedExecutableTransactionWithAliases,
1766        shared_object_congestion_tracker: &mut SharedObjectCongestionTracker,
1767        previously_deferred_tx_digests: &HashMap<TransactionDigest, DeferralKey>,
1768        execution_time_estimator: &ExecutionTimeEstimator,
1769    ) {
1770        let tx_digest = *transaction.tx().digest();
1771
1772        // Check for unpaid amplification before other deferral checks.
1773        // SIP-45: Paid amplification allows (gas_price / RGP + 1) submissions.
1774        // Transactions with more duplicates than paid for are deferred.
1775        //
1776        // A transaction that names its proposers has already had its amplification bounded by
1777        // validity_check, which sizes the proposer set against the gas price, so there is nothing
1778        // left to charge for here. A set recorded for another epoch is ignored and so bounds
1779        // nothing, leaving the transaction subject to deferral like any other.
1780        if protocol_config.defer_unpaid_amplification()
1781            && !transaction
1782                .tx()
1783                .transaction_data()
1784                .expiration()
1785                .restricts_proposers(self.epoch_store.epoch())
1786        {
1787            let occurrence_count = state
1788                .occurrence_counts
1789                .get(&tx_digest)
1790                .copied()
1791                .unwrap_or(0);
1792
1793            let rgp = self.epoch_store.reference_gas_price();
1794            let gas_price = transaction.tx().transaction_data().gas_price();
1795            let allowed_count = (gas_price / rgp.max(1)) + 1;
1796
1797            if occurrence_count as u64 > allowed_count {
1798                self.metrics
1799                    .consensus_handler_unpaid_amplification_deferrals
1800                    .inc();
1801
1802                let deferred_from_round = previously_deferred_tx_digests
1803                    .get(&tx_digest)
1804                    .map(|k| k.deferred_from_round())
1805                    .unwrap_or(commit_info.round);
1806
1807                let deferral_key = DeferralKey::new_for_consensus_round(
1808                    commit_info.round + 1,
1809                    deferred_from_round,
1810                );
1811
1812                if transaction_deferral_within_limit(
1813                    &deferral_key,
1814                    protocol_config.max_deferral_rounds_for_congestion_control(),
1815                ) {
1816                    assert_reachable!("unpaid amplification deferral");
1817                    debug!(
1818                        "Deferring transaction {:?} due to unpaid amplification (count={}, allowed={})",
1819                        tx_digest, occurrence_count, allowed_count
1820                    );
1821                    deferred_txns
1822                        .entry(deferral_key)
1823                        .or_default()
1824                        .push(transaction);
1825                    return;
1826                }
1827            }
1828        }
1829
1830        // Check for owned object double-spend: if this transaction won an owned object
1831        // lock while another transaction in the same commit tried to lock the same object,
1832        // defer it as a penalty.
1833        if let Some(conflict_info) = state.contested_transaction_digests.get(&tx_digest) {
1834            self.metrics.consensus_handler_double_spend_deferrals.inc();
1835            self.metrics
1836                .consensus_handler_double_spend_conflict_count
1837                .with_label_values(&["gas_object"])
1838                .observe(conflict_info.gas_object_conflicts as f64);
1839            self.metrics
1840                .consensus_handler_double_spend_conflict_count
1841                .with_label_values(&["non_gas_object"])
1842                .observe(conflict_info.non_gas_object_conflicts as f64);
1843            // Attribute the winning side of the conflict to the authority that sequenced the
1844            // contested holder transaction.
1845            self.metrics
1846                .consensus_handler_double_spend_conflicting_authority
1847                .with_label_values(&[
1848                    self.authority_hostname(conflict_info.winner_author),
1849                    "winner",
1850                ])
1851                .inc();
1852
1853            if protocol_config.defer_owned_object_double_spend() {
1854                let deferred_from_round = previously_deferred_tx_digests
1855                    .get(&tx_digest)
1856                    .map(|k| k.deferred_from_round())
1857                    .unwrap_or(commit_info.round);
1858
1859                let deferral_key = DeferralKey::new_for_consensus_round(
1860                    commit_info.round + 1,
1861                    deferred_from_round,
1862                );
1863
1864                if transaction_deferral_within_limit(
1865                    &deferral_key,
1866                    protocol_config.max_deferral_rounds_for_congestion_control(),
1867                ) {
1868                    debug!(
1869                        "Deferring transaction {:?} due to owned object double-spend contention \
1870                        (gas_conflicts={}, non_gas_conflicts={})",
1871                        tx_digest,
1872                        conflict_info.gas_object_conflicts,
1873                        conflict_info.non_gas_object_conflicts,
1874                    );
1875                    assert_reachable!(
1876                        "Successfully deferred transaction attempting to double spend owned object."
1877                    );
1878                    deferred_txns
1879                        .entry(deferral_key)
1880                        .or_default()
1881                        .push(transaction);
1882                    return;
1883                }
1884            }
1885        }
1886
1887        let tx_cost = shared_object_congestion_tracker.get_tx_cost(
1888            execution_time_estimator,
1889            transaction.tx(),
1890            state.indirect_state_observer.as_mut().unwrap(),
1891        );
1892
1893        let deferral_info = self.epoch_store.should_defer(
1894            transaction.tx(),
1895            commit_info,
1896            state.dkg_failed,
1897            state.randomness_round.is_some(),
1898            previously_deferred_tx_digests,
1899            shared_object_congestion_tracker,
1900        );
1901
1902        if let Some((deferral_key, deferral_reason)) = deferral_info {
1903            debug!(
1904                "Deferring consensus certificate for transaction {:?} until {:?}",
1905                tx_digest, deferral_key
1906            );
1907
1908            match deferral_reason {
1909                DeferralReason::RandomnessNotReady => {
1910                    deferred_txns
1911                        .entry(deferral_key)
1912                        .or_default()
1913                        .push(transaction);
1914                }
1915                DeferralReason::SharedObjectCongestion(congested_objects) => {
1916                    self.metrics.consensus_handler_congested_transactions.inc();
1917                    if transaction_deferral_within_limit(
1918                        &deferral_key,
1919                        protocol_config.max_deferral_rounds_for_congestion_control(),
1920                    ) {
1921                        deferred_txns
1922                            .entry(deferral_key)
1923                            .or_default()
1924                            .push(transaction);
1925                    } else {
1926                        assert_sometimes!(
1927                            transaction.tx().data().transaction_data().uses_randomness(),
1928                            "cancelled randomness-using transaction"
1929                        );
1930                        assert_sometimes!(
1931                            !transaction.tx().data().transaction_data().uses_randomness(),
1932                            "cancelled non-randomness-using transaction"
1933                        );
1934
1935                        // Cancel the transaction that has been deferred for too long.
1936                        debug!(
1937                            "Cancelling consensus transaction {:?} with deferral key {:?} due to congestion on objects {:?}",
1938                            tx_digest, deferral_key, congested_objects
1939                        );
1940                        cancelled_txns.insert(
1941                            tx_digest,
1942                            CancelConsensusCertificateReason::CongestionOnObjects(
1943                                congested_objects,
1944                            ),
1945                        );
1946                        scheduled_txns.push(transaction);
1947                    }
1948                }
1949            }
1950        } else {
1951            // Update object execution cost for all scheduled transactions
1952            shared_object_congestion_tracker.bump_object_execution_cost(tx_cost, transaction.tx());
1953            scheduled_txns.push(transaction);
1954        }
1955    }
1956
1957    fn merge_and_reorder_transactions(
1958        &self,
1959        state: &mut CommitHandlerState,
1960        commit_info: &ConsensusCommitInfo,
1961        user_transactions: Vec<VerifiedExecutableTransactionWithAliases>,
1962    ) -> (
1963        Vec<VerifiedExecutableTransactionWithAliases>,
1964        Vec<VerifiedExecutableTransactionWithAliases>,
1965        HashMap<TransactionDigest, DeferralKey>,
1966    ) {
1967        let protocol_config = self.epoch_store.protocol_config();
1968
1969        let (mut txns, mut randomness_txns, previously_deferred_tx_digests) =
1970            self.load_deferred_transactions(state, commit_info);
1971
1972        txns.reserve(user_transactions.len());
1973        randomness_txns.reserve(user_transactions.len());
1974
1975        // There may be randomness transactions in `txns`, which were deferred due to congestion.
1976        // They must be placed back into `randomness_txns`.
1977        let mut txns: Vec<_> = txns
1978            .into_iter()
1979            .filter_map(|tx| {
1980                if tx.tx().transaction_data().uses_randomness() {
1981                    randomness_txns.push(tx);
1982                    None
1983                } else {
1984                    Some(tx)
1985                }
1986            })
1987            .collect();
1988
1989        for txn in user_transactions {
1990            if txn.tx().transaction_data().uses_randomness() {
1991                randomness_txns.push(txn);
1992            } else {
1993                txns.push(txn);
1994            }
1995        }
1996
1997        PostConsensusTxReorder::reorder(
1998            &mut txns,
1999            protocol_config.consensus_transaction_ordering(),
2000        );
2001        PostConsensusTxReorder::reorder(
2002            &mut randomness_txns,
2003            protocol_config.consensus_transaction_ordering(),
2004        );
2005
2006        (txns, randomness_txns, previously_deferred_tx_digests)
2007    }
2008
2009    fn load_deferred_transactions(
2010        &self,
2011        state: &mut CommitHandlerState,
2012        commit_info: &ConsensusCommitInfo,
2013    ) -> (
2014        Vec<VerifiedExecutableTransactionWithAliases>,
2015        Vec<VerifiedExecutableTransactionWithAliases>,
2016        HashMap<TransactionDigest, DeferralKey>,
2017    ) {
2018        let mut previously_deferred_tx_digests = HashMap::new();
2019
2020        let deferred_txs: Vec<_> = self
2021            .epoch_store
2022            .load_deferred_transactions_for_up_to_consensus_round_v2(
2023                &mut state.output,
2024                commit_info.round,
2025            )
2026            .expect("db error")
2027            .into_iter()
2028            .flat_map(|(key, txns)| txns.into_iter().map(move |tx| (key, tx)))
2029            .map(|(key, tx)| {
2030                previously_deferred_tx_digests.insert(*tx.tx().digest(), key);
2031                tx
2032            })
2033            .collect();
2034        trace!(
2035            "loading deferred transactions: {:?}",
2036            deferred_txs.iter().map(|tx| tx.tx().digest())
2037        );
2038
2039        let deferred_randomness_txs = if state.dkg_failed || state.randomness_round.is_some() {
2040            let txns: Vec<_> = self
2041                .epoch_store
2042                .load_deferred_transactions_for_randomness_v2(&mut state.output)
2043                .expect("db error")
2044                .into_iter()
2045                .flat_map(|(key, txns)| txns.into_iter().map(move |tx| (key, tx)))
2046                .map(|(key, tx)| {
2047                    previously_deferred_tx_digests.insert(*tx.tx().digest(), key);
2048                    tx
2049                })
2050                .collect();
2051            trace!(
2052                "loading deferred randomness transactions: {:?}",
2053                txns.iter().map(|tx| tx.tx().digest())
2054            );
2055            txns
2056        } else {
2057            vec![]
2058        };
2059
2060        (
2061            deferred_txs,
2062            deferred_randomness_txs,
2063            previously_deferred_tx_digests,
2064        )
2065    }
2066
2067    fn init_congestion_tracker(
2068        &self,
2069        commit_info: &ConsensusCommitInfo,
2070        for_randomness: bool,
2071        txns: &[VerifiedExecutableTransactionWithAliases],
2072    ) -> SharedObjectCongestionTracker {
2073        #[allow(unused_mut)]
2074        let mut ret = SharedObjectCongestionTracker::from_protocol_config(
2075            self.epoch_store
2076                .consensus_quarantine
2077                .read()
2078                .load_initial_object_debts(
2079                    &self.epoch_store,
2080                    commit_info.round,
2081                    for_randomness,
2082                    txns,
2083                )
2084                .expect("db error"),
2085            self.epoch_store.protocol_config(),
2086            for_randomness,
2087            self.congestion_logger.is_some(),
2088        );
2089
2090        fail_point_arg!(
2091            "initial_congestion_tracker",
2092            |tracker: SharedObjectCongestionTracker| {
2093                info!(
2094                    "Initialize shared_object_congestion_tracker to  {:?}",
2095                    tracker
2096                );
2097                ret = tracker;
2098            }
2099        );
2100
2101        ret
2102    }
2103
2104    fn process_gasless_transactions(
2105        &self,
2106        commit_info: &ConsensusCommitInfo,
2107        user_transactions: &[VerifiedExecutableTransactionWithAliases],
2108    ) {
2109        let gasless_count = user_transactions
2110            .iter()
2111            .filter(|txn| txn.tx().transaction_data().is_gasless_transaction())
2112            .count() as u64;
2113        self.consensus_gasless_counter
2114            .record_commit(commit_info.timestamp, gasless_count);
2115    }
2116
2117    fn process_jwks(
2118        &self,
2119        state: &mut CommitHandlerState,
2120        commit_info: &ConsensusCommitInfo,
2121        new_jwks: Vec<(AuthorityName, JwkId, JWK)>,
2122    ) {
2123        for (authority_name, jwk_id, jwk) in new_jwks {
2124            self.epoch_store.record_jwk_vote(
2125                &mut state.output,
2126                commit_info.round,
2127                authority_name,
2128                &jwk_id,
2129                &jwk,
2130            );
2131        }
2132    }
2133
2134    fn process_capability_notifications(
2135        &self,
2136        capability_notifications: Vec<AuthorityCapabilitiesV2>,
2137    ) {
2138        for capabilities in capability_notifications {
2139            self.epoch_store
2140                .record_capabilities_v2(&capabilities)
2141                .expect("db error");
2142        }
2143    }
2144
2145    /// Applies deny-config updates at commit time. The live path already applies them at
2146    /// block verification in `SuiTxValidator` (re-application here is dropped as a stale
2147    /// generation), but a validator that is catching up can process commits without
2148    /// verifying every block in them, so committed updates must also be applied here.
2149    fn process_transaction_deny_config_updates(
2150        &self,
2151        updates: Vec<(AuthorityName, SharedTransactionDenyConfig)>,
2152    ) {
2153        for (author, update) in updates {
2154            self.transaction_deny_config_manager
2155                .apply_updates(author, vec![update]);
2156        }
2157    }
2158
2159    fn process_execution_time_observations(
2160        &self,
2161        state: &mut CommitHandlerState,
2162        execution_time_observations: Vec<ExecutionTimeObservation>,
2163    ) {
2164        let _scope = monitored_scope("ConsensusCommitHandler::process_execution_time_observations");
2165        let mut execution_time_estimator = self
2166            .epoch_store
2167            .execution_time_estimator
2168            .try_lock()
2169            .expect("should only ever be called from the commit handler thread");
2170
2171        for ExecutionTimeObservation {
2172            authority,
2173            generation,
2174            estimates,
2175        } in execution_time_observations
2176        {
2177            let authority_index = self
2178                .epoch_store
2179                .committee()
2180                .authority_index(&authority)
2181                .unwrap();
2182            execution_time_estimator.process_observations_from_consensus(
2183                authority_index,
2184                Some(generation),
2185                &estimates,
2186            );
2187            state
2188                .output
2189                .insert_execution_time_observation(authority_index, generation, estimates);
2190        }
2191    }
2192
2193    fn process_checkpoint_signature_messages(
2194        &self,
2195        checkpoint_signature_messages: Vec<CheckpointSignatureMessage>,
2196    ) {
2197        for checkpoint_signature_message in checkpoint_signature_messages {
2198            self.checkpoint_service
2199                .notify_checkpoint_signature(&checkpoint_signature_message)
2200                .expect("db error");
2201        }
2202    }
2203
2204    async fn process_dkg_updates(
2205        &self,
2206        state: &mut CommitHandlerState,
2207        commit_info: &ConsensusCommitInfo,
2208        randomness_manager: Option<&mut RandomnessManager>,
2209        randomness_dkg_messages: Vec<(AuthorityName, Vec<u8>)>,
2210        randomness_dkg_confirmations: Vec<(AuthorityName, Vec<u8>)>,
2211    ) {
2212        if !self.epoch_store.randomness_state_enabled() {
2213            let num_dkg_messages = randomness_dkg_messages.len();
2214            let num_dkg_confirmations = randomness_dkg_confirmations.len();
2215            if num_dkg_messages + num_dkg_confirmations > 0 {
2216                debug_fatal!(
2217                    "received {} RandomnessDkgMessage and {} RandomnessDkgConfirmation messages when randomness is not enabled",
2218                    num_dkg_messages,
2219                    num_dkg_confirmations
2220                );
2221            }
2222            return;
2223        }
2224
2225        let randomness_manager =
2226            randomness_manager.expect("randomness manager should exist if randomness is enabled");
2227
2228        let randomness_dkg_updates =
2229            self.process_randomness_dkg_messages(randomness_manager, randomness_dkg_messages);
2230
2231        let randomness_dkg_confirmation_updates = self.process_randomness_dkg_confirmations(
2232            state,
2233            randomness_manager,
2234            randomness_dkg_confirmations,
2235        );
2236
2237        // Keep advancing the DKG state machine until it is resolved, regardless of
2238        // whether new messages/confirmations were processed this commit. Preserve
2239        // the mainnet epoch fallback until the protocol flag is active everywhere.
2240        let always_advance_dkg_to_resolution = (self
2241            .epoch_store
2242            .protocol_config()
2243            .always_advance_dkg_to_resolution()
2244            || (self.epoch_store.get_chain() == Chain::Mainnet
2245                && self.epoch_store.epoch() >= 1143))
2246            && randomness_manager.dkg_status() == DkgStatus::Pending;
2247
2248        if randomness_dkg_updates
2249            || randomness_dkg_confirmation_updates
2250            || always_advance_dkg_to_resolution
2251        {
2252            randomness_manager
2253                .advance_dkg(&mut state.output, commit_info.round)
2254                .await
2255                .expect("epoch ended");
2256        }
2257    }
2258
2259    fn process_randomness_dkg_messages(
2260        &self,
2261        randomness_manager: &mut RandomnessManager,
2262        randomness_dkg_messages: Vec<(AuthorityName, Vec<u8>)>,
2263    ) -> bool /* randomness state updated */ {
2264        if randomness_dkg_messages.is_empty() {
2265            return false;
2266        }
2267
2268        let mut randomness_state_updated = false;
2269        for (authority, bytes) in randomness_dkg_messages {
2270            match bcs::from_bytes(&bytes) {
2271                Ok(message) => {
2272                    randomness_manager
2273                        .add_message(&authority, message)
2274                        // TODO: make infallible
2275                        .expect("epoch ended");
2276                    randomness_state_updated = true;
2277                }
2278
2279                Err(e) => {
2280                    warn!(
2281                        "Failed to deserialize RandomnessDkgMessage from {:?}: {e:?}",
2282                        authority.concise(),
2283                    );
2284                }
2285            }
2286        }
2287
2288        randomness_state_updated
2289    }
2290
2291    fn process_randomness_dkg_confirmations(
2292        &self,
2293        state: &mut CommitHandlerState,
2294        randomness_manager: &mut RandomnessManager,
2295        randomness_dkg_confirmations: Vec<(AuthorityName, Vec<u8>)>,
2296    ) -> bool /* randomness state updated */ {
2297        if randomness_dkg_confirmations.is_empty() {
2298            return false;
2299        }
2300
2301        let mut randomness_state_updated = false;
2302        for (authority, bytes) in randomness_dkg_confirmations {
2303            match bcs::from_bytes(&bytes) {
2304                Ok(message) => {
2305                    randomness_manager
2306                        .add_confirmation(&mut state.output, &authority, message)
2307                        // TODO: make infallible
2308                        .expect("epoch ended");
2309                    randomness_state_updated = true;
2310                }
2311                Err(e) => {
2312                    warn!(
2313                        "Failed to deserialize RandomnessDkgConfirmation from {:?}: {e:?}",
2314                        authority.concise(),
2315                    );
2316                }
2317            }
2318        }
2319
2320        randomness_state_updated
2321    }
2322
2323    /// Returns true if we have collected a quorum of end of publish messages (either in this round or a previous round).
2324    fn process_end_of_publish_transactions(
2325        &self,
2326        state: &mut CommitHandlerState,
2327        end_of_publish_transactions: Vec<AuthorityName>,
2328    ) -> bool {
2329        let mut eop_aggregator = self.epoch_store.end_of_publish.try_lock().expect(
2330            "No contention on end_of_publish as it is only accessed from consensus handler",
2331        );
2332
2333        if eop_aggregator.has_quorum() {
2334            return true;
2335        }
2336
2337        if end_of_publish_transactions.is_empty() {
2338            return false;
2339        }
2340
2341        for authority in end_of_publish_transactions {
2342            info!("Received EndOfPublish from {:?}", authority.concise());
2343
2344            // It is ok to just release lock here as this function is the only place that transition into RejectAllCerts state
2345            // And this function itself is always executed from consensus task
2346            state.output.insert_end_of_publish(authority);
2347            if eop_aggregator
2348                .insert_generic(authority, ())
2349                .is_quorum_reached()
2350            {
2351                debug!(
2352                    "Collected enough end_of_publish messages with last message from validator {:?}",
2353                    authority.concise(),
2354                );
2355                return true;
2356            }
2357        }
2358
2359        false
2360    }
2361
2362    /// Once the timestamp deadline is reached or 2f+1 EndOfPublish messages are collected, we call
2363    /// this function every round until the epoch ends.
2364    fn advance_end_of_epoch_state_machine(
2365        &self,
2366        state: &mut CommitHandlerState,
2367        deadline_reached: bool,
2368    ) -> (
2369        RwLockWriteGuard<'_, ReconfigState>,
2370        bool, // true if final round
2371        Option<AbandonedDeferredTxns>,
2372    ) {
2373        let mut reconfig_state = self.epoch_store.get_reconfig_state_write_lock_guard();
2374        let start_state_is_reject_all_tx = reconfig_state.is_reject_all_tx();
2375
2376        // Record the close time only on the first entry into the state machine.
2377        // A manual epoch close records it when closing user certs.
2378        if reconfig_state.should_accept_user_certs() {
2379            self.epoch_store.record_epoch_close_time_once();
2380        }
2381
2382        reconfig_state.close_all_certs();
2383
2384        let commit_has_deferred_txns = state.output.has_deferred_transactions();
2385        let previous_commits_have_deferred_txns = !self.epoch_store.deferred_transactions_empty();
2386        let has_deferred_txns = commit_has_deferred_txns || previous_commits_have_deferred_txns;
2387
2388        // The epoch closes normally only once all deferred transactions have been scheduled;
2389        // past the deadline it closes regardless, abandoning whatever remains.
2390        let should_close = !has_deferred_txns || deadline_reached;
2391        let final_round = should_close && !start_state_is_reject_all_tx;
2392
2393        let mut abandoned_deferred_txns = None;
2394        if final_round {
2395            info!("Transitioning to RejectAllTx");
2396            if has_deferred_txns {
2397                // Closing with deferred transactions remaining implies the deadline forced it.
2398                debug_assert!(deadline_reached);
2399                abandoned_deferred_txns = self.abandon_deferred_transactions(state);
2400            }
2401            reconfig_state.close_all_tx();
2402        } else if !should_close {
2403            debug!(
2404                "Blocking end of epoch on deferred transactions, from previous commits?={}, from this commit?={}",
2405                previous_commits_have_deferred_txns, commit_has_deferred_txns,
2406            );
2407        }
2408
2409        state.output.store_reconfig_state(reconfig_state.clone());
2410
2411        (reconfig_state, final_round, abandoned_deferred_txns)
2412    }
2413
2414    /// Abandons every deferred transaction still unscheduled when the epoch close deadline
2415    /// forces the epoch closed: drops this commit's newly staged deferrals and stages all
2416    /// remaining deferral keys for deletion.
2417    ///
2418    /// (Without this, post-close commits would reload and re-defer them forever, growing the
2419    /// cache without bound.)
2420    fn abandon_deferred_transactions(
2421        &self,
2422        state: &mut CommitHandlerState,
2423    ) -> Option<AbandonedDeferredTxns> {
2424        state.output.clear_deferred_transactions();
2425
2426        let already_deleted: BTreeSet<_> = state.output.get_deleted_deferred_txn_keys().collect();
2427        let mut count = 0;
2428        let mut sample = Vec::new();
2429        let mut abandoned_keys = Vec::new();
2430        {
2431            let deferred_transactions = self
2432                .epoch_store
2433                .consensus_output_cache
2434                .deferred_transactions
2435                .lock();
2436            for (key, txns) in deferred_transactions.iter() {
2437                if already_deleted.contains(key) {
2438                    // Loaded and scheduled by this commit; not abandoned.
2439                    continue;
2440                }
2441                abandoned_keys.push(*key);
2442                count += txns.len();
2443                for tx in txns {
2444                    if sample.len() < 10 {
2445                        sample.push((*key, *tx.tx().digest()));
2446                    }
2447                }
2448            }
2449        }
2450        if abandoned_keys.is_empty() {
2451            return None;
2452        }
2453        state
2454            .output
2455            .delete_loaded_deferred_transactions(&abandoned_keys);
2456        (count > 0).then_some(AbandonedDeferredTxns { count, sample })
2457    }
2458
2459    fn gather_commit_metadata(
2460        &self,
2461        consensus_commit: &impl ConsensusCommitAPI,
2462    ) -> (u64, AuthorityIndex, u64) {
2463        let timestamp = consensus_commit.commit_timestamp_ms();
2464        let leader_author = consensus_commit.leader_author_index();
2465        let commit_sub_dag_index = consensus_commit.commit_sub_dag_index();
2466
2467        let system_time_ms = SystemTime::now()
2468            .duration_since(UNIX_EPOCH)
2469            .unwrap()
2470            .as_millis() as i64;
2471
2472        let consensus_timestamp_bias_ms = system_time_ms - (timestamp as i64);
2473        let consensus_timestamp_bias_seconds = consensus_timestamp_bias_ms as f64 / 1000.0;
2474        self.metrics
2475            .consensus_timestamp_bias
2476            .observe(consensus_timestamp_bias_seconds);
2477
2478        let epoch_start = self
2479            .epoch_store
2480            .epoch_start_config()
2481            .epoch_start_timestamp_ms();
2482        let timestamp = if timestamp < epoch_start {
2483            error!(
2484                "Unexpected commit timestamp {timestamp} less then epoch start time {epoch_start}, author {leader_author}"
2485            );
2486            epoch_start
2487        } else {
2488            timestamp
2489        };
2490
2491        (timestamp, leader_author, commit_sub_dag_index)
2492    }
2493
2494    fn create_authenticator_state_update(
2495        &self,
2496        last_committed_round: u64,
2497        commit_info: &ConsensusCommitInfo,
2498    ) -> Option<VerifiedExecutableTransactionWithAliases> {
2499        // Load all jwks that became active in the previous round, and commit them in this round.
2500        // We want to delay one round because none of the transactions in the previous round could
2501        // have been authenticated with the jwks that became active in that round.
2502        //
2503        // Because of this delay, jwks that become active in the last round of the epoch will
2504        // never be committed. That is ok, because in the new epoch, the validators should
2505        // immediately re-submit these jwks, and they can become active then.
2506        let new_jwks = self
2507            .epoch_store
2508            .get_new_jwks(last_committed_round)
2509            .expect("Unrecoverable error in consensus handler");
2510
2511        if !new_jwks.is_empty() {
2512            let authenticator_state_update_transaction = authenticator_state_update_transaction(
2513                &self.epoch_store,
2514                commit_info.round,
2515                new_jwks,
2516            );
2517            debug!(
2518                "adding AuthenticatorStateUpdate({:?}) tx: {:?}",
2519                authenticator_state_update_transaction.digest(),
2520                authenticator_state_update_transaction,
2521            );
2522
2523            Some(VerifiedExecutableTransactionWithAliases::no_aliases(
2524                authenticator_state_update_transaction,
2525            ))
2526        } else {
2527            None
2528        }
2529    }
2530
2531    // Returns the hostname of the block author at the given committee index, used as a metric
2532    // label. Falls back to "unknown" if the index is out of bounds.
2533    fn authority_hostname(&self, author: usize) -> &str {
2534        self.committee
2535            .to_authority_index(author)
2536            .map(|index| self.committee.authority(index).hostname.as_str())
2537            .unwrap_or("unknown")
2538    }
2539
2540    // Filters out rejected or deprecated transactions.
2541    // Returns FilteredConsensusOutput containing transactions and owned_object_locks.
2542    #[instrument(level = "trace", skip_all)]
2543    fn filter_consensus_txns(
2544        &mut self,
2545        initial_reconfig_state: ReconfigState,
2546        commit_info: &ConsensusCommitInfo,
2547        block_transactions: ParsedConsensusTransactions,
2548    ) -> FilteredConsensusOutput {
2549        let _scope = monitored_scope("ConsensusCommitHandler::filter_consensus_txns");
2550        let mut transactions = Vec::new();
2551        let mut owned_object_locks = HashMap::new();
2552        let mut dropped_transaction_keys = Vec::new();
2553        // Consensus transaction status updates are collected here and flushed in a
2554        // single batched write (one lock acquisition, notifications outside the lock)
2555        // at the end of the commit, rather than one write+notify per transaction.
2556        let mut status_updates: Vec<(ConsensusPosition, ConsensusTxStatus)> = Vec::new();
2557        let mut contested_transaction_digests: HashMap<TransactionDigest, ConflictInfo> =
2558            HashMap::new();
2559        // Block author for each transaction that successfully acquired owned object locks, so we
2560        // can attribute the winning side of a double-spend conflict to the authority that
2561        // sequenced it.
2562        let mut lock_holder_authors: HashMap<TransactionDigest, usize> = HashMap::new();
2563        let epoch = self.epoch_store.epoch();
2564        let mut num_finalized_user_transactions = vec![0; self.committee.size()];
2565        let mut num_rejected_user_transactions = vec![0; self.committee.size()];
2566        let mut num_dropped_user_transactions = vec![0; self.committee.size()];
2567
2568        // Prefetch the cross-commit owned-object lock state for the whole commit in one
2569        // batched read. These locks are constant for the duration of a commit (new locks
2570        // are only written at commit end), so this replaces the per-transaction
2571        // quarantine+DB lookup that try_acquire_owned_object_locks_post_consensus used to
2572        // do. Over-reading refs of transactions that are later filtered out is harmless.
2573        let existing_locks = {
2574            let mut prefetch_refs: Vec<ObjectRef> = Vec::new();
2575            for (_block, parsed_transactions) in &block_transactions {
2576                for parsed in parsed_transactions {
2577                    if let ConsensusTransactionKind::UserTransactionV2(tx_with_claims) =
2578                        &parsed.transaction.kind
2579                        && let Some(refs) = owned_object_refs_to_lock(tx_with_claims)
2580                    {
2581                        prefetch_refs.extend(refs);
2582                    }
2583                }
2584            }
2585            prefetch_refs.sort();
2586            prefetch_refs.dedup();
2587            // On a read error fall back to an empty map (treat refs as unlocked) — the
2588            // same lenient behavior the per-transaction read had.
2589            self.epoch_store
2590                .get_owned_object_locks_map(&prefetch_refs)
2591                .unwrap_or_default()
2592        };
2593
2594        for (block, parsed_transactions) in block_transactions {
2595            let author = block.author.value();
2596            let author_hostname = self.committee.authority(block.author).hostname.as_str();
2597            // TODO: consider only messages within 1~3 rounds of the leader?
2598            self.last_consensus_stats.stats.inc_num_messages(author);
2599
2600            // Set the "ping" transaction status for this block. This is necessary as there might be some ping requests waiting for the ping transaction to be certified.
2601            status_updates.push((
2602                ConsensusPosition::ping(epoch, block),
2603                ConsensusTxStatus::Finalized,
2604            ));
2605
2606            for (tx_index, parsed) in parsed_transactions.into_iter().enumerate() {
2607                let position = ConsensusPosition {
2608                    epoch,
2609                    block,
2610                    index: tx_index as TransactionIndex,
2611                };
2612
2613                // Transaction has appeared in consensus output, we can increment the submission count
2614                // for this tx for DoS protection.
2615                if let Some(tx) = parsed.transaction.kind.as_user_transaction() {
2616                    let digest = tx.digest();
2617                    if let Some((spam_weight, submitter_client_addrs)) = self
2618                        .epoch_store
2619                        .submitted_transaction_cache
2620                        .increment_submission_count(digest)
2621                    {
2622                        if let Some(ref traffic_controller) = self.traffic_controller {
2623                            debug!(
2624                                "Transaction {digest} exceeded submission limits, spam_weight: {spam_weight:?} applied to {} client addresses",
2625                                submitter_client_addrs.len()
2626                            );
2627
2628                            // Apply spam weight to all client addresses that submitted this transaction
2629                            for addr in submitter_client_addrs {
2630                                traffic_controller.tally(
2631                                    TrafficTally::new(Some(addr), None, None, spam_weight.clone())
2632                                        .with_method(
2633                                            "consensus_submission_limit_exceeded".to_string(),
2634                                        ),
2635                                );
2636                            }
2637                        } else {
2638                            warn!(
2639                                "Transaction {digest} exceeded submission limits, spam_weight: {spam_weight:?} for {} client addresses (traffic controller not configured)",
2640                                submitter_client_addrs.len()
2641                            );
2642                        }
2643                    }
2644                }
2645
2646                // Record metrics for every committed transaction, regardless of whether it was
2647                // accepted or rejected by consensus, so we measure the full committed output.
2648                let kind = classify(&parsed.transaction);
2649                let outcome = if parsed.rejected {
2650                    "rejected"
2651                } else {
2652                    "accepted"
2653                };
2654                self.metrics
2655                    .consensus_handler_processed
2656                    .with_label_values(&[kind, outcome])
2657                    .inc();
2658                self.metrics.observe_consensus_handler_transaction_size(
2659                    kind,
2660                    outcome,
2661                    parsed.serialized_len,
2662                );
2663                // Per-author breakdown is only tracked for user transactions, since that is where
2664                // rejections and author-attributable spam are meaningful. Keeping the block author
2665                // label scoped to user transactions also bounds metric cardinality.
2666                if parsed.transaction.is_user_transaction() {
2667                    self.metrics
2668                        .consensus_handler_processed_user_transactions
2669                        .with_label_values(&[outcome, author_hostname])
2670                        .inc();
2671                }
2672
2673                if parsed.rejected {
2674                    if parsed.transaction.is_user_transaction() {
2675                        status_updates.push((position, ConsensusTxStatus::Rejected));
2676                        num_rejected_user_transactions[author] += 1;
2677                    }
2678                    // Skip processing rejected transactions.
2679                    continue;
2680                }
2681
2682                if parsed.transaction.is_user_transaction() {
2683                    self.last_consensus_stats
2684                        .stats
2685                        .inc_num_user_transactions(author);
2686                }
2687
2688                if !initial_reconfig_state.should_accept_consensus_certs() {
2689                    // (Note: we no longer need to worry about the previously deferred condition, since we are only
2690                    // processing newly-received transactions at this time).
2691                    match &parsed.transaction.kind {
2692                        ConsensusTransactionKind::UserTransactionV2(_)
2693                        // deprecated and ignore later, but added for exhaustive match
2694                        | ConsensusTransactionKind::UserTransaction(_)
2695                        | ConsensusTransactionKind::CertifiedTransaction(_)
2696                        | ConsensusTransactionKind::CapabilityNotification(_)
2697                        | ConsensusTransactionKind::CapabilityNotificationV2(_)
2698                        | ConsensusTransactionKind::EndOfPublish(_)
2699                        // Note: we no longer have to check protocol_config.ignore_execution_time_observations_after_certs_closed()
2700                        | ConsensusTransactionKind::ExecutionTimeObservation(_)
2701                        | ConsensusTransactionKind::NewJWKFetched(_, _, _)
2702                        | ConsensusTransactionKind::UpdateTransactionDenyConfig(_) => {
2703                            // Deterministic drop: the reconfig state is derived from prior
2704                            // commits, so all validators ignore this transaction. Record a
2705                            // terminal status for user transactions so status waiters are
2706                            // not leaked — no consensus-processed signal will ever come for
2707                            // them, and epoch termination (the only other backstop) can
2708                            // itself stall when reconfiguration is stuck.
2709                            if parsed.transaction.is_user_transaction() {
2710                                status_updates.push((position, ConsensusTxStatus::Dropped));
2711                                num_dropped_user_transactions[author] += 1;
2712                                self.metrics
2713                                    .consensus_handler_dropped_transactions
2714                                    .with_label_values(&["end_of_epoch"])
2715                                    .inc();
2716                            }
2717                            debug!(
2718                                "Ignoring consensus transaction {:?} because of end of epoch",
2719                                parsed.transaction.key()
2720                            );
2721                            continue;
2722                        }
2723
2724                        // These are the message types that are still processed even if !should_accept_consensus_certs()
2725                        ConsensusTransactionKind::CheckpointSignature(_)
2726                        | ConsensusTransactionKind::CheckpointSignatureV2(_)
2727                        | ConsensusTransactionKind::RandomnessStateUpdate(_, _)
2728                        | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
2729                        | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _) => ()
2730                    }
2731                }
2732
2733                if !initial_reconfig_state.should_accept_tx() {
2734                    match &parsed.transaction.kind {
2735                        ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
2736                        | ConsensusTransactionKind::RandomnessDkgMessage(_, _) => continue,
2737                        _ => {}
2738                    }
2739                }
2740
2741                // Handle deprecated messages
2742                match &parsed.transaction.kind {
2743                    ConsensusTransactionKind::CapabilityNotification(_)
2744                    | ConsensusTransactionKind::RandomnessStateUpdate(_, _)
2745                    | ConsensusTransactionKind::CheckpointSignature(_) => {
2746                        debug_fatal!(
2747                            "BUG: saw deprecated tx {:?}for commit round {}",
2748                            parsed.transaction.key(),
2749                            commit_info.round
2750                        );
2751                        continue;
2752                    }
2753                    _ => {}
2754                }
2755
2756                if parsed.transaction.is_user_transaction() {
2757                    let author_name = self
2758                        .epoch_store
2759                        .committee()
2760                        .authority_by_index(author as u32)
2761                        .unwrap();
2762                    if self
2763                        .epoch_store
2764                        .has_received_end_of_publish_from(author_name)
2765                    {
2766                        // In some edge cases, consensus might resend previously seen certificate after EndOfPublish
2767                        // An honest validator should not send a new transaction after EndOfPublish. Whether the
2768                        // transaction is duplicate or not, we filter it out here.
2769                        // Deterministic drop (the EndOfPublish set is accumulated from
2770                        // prior commits): record a terminal status so waiters are not
2771                        // leaked, as with the certs-closed drop above.
2772                        status_updates.push((position, ConsensusTxStatus::Dropped));
2773                        num_dropped_user_transactions[author] += 1;
2774                        self.metrics
2775                            .consensus_handler_dropped_transactions
2776                            .with_label_values(&["end_of_publish"])
2777                            .inc();
2778                        warn!(
2779                            "Ignoring consensus transaction {:?} from authority {:?}, which already sent EndOfPublish message to consensus",
2780                            author_name.concise(),
2781                            parsed.transaction.key(),
2782                        );
2783                        continue;
2784                    }
2785                }
2786
2787                // Perform post-consensus owned object conflict detection. If lock acquisition
2788                // fails, the transaction has invalid/conflicting owned inputs and should be dropped.
2789                // This must happen AFTER all filtering checks above to avoid acquiring locks
2790                // for transactions that will be dropped (e.g., during epoch change).
2791                // Only applies to UserTransactionV2 - other transaction types don't need lock acquisition.
2792                if let ConsensusTransactionKind::UserTransactionV2(tx_with_claims) =
2793                    &parsed.transaction.kind
2794                {
2795                    let tx = tx_with_claims.tx();
2796                    let Some(owned_object_refs) = owned_object_refs_to_lock(tx_with_claims) else {
2797                        // Invalid input object error is deterministic across all validators.
2798                        self.metrics
2799                            .consensus_handler_dropped_transactions
2800                            .with_label_values(&["invalid_input"])
2801                            .inc();
2802                        status_updates.push((position, ConsensusTxStatus::Dropped));
2803                        num_dropped_user_transactions[author] += 1;
2804                        // Record the concrete input error as the reject reason so effects
2805                        // waiters get a terminal, non-retriable error instead of a bare
2806                        // Dropped with no reason (which clients treat as retriable).
2807                        if let Err(e) = tx.transaction_data().input_objects() {
2808                            self.epoch_store
2809                                .set_rejection_vote_reason(position, &e.into());
2810                        }
2811                        dropped_transaction_keys.push(parsed.transaction.key());
2812                        debug_fatal!("Invalid input objects for transaction {}", tx.digest());
2813                        continue;
2814                    };
2815
2816                    match self
2817                        .epoch_store
2818                        .try_acquire_owned_object_locks_post_consensus(
2819                            &owned_object_refs,
2820                            *tx.digest(),
2821                            &owned_object_locks,
2822                            &existing_locks,
2823                        ) {
2824                        Ok(new_locks) => {
2825                            owned_object_locks.extend(new_locks.into_iter());
2826                            lock_holder_authors.entry(*tx.digest()).or_insert(author);
2827                            // Lock acquisition succeeded - now set Finalized status
2828                            status_updates.push((position, ConsensusTxStatus::Finalized));
2829                            num_finalized_user_transactions[author] += 1;
2830                        }
2831                        Err(e) => {
2832                            // Flag intra-commit double-spend: if the conflict is with
2833                            // a transaction in this commit (in owned_object_locks), the
2834                            // holder should be deferred as penalty.
2835                            let gas_object_ids: HashSet<ObjectID> = tx
2836                                .transaction_data()
2837                                .gas()
2838                                .iter()
2839                                .map(|obj_ref| obj_ref.0)
2840                                .collect();
2841                            let mut is_intra_commit_conflict = false;
2842                            for obj_ref in &owned_object_refs {
2843                                if let Some(holder_digest) = owned_object_locks.get(obj_ref) {
2844                                    is_intra_commit_conflict = true;
2845                                    let info = contested_transaction_digests
2846                                        .entry(*holder_digest)
2847                                        .or_default();
2848                                    info.winner_author = lock_holder_authors
2849                                        .get(holder_digest)
2850                                        .copied()
2851                                        .unwrap_or(author);
2852                                    if gas_object_ids.contains(&obj_ref.0) {
2853                                        info.gas_object_conflicts += 1;
2854                                    } else {
2855                                        info.non_gas_object_conflicts += 1;
2856                                    }
2857                                }
2858                            }
2859                            // Attribute the losing side of the conflict to the authority that
2860                            // sequenced this dropped transaction. Counted once per loser, even
2861                            // if it conflicted on multiple objects.
2862                            if is_intra_commit_conflict {
2863                                self.metrics
2864                                    .consensus_handler_double_spend_conflicting_authority
2865                                    .with_label_values(&[self.authority_hostname(author), "loser"])
2866                                    .inc();
2867                            }
2868                            debug!("Dropping transaction {}: {}", tx.digest(), e);
2869                            self.metrics
2870                                .consensus_handler_dropped_transactions
2871                                .with_label_values(&["lock_conflict"])
2872                                .inc();
2873                            status_updates.push((position, ConsensusTxStatus::Dropped));
2874                            num_dropped_user_transactions[author] += 1;
2875                            self.epoch_store.set_rejection_vote_reason(position, &e);
2876                            dropped_transaction_keys.push(parsed.transaction.key());
2877                            continue;
2878                        }
2879                    }
2880                }
2881
2882                let transaction = SequencedConsensusTransactionKind::External(parsed.transaction);
2883                transactions.push((transaction, author as u32));
2884            }
2885        }
2886
2887        // Flush all collected status updates in one batched write. None of the logic
2888        // above reads transaction status back, so deferring visibility to here (still
2889        // before dedup/processing) is equivalent to setting each status inline.
2890        self.epoch_store.set_consensus_tx_statuses(status_updates);
2891
2892        for (i, authority) in self.committee.authorities() {
2893            let hostname = &authority.hostname;
2894            self.metrics
2895                .consensus_committed_messages
2896                .with_label_values(&[hostname])
2897                .set(self.last_consensus_stats.stats.get_num_messages(i.value()) as i64);
2898            self.metrics
2899                .consensus_committed_user_transactions
2900                .with_label_values(&[hostname])
2901                .set(
2902                    self.last_consensus_stats
2903                        .stats
2904                        .get_num_user_transactions(i.value()) as i64,
2905                );
2906            self.metrics
2907                .consensus_finalized_user_transactions
2908                .with_label_values(&[hostname])
2909                .add(num_finalized_user_transactions[i.value()] as i64);
2910            self.metrics
2911                .consensus_rejected_user_transactions
2912                .with_label_values(&[hostname])
2913                .add(num_rejected_user_transactions[i.value()] as i64);
2914            self.metrics
2915                .consensus_dropped_user_transactions
2916                .with_label_values(&[hostname])
2917                .add(num_dropped_user_transactions[i.value()] as i64);
2918        }
2919
2920        FilteredConsensusOutput {
2921            transactions,
2922            owned_object_locks,
2923            dropped_transaction_keys,
2924            contested_transaction_digests,
2925        }
2926    }
2927
2928    fn deduplicate_consensus_txns(
2929        &mut self,
2930        state: &mut CommitHandlerState,
2931        commit_info: &ConsensusCommitInfo,
2932        transactions: Vec<(SequencedConsensusTransactionKind, u32)>,
2933    ) -> Vec<VerifiedSequencedConsensusTransaction> {
2934        let _scope = monitored_scope("ConsensusCommitHandler::deduplicate_consensus_txns");
2935        let mut all_transactions = Vec::new();
2936
2937        // Track occurrence counts for each transaction key within this commit.
2938        // Also serves as the deduplication set (count > 1 means duplicate within commit).
2939        let mut occurrence_counts: HashMap<SequencedConsensusTransactionKey, u32> = HashMap::new();
2940        // Keys being seen for the first time (not duplicates from previous commits).
2941        let mut first_commit_keys: HashSet<SequencedConsensusTransactionKey> = HashSet::new();
2942
2943        for (seq, (transaction, cert_origin)) in transactions.into_iter().enumerate() {
2944            // In process_consensus_transactions_and_commit_boundary(), we will add a system consensus commit
2945            // prologue transaction, which will be the first transaction in this consensus commit batch.
2946            // Therefore, the transaction sequence number starts from 1 here.
2947            let current_tx_index = ExecutionIndices {
2948                last_committed_round: commit_info.round,
2949                sub_dag_index: commit_info.consensus_commit_ref.index.into(),
2950                transaction_index: (seq + 1) as u64,
2951            };
2952
2953            self.last_consensus_stats.index = current_tx_index;
2954
2955            let certificate_author = *self
2956                .epoch_store
2957                .committee()
2958                .authority_by_index(cert_origin)
2959                .unwrap();
2960
2961            let sequenced_transaction = SequencedConsensusTransaction {
2962                certificate_author_index: cert_origin,
2963                certificate_author,
2964                consensus_index: current_tx_index,
2965                transaction,
2966            };
2967
2968            let Some(verified_transaction) = self
2969                .epoch_store
2970                .verify_consensus_transaction(sequenced_transaction)
2971            else {
2972                continue;
2973            };
2974
2975            let key = verified_transaction.0.key();
2976
2977            if let Some(tx_digest) = key.user_transaction_digest() {
2978                self.epoch_store
2979                    .cache_recently_finalized_transaction(tx_digest);
2980            }
2981
2982            // Increment count and check if this is a duplicate within this commit.
2983            // This replaces the separate processed_set HashSet.
2984            let count = occurrence_counts.entry(key.clone()).or_insert(0);
2985            *count += 1;
2986            let in_commit = *count > 1;
2987
2988            let in_cache = self.processed_cache.put(key.clone(), ()).is_some();
2989            if in_commit || in_cache {
2990                self.metrics.skipped_consensus_txns_cache_hit.inc();
2991                continue;
2992            }
2993            if self
2994                .epoch_store
2995                .is_consensus_message_processed(&key)
2996                .expect("db error")
2997            {
2998                self.metrics.skipped_consensus_txns.inc();
2999                continue;
3000            }
3001
3002            first_commit_keys.insert(key.clone());
3003
3004            state.output.record_consensus_message_processed(key);
3005
3006            all_transactions.push(verified_transaction);
3007        }
3008
3009        for key in first_commit_keys {
3010            if let Some(&count) = occurrence_counts.get(&key)
3011                && count > 1
3012            {
3013                self.metrics
3014                    .consensus_handler_duplicate_tx_count
3015                    .observe(count as f64);
3016            }
3017        }
3018
3019        // Copy user transaction occurrence counts to state for unpaid amplification detection.
3020        assert!(
3021            state.occurrence_counts.is_empty(),
3022            "occurrence_counts should be empty before populating"
3023        );
3024        state.occurrence_counts.reserve(occurrence_counts.len());
3025        state.occurrence_counts.extend(
3026            occurrence_counts
3027                .into_iter()
3028                .filter_map(|(key, count)| key.user_transaction_digest().map(|d| (d, count))),
3029        );
3030
3031        all_transactions
3032    }
3033
3034    fn build_commit_handler_input(
3035        &self,
3036        transactions: Vec<VerifiedSequencedConsensusTransaction>,
3037    ) -> CommitHandlerInput {
3038        let _scope = monitored_scope("ConsensusCommitHandler::build_commit_handler_input");
3039        let epoch = self.epoch_store.epoch();
3040        let mut commit_handler_input = CommitHandlerInput::default();
3041
3042        for VerifiedSequencedConsensusTransaction(transaction) in transactions.into_iter() {
3043            match transaction.transaction {
3044                SequencedConsensusTransactionKind::External(consensus_transaction) => {
3045                    match consensus_transaction.kind {
3046                        // === User transactions ===
3047                        ConsensusTransactionKind::UserTransactionV2(tx) => {
3048                            // Extract the aliases claim (required) from the claims
3049                            let used_alias_versions = tx.aliases();
3050                            let inner_tx = tx.into_tx();
3051                            // Safe because transactions are certified by consensus.
3052                            let tx = VerifiedTransaction::new_unchecked(inner_tx);
3053                            // TODO(fastpath): accept position in consensus, after plumbing consensus round, authority index, and transaction index here.
3054                            let transaction =
3055                                VerifiedExecutableTransaction::new_from_consensus(tx, epoch);
3056                            if let Some(used_alias_versions) = used_alias_versions {
3057                                commit_handler_input
3058                                    .user_transactions
3059                                    .push(WithAliases::new(transaction, used_alias_versions));
3060                            } else {
3061                                commit_handler_input.user_transactions.push(
3062                                    VerifiedExecutableTransactionWithAliases::no_aliases(
3063                                        transaction,
3064                                    ),
3065                                );
3066                            }
3067                        }
3068
3069                        // === State machines ===
3070                        ConsensusTransactionKind::EndOfPublish(authority_public_key_bytes) => {
3071                            commit_handler_input
3072                                .end_of_publish_transactions
3073                                .push(authority_public_key_bytes);
3074                        }
3075                        ConsensusTransactionKind::NewJWKFetched(
3076                            authority_public_key_bytes,
3077                            jwk_id,
3078                            jwk,
3079                        ) => {
3080                            commit_handler_input.new_jwks.push((
3081                                authority_public_key_bytes,
3082                                jwk_id,
3083                                jwk,
3084                            ));
3085                        }
3086                        ConsensusTransactionKind::RandomnessDkgMessage(
3087                            authority_public_key_bytes,
3088                            items,
3089                        ) => {
3090                            commit_handler_input
3091                                .randomness_dkg_messages
3092                                .push((authority_public_key_bytes, items));
3093                        }
3094                        ConsensusTransactionKind::RandomnessDkgConfirmation(
3095                            authority_public_key_bytes,
3096                            items,
3097                        ) => {
3098                            commit_handler_input
3099                                .randomness_dkg_confirmations
3100                                .push((authority_public_key_bytes, items));
3101                        }
3102                        ConsensusTransactionKind::CapabilityNotificationV2(
3103                            authority_capabilities_v2,
3104                        ) => {
3105                            commit_handler_input
3106                                .capability_notifications
3107                                .push(authority_capabilities_v2);
3108                        }
3109                        ConsensusTransactionKind::ExecutionTimeObservation(
3110                            execution_time_observation,
3111                        ) => {
3112                            commit_handler_input
3113                                .execution_time_observations
3114                                .push(execution_time_observation);
3115                        }
3116                        ConsensusTransactionKind::CheckpointSignatureV2(
3117                            checkpoint_signature_message,
3118                        ) => {
3119                            commit_handler_input
3120                                .checkpoint_signature_messages
3121                                .push(*checkpoint_signature_message);
3122                        }
3123                        ConsensusTransactionKind::UpdateTransactionDenyConfig(msg) => {
3124                            commit_handler_input
3125                                .transaction_deny_config_updates
3126                                .push((transaction.certificate_author, *msg));
3127                        }
3128
3129                        // Deprecated messages, filtered earlier by filter_consensus_txns()
3130                        // or rejected by SuiTxValidator. Kept for exhaustiveness.
3131                        ConsensusTransactionKind::CheckpointSignature(_)
3132                        | ConsensusTransactionKind::RandomnessStateUpdate(_, _)
3133                        | ConsensusTransactionKind::CapabilityNotification(_)
3134                        | ConsensusTransactionKind::CertifiedTransaction(_)
3135                        | ConsensusTransactionKind::UserTransaction(_) => {
3136                            unreachable!("filtered earlier")
3137                        }
3138                    }
3139                }
3140                // TODO: I think we can delete this, it was only used to inject randomness state update into the tx stream.
3141                SequencedConsensusTransactionKind::System(_verified_envelope) => unreachable!(),
3142            }
3143        }
3144
3145        commit_handler_input
3146    }
3147}
3148
3149/// Sends transactions to the execution scheduler in a separate task,
3150/// to avoid blocking consensus handler.
3151pub(crate) type SchedulerMessage = (
3152    Vec<(Schedulable, AssignedVersions)>,
3153    Option<SettlementBatchInfo>,
3154);
3155
3156#[derive(Clone)]
3157pub(crate) struct ExecutionSchedulerSender {
3158    sender: monitored_mpsc::UnboundedSender<SchedulerMessage>,
3159}
3160
3161impl ExecutionSchedulerSender {
3162    fn start(
3163        settlement_scheduler: SettlementScheduler,
3164        epoch_store: Arc<AuthorityPerEpochStore>,
3165    ) -> Self {
3166        let (sender, recv) = monitored_mpsc::unbounded_channel("execution_scheduler_sender");
3167        spawn_monitored_task!(Self::run(recv, settlement_scheduler, epoch_store));
3168        Self { sender }
3169    }
3170
3171    pub(crate) fn new_for_testing(
3172        sender: monitored_mpsc::UnboundedSender<SchedulerMessage>,
3173    ) -> Self {
3174        Self { sender }
3175    }
3176
3177    fn send(
3178        &self,
3179        transactions: Vec<(Schedulable, AssignedVersions)>,
3180        settlement: Option<SettlementBatchInfo>,
3181    ) {
3182        let _ = self.sender.send((transactions, settlement));
3183    }
3184
3185    async fn run(
3186        mut recv: monitored_mpsc::UnboundedReceiver<SchedulerMessage>,
3187        settlement_scheduler: SettlementScheduler,
3188        epoch_store: Arc<AuthorityPerEpochStore>,
3189    ) {
3190        while let Some((transactions, settlement)) = recv.recv().await {
3191            let _guard = monitored_scope("ConsensusHandler::enqueue");
3192            let txns = transactions
3193                .into_iter()
3194                .map(|(txn, versions)| (txn, ExecutionEnv::new().with_assigned_versions(versions)))
3195                .collect();
3196            if let Some(settlement) = settlement {
3197                settlement_scheduler.enqueue_v2(txns, settlement, &epoch_store);
3198            } else {
3199                settlement_scheduler.enqueue(txns, &epoch_store);
3200            }
3201        }
3202    }
3203}
3204
3205/// Capacity of the channel from the deserialize worker to the commit handler. Small/bounded: lets
3206/// the worker prepare ~1 commit ahead (pipelining) while applying backpressure when the handler is
3207/// behind, instead of buffering parsed commits unboundedly.
3208const CONSENSUS_HANDLER_DESERIALIZE_CHANNEL_CAPACITY: usize = 2;
3209
3210/// Transactions BCS-deserialized out of a consensus commit, grouped by block. Produced by the
3211/// deserialize worker and consumed by the commit handler, so parsing stays off the handler's
3212/// critical path.
3213type ParsedConsensusTransactions = Vec<(BlockRef, Vec<ParsedTransaction>)>;
3214
3215/// Manages the lifetime of tasks handling the commits and transactions output by consensus.
3216pub(crate) struct MysticetiConsensusHandler {
3217    tasks: JoinSet<()>,
3218}
3219
3220impl MysticetiConsensusHandler {
3221    pub(crate) fn new(
3222        last_processed_commit_at_startup: CommitIndex,
3223        mut consensus_handler: ConsensusHandler<CheckpointService>,
3224        mut commit_receiver: UnboundedReceiver<consensus_core::CommittedSubDag>,
3225        commit_consumer_monitor: Arc<CommitConsumerMonitor>,
3226    ) -> Self {
3227        debug!(
3228            last_processed_commit_at_startup,
3229            "Starting consensus replay"
3230        );
3231        let mut tasks = JoinSet::new();
3232
3233        // Stage 1 — deserialize worker: BCS-parses each commit's transactions off the handler's
3234        // critical path, so parsing overlaps the handler processing the previous commit. The
3235        // channel is bounded (small) so the worker prepares ~1 commit ahead but applies
3236        // backpressure (rather than buffering parsed commits unboundedly) when the handler is the
3237        // bottleneck. Single-threaded; ordering is preserved (one worker, FIFO channel, one handler).
3238        let (parsed_sender, mut parsed_receiver) = monitored_mpsc::channel(
3239            "consensus_deserialized_commits",
3240            CONSENSUS_HANDLER_DESERIALIZE_CHANNEL_CAPACITY,
3241        );
3242        tasks.spawn(monitored_future!(async move {
3243            while let Some(consensus_commit) = commit_receiver.recv().await {
3244                let transactions: ParsedConsensusTransactions = {
3245                    let _scope = monitored_scope("ConsensusCommitHandler::deserialize_worker");
3246                    consensus_commit.transactions()
3247                };
3248                // The send is intentionally outside the scope above: on the bounded channel it
3249                // blocks when the handler is the bottleneck, and that idle-wait would otherwise
3250                // inflate the deserialize_worker scope (making it read as CPU work, not waiting).
3251                if parsed_sender
3252                    .send((consensus_commit, transactions))
3253                    .await
3254                    .is_err()
3255                {
3256                    break;
3257                }
3258            }
3259        }));
3260
3261        // Stage 2 — commit handler: processes pre-parsed commits in order.
3262        tasks.spawn(monitored_future!(async move {
3263            // TODO: pause when execution is overloaded, so consensus can detect the backpressure.
3264            while let Some((consensus_commit, transactions)) = parsed_receiver.recv().await {
3265                let commit_index = consensus_commit.commit_ref.index;
3266                if commit_index <= last_processed_commit_at_startup {
3267                    consensus_handler.handle_prior_consensus_commit(consensus_commit);
3268                } else {
3269                    consensus_handler
3270                        .handle_consensus_commit(consensus_commit, transactions)
3271                        .await;
3272                }
3273                commit_consumer_monitor.set_highest_handled_commit(commit_index);
3274            }
3275        }));
3276        Self { tasks }
3277    }
3278
3279    pub(crate) async fn abort(&mut self) {
3280        self.tasks.shutdown().await;
3281    }
3282}
3283
3284fn authenticator_state_update_transaction(
3285    epoch_store: &AuthorityPerEpochStore,
3286    round: u64,
3287    mut new_active_jwks: Vec<ActiveJwk>,
3288) -> VerifiedExecutableTransaction {
3289    let epoch = epoch_store.epoch();
3290    new_active_jwks.sort();
3291
3292    info!("creating authenticator state update transaction");
3293    assert!(epoch_store.authenticator_state_enabled());
3294    let transaction = VerifiedTransaction::new_authenticator_state_update(
3295        epoch,
3296        round,
3297        new_active_jwks,
3298        epoch_store
3299            .epoch_start_config()
3300            .authenticator_obj_initial_shared_version()
3301            .expect("authenticator state obj must exist"),
3302    );
3303    VerifiedExecutableTransaction::new_system(transaction, epoch)
3304}
3305
3306/// The owned (non-immutable `ImmOrOwnedMoveObject`) object refs that a
3307/// `UserTransactionV2` must lock post-consensus. Immutable objects are excluded as
3308/// they can be used concurrently. Returns `None` if the transaction's input objects
3309/// are invalid. Used both to prefetch existing locks for the whole commit and, per
3310/// transaction, to perform conflict detection — keeping the two in sync.
3311fn owned_object_refs_to_lock(
3312    tx_with_claims: &PlainTransactionWithClaims,
3313) -> Option<Vec<ObjectRef>> {
3314    let immutable_object_ids: HashSet<ObjectID> =
3315        tx_with_claims.get_immutable_objects().into_iter().collect();
3316    let input_objects = tx_with_claims
3317        .tx()
3318        .transaction_data()
3319        .input_objects()
3320        .ok()?;
3321    Some(
3322        input_objects
3323            .iter()
3324            .filter_map(|obj| match obj {
3325                InputObjectKind::ImmOrOwnedMoveObject(obj_ref)
3326                    if !immutable_object_ids.contains(&obj_ref.0) =>
3327                {
3328                    Some(*obj_ref)
3329                }
3330                _ => None,
3331            })
3332            .collect(),
3333    )
3334}
3335
3336/// Label for the `sequencing_certificate_*` metrics.
3337pub(crate) fn tx_type_label(transactions: &[ConsensusTransaction]) -> &'static str {
3338    match transactions {
3339        [transaction] => classify(transaction),
3340        _ => "soft_bundle",
3341    }
3342}
3343
3344pub(crate) fn classify(transaction: &ConsensusTransaction) -> &'static str {
3345    match &transaction.kind {
3346        // Deprecated and rejected by SuiTxValidator; never classified in practice.
3347        ConsensusTransactionKind::CertifiedTransaction(_) => "_deprecated_certificate",
3348        ConsensusTransactionKind::CheckpointSignature(_) => "checkpoint_signature",
3349        ConsensusTransactionKind::CheckpointSignatureV2(_) => "checkpoint_signature",
3350        ConsensusTransactionKind::EndOfPublish(_) => "end_of_publish",
3351        ConsensusTransactionKind::CapabilityNotification(_) => "capability_notification",
3352        ConsensusTransactionKind::CapabilityNotificationV2(_) => "capability_notification_v2",
3353        ConsensusTransactionKind::NewJWKFetched(_, _, _) => "new_jwk_fetched",
3354        ConsensusTransactionKind::RandomnessStateUpdate(_, _) => "randomness_state_update",
3355        ConsensusTransactionKind::RandomnessDkgMessage(_, _) => "randomness_dkg_message",
3356        ConsensusTransactionKind::RandomnessDkgConfirmation(_, _) => "randomness_dkg_confirmation",
3357        ConsensusTransactionKind::UserTransaction(_) => "_deprecated_user_transaction",
3358        ConsensusTransactionKind::UserTransactionV2(tx) => {
3359            if tx.tx().is_consensus_tx() {
3360                "shared_user_transaction_v2"
3361            } else {
3362                "owned_user_transaction_v2"
3363            }
3364        }
3365        ConsensusTransactionKind::ExecutionTimeObservation(_) => "execution_time_observation",
3366        ConsensusTransactionKind::UpdateTransactionDenyConfig(_) => {
3367            "update_transaction_deny_config"
3368        }
3369    }
3370}
3371
3372#[derive(Debug, Clone, Serialize, Deserialize)]
3373pub struct SequencedConsensusTransaction {
3374    pub certificate_author_index: AuthorityIndex,
3375    pub certificate_author: AuthorityName,
3376    pub consensus_index: ExecutionIndices,
3377    pub transaction: SequencedConsensusTransactionKind,
3378}
3379
3380#[derive(Debug, Clone)]
3381#[allow(clippy::large_enum_variant)]
3382pub enum SequencedConsensusTransactionKind {
3383    External(ConsensusTransaction),
3384    System(VerifiedExecutableTransaction),
3385}
3386
3387impl Serialize for SequencedConsensusTransactionKind {
3388    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3389        let serializable = SerializableSequencedConsensusTransactionKind::from(self);
3390        serializable.serialize(serializer)
3391    }
3392}
3393
3394impl<'de> Deserialize<'de> for SequencedConsensusTransactionKind {
3395    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3396        let serializable =
3397            SerializableSequencedConsensusTransactionKind::deserialize(deserializer)?;
3398        Ok(serializable.into())
3399    }
3400}
3401
3402// We can't serialize SequencedConsensusTransactionKind directly because it contains a
3403// VerifiedExecutableTransaction, which is not serializable (by design). This wrapper allows us to
3404// convert to a serializable format easily.
3405#[derive(Debug, Clone, Serialize, Deserialize)]
3406#[allow(clippy::large_enum_variant)]
3407enum SerializableSequencedConsensusTransactionKind {
3408    External(ConsensusTransaction),
3409    System(TrustedExecutableTransaction),
3410}
3411
3412impl From<&SequencedConsensusTransactionKind> for SerializableSequencedConsensusTransactionKind {
3413    fn from(kind: &SequencedConsensusTransactionKind) -> Self {
3414        match kind {
3415            SequencedConsensusTransactionKind::External(ext) => {
3416                SerializableSequencedConsensusTransactionKind::External(ext.clone())
3417            }
3418            SequencedConsensusTransactionKind::System(txn) => {
3419                SerializableSequencedConsensusTransactionKind::System(txn.clone().serializable())
3420            }
3421        }
3422    }
3423}
3424
3425impl From<SerializableSequencedConsensusTransactionKind> for SequencedConsensusTransactionKind {
3426    fn from(kind: SerializableSequencedConsensusTransactionKind) -> Self {
3427        match kind {
3428            SerializableSequencedConsensusTransactionKind::External(ext) => {
3429                SequencedConsensusTransactionKind::External(ext)
3430            }
3431            SerializableSequencedConsensusTransactionKind::System(txn) => {
3432                SequencedConsensusTransactionKind::System(txn.into())
3433            }
3434        }
3435    }
3436}
3437
3438#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq, Debug, Ord, PartialOrd)]
3439pub enum SequencedConsensusTransactionKey {
3440    External(ConsensusTransactionKey),
3441    System(TransactionDigest),
3442}
3443
3444impl SequencedConsensusTransactionKey {
3445    pub fn user_transaction_digest(&self) -> Option<TransactionDigest> {
3446        match self {
3447            SequencedConsensusTransactionKey::External(key) => match key {
3448                ConsensusTransactionKey::Certificate(digest) => Some(*digest),
3449                _ => None,
3450            },
3451            SequencedConsensusTransactionKey::System(_) => None,
3452        }
3453    }
3454}
3455
3456impl SequencedConsensusTransactionKind {
3457    pub fn key(&self) -> SequencedConsensusTransactionKey {
3458        match self {
3459            SequencedConsensusTransactionKind::External(ext) => {
3460                SequencedConsensusTransactionKey::External(ext.key())
3461            }
3462            SequencedConsensusTransactionKind::System(txn) => {
3463                SequencedConsensusTransactionKey::System(*txn.digest())
3464            }
3465        }
3466    }
3467
3468    pub fn get_tracking_id(&self) -> u64 {
3469        match self {
3470            SequencedConsensusTransactionKind::External(ext) => ext.get_tracking_id(),
3471            SequencedConsensusTransactionKind::System(_txn) => 0,
3472        }
3473    }
3474
3475    pub fn is_executable_transaction(&self) -> bool {
3476        match self {
3477            SequencedConsensusTransactionKind::External(ext) => ext.is_user_transaction(),
3478            SequencedConsensusTransactionKind::System(_) => true,
3479        }
3480    }
3481
3482    pub fn executable_transaction_digest(&self) -> Option<TransactionDigest> {
3483        match self {
3484            SequencedConsensusTransactionKind::External(ext) => match &ext.kind {
3485                ConsensusTransactionKind::UserTransactionV2(txn) => Some(*txn.tx().digest()),
3486                _ => None,
3487            },
3488            SequencedConsensusTransactionKind::System(txn) => Some(*txn.digest()),
3489        }
3490    }
3491
3492    pub fn is_end_of_publish(&self) -> bool {
3493        match self {
3494            SequencedConsensusTransactionKind::External(ext) => {
3495                matches!(ext.kind, ConsensusTransactionKind::EndOfPublish(..))
3496            }
3497            SequencedConsensusTransactionKind::System(_) => false,
3498        }
3499    }
3500}
3501
3502impl SequencedConsensusTransaction {
3503    pub fn sender_authority(&self) -> AuthorityName {
3504        self.certificate_author
3505    }
3506
3507    pub fn key(&self) -> SequencedConsensusTransactionKey {
3508        self.transaction.key()
3509    }
3510
3511    pub fn is_end_of_publish(&self) -> bool {
3512        if let SequencedConsensusTransactionKind::External(ref transaction) = self.transaction {
3513            matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..))
3514        } else {
3515            false
3516        }
3517    }
3518
3519    pub fn try_take_execution_time_observation(&mut self) -> Option<ExecutionTimeObservation> {
3520        if let SequencedConsensusTransactionKind::External(ConsensusTransaction {
3521            kind: ConsensusTransactionKind::ExecutionTimeObservation(observation),
3522            ..
3523        }) = &mut self.transaction
3524        {
3525            Some(std::mem::take(observation))
3526        } else {
3527            None
3528        }
3529    }
3530
3531    pub fn is_system(&self) -> bool {
3532        matches!(
3533            self.transaction,
3534            SequencedConsensusTransactionKind::System(_)
3535        )
3536    }
3537
3538    pub fn is_user_tx_with_randomness(&self, randomness_state_enabled: bool) -> bool {
3539        if !randomness_state_enabled {
3540            // If randomness is disabled, these should be processed same as a tx without randomness,
3541            // which will eventually fail when the randomness state object is not found.
3542            return false;
3543        }
3544        match &self.transaction {
3545            SequencedConsensusTransactionKind::External(ConsensusTransaction {
3546                kind: ConsensusTransactionKind::UserTransactionV2(txn),
3547                ..
3548            }) => txn.tx().transaction_data().uses_randomness(),
3549            _ => false,
3550        }
3551    }
3552
3553    pub fn as_consensus_txn(&self) -> Option<&SenderSignedData> {
3554        match &self.transaction {
3555            SequencedConsensusTransactionKind::External(ConsensusTransaction {
3556                kind: ConsensusTransactionKind::UserTransactionV2(txn),
3557                ..
3558            }) if txn.tx().is_consensus_tx() => Some(txn.tx().data()),
3559            SequencedConsensusTransactionKind::System(txn) if txn.is_consensus_tx() => {
3560                Some(txn.data())
3561            }
3562            _ => None,
3563        }
3564    }
3565}
3566
3567#[derive(Debug, Clone, Serialize, Deserialize)]
3568pub struct VerifiedSequencedConsensusTransaction(pub SequencedConsensusTransaction);
3569
3570#[cfg(test)]
3571impl VerifiedSequencedConsensusTransaction {
3572    pub fn new_test(transaction: ConsensusTransaction) -> Self {
3573        Self(SequencedConsensusTransaction::new_test(transaction))
3574    }
3575}
3576
3577impl SequencedConsensusTransaction {
3578    pub fn new_test(transaction: ConsensusTransaction) -> Self {
3579        Self {
3580            certificate_author_index: 0,
3581            certificate_author: AuthorityName::ZERO,
3582            consensus_index: Default::default(),
3583            transaction: SequencedConsensusTransactionKind::External(transaction),
3584        }
3585    }
3586}
3587
3588#[derive(Serialize, Deserialize)]
3589pub(crate) struct CommitIntervalObserver {
3590    ring_buffer: VecDeque<u64>,
3591}
3592
3593impl CommitIntervalObserver {
3594    pub fn new(window_size: u32) -> Self {
3595        Self {
3596            ring_buffer: VecDeque::with_capacity(window_size as usize),
3597        }
3598    }
3599
3600    pub fn observe_commit_time(&mut self, consensus_commit: &impl ConsensusCommitAPI) {
3601        let commit_time = consensus_commit.commit_timestamp_ms();
3602        if self.ring_buffer.len() == self.ring_buffer.capacity() {
3603            self.ring_buffer.pop_front();
3604        }
3605        self.ring_buffer.push_back(commit_time);
3606    }
3607
3608    pub fn commit_interval_estimate(&self) -> Option<Duration> {
3609        if self.ring_buffer.len() <= 1 {
3610            None
3611        } else {
3612            let first = self.ring_buffer.front().unwrap();
3613            let last = self.ring_buffer.back().unwrap();
3614            let duration = last.saturating_sub(*first);
3615            let num_commits = self.ring_buffer.len() as u64;
3616            Some(Duration::from_millis(duration.div_ceil(num_commits)))
3617        }
3618    }
3619}
3620
3621#[cfg(test)]
3622mod tests {
3623    use consensus_core::{
3624        BlockAPI, CommitDigest, CommitRef, CommittedSubDag, TestBlock, Transaction, VerifiedBlock,
3625    };
3626    use futures::pin_mut;
3627    use prometheus::Registry;
3628    use sui_protocol_config::{ConsensusTransactionOrdering, ProtocolConfig};
3629    use sui_types::{
3630        base_types::ExecutionDigests,
3631        base_types::{AuthorityName, FullObjectRef, ObjectID, SuiAddress, random_object_ref},
3632        committee::Committee,
3633        crypto::deterministic_random_account_key,
3634        gas::GasCostSummary,
3635        message_envelope::Message,
3636        messages_checkpoint::{
3637            CheckpointContents, CheckpointSignatureMessage, CheckpointSummary,
3638            SignedCheckpointSummary,
3639        },
3640        messages_consensus::ConsensusTransaction,
3641        object::Object,
3642        transaction::{
3643            CertifiedTransaction, TransactionData, TransactionDataAPI, VerifiedCertificate,
3644        },
3645    };
3646
3647    use super::*;
3648    use crate::{
3649        authority::{
3650            authority_per_epoch_store::ConsensusStatsAPI,
3651            consensus_tx_status_cache::NotifyReadConsensusTxStatusResult,
3652            test_authority_builder::TestAuthorityBuilder,
3653        },
3654        checkpoints::CheckpointServiceNoop,
3655        consensus_adapter::consensus_tests::test_user_transaction,
3656        consensus_test_utils::{TestConsensusCommit, setup_consensus_handler_for_testing},
3657        post_consensus_tx_reorder::PostConsensusTxReorder,
3658    };
3659
3660    fn epoch_close_deadline_config(deadline_ms: Option<u64>) -> ProtocolConfig {
3661        let mut protocol_config = ProtocolConfig::get_for_max_version_UNSAFE();
3662        if let Some(deadline_ms) = deadline_ms {
3663            protocol_config.set_epoch_close_deadline_ms_for_testing(deadline_ms);
3664        } else {
3665            protocol_config.disable_epoch_close_deadline_ms_for_testing();
3666        }
3667        protocol_config
3668    }
3669
3670    #[tokio::test]
3671    async fn test_consensus_handler_max_transaction_size() {
3672        let metrics = AuthorityMetrics::new(&Registry::new());
3673
3674        metrics.observe_consensus_handler_transaction_size("class_a", "accepted", 100);
3675        metrics.observe_consensus_handler_transaction_size("class_a", "rejected", 80);
3676        metrics.observe_consensus_handler_transaction_size("class_b", "accepted", 50);
3677
3678        assert_eq!(
3679            metrics
3680                .consensus_handler_max_transaction_size
3681                .with_label_values(&["class_a"])
3682                .get(),
3683            100
3684        );
3685        assert_eq!(
3686            metrics
3687                .consensus_handler_max_transaction_size
3688                .with_label_values(&["class_b"])
3689                .get(),
3690            50
3691        );
3692
3693        metrics.observe_consensus_handler_transaction_size("class_a", "rejected", 120);
3694
3695        assert_eq!(
3696            metrics
3697                .consensus_handler_max_transaction_size
3698                .with_label_values(&["class_a"])
3699                .get(),
3700            120
3701        );
3702    }
3703
3704    #[tokio::test(flavor = "current_thread")]
3705    async fn test_epoch_close_deadline_preserves_pre_deadline_blocking() {
3706        let state = TestAuthorityBuilder::new()
3707            .with_protocol_config(epoch_close_deadline_config(Some(100)))
3708            .build()
3709            .await;
3710        let epoch_store = state.epoch_store_for_testing();
3711        epoch_store.insert_deferred_transactions_for_test(
3712            DeferralKey::new_for_consensus_round(u64::MAX, 1),
3713            vec![user_txn(1)],
3714        );
3715        let scheduled_end = epoch_store.next_reconfiguration_timestamp_ms();
3716        let mut setup = setup_consensus_handler_for_testing(&state).await;
3717
3718        setup
3719            .consensus_handler
3720            .handle_consensus_commit_for_test(TestConsensusCommit::empty(1, scheduled_end + 99, 1))
3721            .await;
3722
3723        let reconfig_state = epoch_store.get_reconfig_state_read_lock_guard();
3724        assert!(reconfig_state.is_reject_all_certs());
3725        assert!(!reconfig_state.is_reject_all_tx());
3726        assert_eq!(
3727            epoch_store.get_all_deferred_transactions_for_test().len(),
3728            1
3729        );
3730        assert!(
3731            epoch_store
3732                .get_pending_checkpoints(None)
3733                .unwrap()
3734                .iter()
3735                .all(|(_, checkpoint)| !checkpoint.details.last_of_epoch)
3736        );
3737        assert_eq!(
3738            setup
3739                .consensus_handler
3740                .metrics
3741                .consensus_handler_dropped_transactions
3742                .with_label_values(&["epoch_close_deadline"])
3743                .get(),
3744            0
3745        );
3746    }
3747
3748    #[tokio::test(flavor = "current_thread")]
3749    async fn test_epoch_close_deadline_none_preserves_indefinite_blocking() {
3750        let state = TestAuthorityBuilder::new()
3751            .with_protocol_config(epoch_close_deadline_config(None))
3752            .build()
3753            .await;
3754        let epoch_store = state.epoch_store_for_testing();
3755        epoch_store.insert_deferred_transactions_for_test(
3756            DeferralKey::new_for_consensus_round(u64::MAX, 1),
3757            vec![user_txn(1)],
3758        );
3759        let scheduled_end = epoch_store.next_reconfiguration_timestamp_ms();
3760        let mut setup = setup_consensus_handler_for_testing(&state).await;
3761
3762        setup
3763            .consensus_handler
3764            .handle_consensus_commit_for_test(TestConsensusCommit::empty(
3765                1,
3766                scheduled_end.saturating_add(1_000_000),
3767                1,
3768            ))
3769            .await;
3770
3771        let reconfig_state = epoch_store.get_reconfig_state_read_lock_guard();
3772        assert!(reconfig_state.is_reject_all_certs());
3773        assert!(!reconfig_state.is_reject_all_tx());
3774    }
3775
3776    #[tokio::test(flavor = "current_thread")]
3777    async fn test_epoch_close_deadline_is_inert_without_deferred_transactions() {
3778        let state = TestAuthorityBuilder::new()
3779            .with_protocol_config(epoch_close_deadline_config(Some(100)))
3780            .build()
3781            .await;
3782        let epoch_store = state.epoch_store_for_testing();
3783        let scheduled_end = epoch_store.next_reconfiguration_timestamp_ms();
3784        let mut setup = setup_consensus_handler_for_testing(&state).await;
3785
3786        // Commit timestamp is past the deadline, so deadline_reached is true — with no
3787        // deferred transactions this must close cleanly with nothing abandoned (no
3788        // debug_fatal panic, metric stays zero).
3789        setup
3790            .consensus_handler
3791            .handle_consensus_commit_for_test(TestConsensusCommit::empty(1, scheduled_end + 100, 1))
3792            .await;
3793
3794        assert!(
3795            epoch_store
3796                .get_reconfig_state_read_lock_guard()
3797                .is_reject_all_tx()
3798        );
3799        let checkpoints = epoch_store.get_pending_checkpoints(None).unwrap();
3800        assert!(checkpoints.last().unwrap().1.details.last_of_epoch);
3801        assert_eq!(
3802            setup
3803                .consensus_handler
3804                .metrics
3805                .consensus_handler_dropped_transactions
3806                .with_label_values(&["epoch_close_deadline"])
3807                .get(),
3808            0
3809        );
3810    }
3811
3812    #[tokio::test(flavor = "current_thread")]
3813    async fn test_epoch_close_deadline_counts_abandoned_transactions_and_closes_first() {
3814        let state = TestAuthorityBuilder::new()
3815            .with_protocol_config(epoch_close_deadline_config(Some(100)))
3816            .build()
3817            .await;
3818        let epoch_store = state.epoch_store_for_testing();
3819        let key = DeferralKey::new_for_consensus_round(u64::MAX, 1);
3820        epoch_store.insert_deferred_transactions_for_test(key, vec![user_txn(1), user_txn(2)]);
3821        let setup = setup_consensus_handler_for_testing(&state).await;
3822        let mut handler_state = CommitHandlerState::new(&epoch_store, 1);
3823
3824        let (reconfig_state, final_round, abandoned) = setup
3825            .consensus_handler
3826            .advance_end_of_epoch_state_machine(&mut handler_state, true);
3827
3828        assert!(reconfig_state.is_reject_all_tx());
3829        assert!(final_round);
3830        let abandoned = abandoned.expect("deferred transactions must be reported as abandoned");
3831        assert_eq!(abandoned.count, 2);
3832        assert_eq!(abandoned.sample.len(), 2);
3833        // The abandoned key must be staged for deletion so the final commit clears both the
3834        // db table and (via record_deferral_deletion) the in-memory cache.
3835        assert!(
3836            handler_state
3837                .output
3838                .get_deleted_deferred_txn_keys()
3839                .any(|deleted| deleted == key)
3840        );
3841    }
3842
3843    #[tokio::test(flavor = "current_thread")]
3844    async fn test_epoch_close_deadline_does_not_report_transactions_drained_in_commit() {
3845        let state = TestAuthorityBuilder::new()
3846            .with_protocol_config(epoch_close_deadline_config(Some(100)))
3847            .build()
3848            .await;
3849        let epoch_store = state.epoch_store_for_testing();
3850        let key = DeferralKey::new_for_consensus_round(1, 0);
3851        epoch_store.insert_deferred_transactions_for_test(key, vec![user_txn(1), user_txn(2)]);
3852        let setup = setup_consensus_handler_for_testing(&state).await;
3853        let mut handler_state = CommitHandlerState::new(&epoch_store, 1);
3854        handler_state
3855            .output
3856            .delete_loaded_deferred_transactions(&[key]);
3857
3858        let (reconfig_state, final_round, abandoned) = setup
3859            .consensus_handler
3860            .advance_end_of_epoch_state_machine(&mut handler_state, true);
3861
3862        assert!(reconfig_state.is_reject_all_tx());
3863        assert!(final_round);
3864        assert!(abandoned.is_none());
3865    }
3866
3867    // debug_fatal only panics when crash_on_debug() is true (debug_assertions, msim, or
3868    // SUI_ENABLE_DEBUG_ASSERTIONS); in a plain release build it logs and continues, so the
3869    // should_panic expectation would fail there.
3870    #[cfg(debug_assertions)]
3871    #[tokio::test(flavor = "current_thread")]
3872    #[should_panic(
3873        expected = "Epoch close deadline reached with unscheduled deferred transactions"
3874    )]
3875    async fn test_epoch_close_deadline_timestamp_jump_abandons_fresh_deferral() {
3876        use sui_protocol_config::{ExecutionTimeEstimateParams, PerObjectCongestionControlMode};
3877
3878        let execution_time_params = ExecutionTimeEstimateParams {
3879            target_utilization: 1,
3880            allowed_txn_cost_overage_burst_limit_us: 0,
3881            max_estimate_us: u64::MAX,
3882            randomness_scalar: 100,
3883            stored_observations_num_included_checkpoints: 10,
3884            stored_observations_limit: u64::MAX,
3885            stake_weighted_median_threshold: 0,
3886            default_none_duration_for_new_keys: true,
3887            observations_chunk_size: None,
3888        };
3889        let mut protocol_config = epoch_close_deadline_config(Some(100));
3890        protocol_config.set_per_object_congestion_control_mode_for_testing(
3891            PerObjectCongestionControlMode::ExecutionTimeEstimate(execution_time_params),
3892        );
3893        protocol_config.set_max_deferral_rounds_for_congestion_control_for_testing(1_000);
3894
3895        let (sender, keypair) = deterministic_random_account_key();
3896        let gas_objects: Vec<_> = (0..4)
3897            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
3898            .collect();
3899        let shared_object = Object::shared_for_testing();
3900        let mut starting_objects = gas_objects.clone();
3901        starting_objects.push(shared_object.clone());
3902        let state = TestAuthorityBuilder::new()
3903            .with_starting_objects(&starting_objects)
3904            .with_protocol_config(protocol_config)
3905            .build()
3906            .await;
3907        let mut consensus_transactions = Vec::new();
3908        for gas_object in gas_objects {
3909            let transaction = test_user_transaction(
3910                &state,
3911                sender,
3912                &keypair,
3913                gas_object,
3914                vec![shared_object.clone()],
3915            )
3916            .await;
3917            consensus_transactions.push(ConsensusTransaction::new_user_transaction_v2_message(
3918                &state.name,
3919                transaction.into(),
3920            ));
3921        }
3922        let epoch_store = state.epoch_store_for_testing();
3923        let scheduled_end = epoch_store.next_reconfiguration_timestamp_ms();
3924        let mut setup = setup_consensus_handler_for_testing(&state).await;
3925
3926        setup
3927            .consensus_handler
3928            .handle_consensus_commit_for_test(TestConsensusCommit::new(
3929                consensus_transactions,
3930                1,
3931                scheduled_end + 100,
3932                1,
3933            ))
3934            .await;
3935    }
3936
3937    #[tokio::test(flavor = "current_thread", start_paused = true)]
3938    async fn test_consensus_commit_handler() {
3939        telemetry_subscribers::init_for_testing();
3940
3941        // GIVEN
3942        // 1 account keypair
3943        let (sender, keypair) = deterministic_random_account_key();
3944        // 12 gas objects.
3945        let gas_objects: Vec<Object> = (0..12)
3946            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
3947            .collect();
3948        // 4 owned objects.
3949        let owned_objects: Vec<Object> = (0..4)
3950            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
3951            .collect();
3952        // 6 shared objects.
3953        let shared_objects: Vec<Object> = (0..6)
3954            .map(|_| Object::shared_for_testing())
3955            .collect::<Vec<_>>();
3956        let mut all_objects = gas_objects.clone();
3957        all_objects.extend(owned_objects.clone());
3958        all_objects.extend(shared_objects.clone());
3959
3960        let network_config =
3961            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
3962                .with_objects(all_objects.clone())
3963                .build();
3964
3965        let state = TestAuthorityBuilder::new()
3966            .with_network_config(&network_config, 0)
3967            .build()
3968            .await;
3969
3970        let epoch_store = state.epoch_store_for_testing().clone();
3971        let new_epoch_start_state = epoch_store.epoch_start_state();
3972        let consensus_committee = new_epoch_start_state.get_consensus_committee();
3973
3974        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
3975
3976        let throughput_calculator = ConsensusThroughputCalculator::new(None, metrics.clone());
3977
3978        let backpressure_manager = BackpressureManager::new_for_tests();
3979        let settlement_scheduler = SettlementScheduler::new(
3980            state.execution_scheduler().as_ref().clone(),
3981            state.get_transaction_cache_reader().clone(),
3982            state.metrics.clone(),
3983        );
3984        let mut consensus_handler = ConsensusHandler::new(
3985            epoch_store,
3986            Arc::new(CheckpointServiceNoop {}),
3987            settlement_scheduler,
3988            state.get_object_cache_reader().clone(),
3989            consensus_committee.clone(),
3990            metrics,
3991            Arc::new(throughput_calculator),
3992            backpressure_manager.subscribe(),
3993            state.traffic_controller.clone(),
3994            None,
3995            state.consensus_gasless_counter.clone(),
3996            state.transaction_deny_config_manager().clone(),
3997        );
3998
3999        // AND create test user transactions alternating between owned and shared input.
4000        let mut user_transactions = vec![];
4001        for (i, gas_object) in gas_objects[0..8].iter().enumerate() {
4002            let input_object = if i % 2 == 0 {
4003                owned_objects.get(i / 2).unwrap().clone()
4004            } else {
4005                shared_objects.get(i / 2).unwrap().clone()
4006            };
4007            let transaction = test_user_transaction(
4008                &state,
4009                sender,
4010                &keypair,
4011                gas_object.clone(),
4012                vec![input_object],
4013            )
4014            .await;
4015            user_transactions.push(transaction);
4016        }
4017
4018        // AND create 4 more user transactions with remaining gas objects and 2 shared objects.
4019        // Having more txns on the same shared object may get deferred.
4020        for (i, gas_object) in gas_objects[8..12].iter().enumerate() {
4021            let shared_object = if i < 2 {
4022                shared_objects[4].clone()
4023            } else {
4024                shared_objects[5].clone()
4025            };
4026            let transaction = test_user_transaction(
4027                &state,
4028                sender,
4029                &keypair,
4030                gas_object.clone(),
4031                vec![shared_object],
4032            )
4033            .await;
4034            user_transactions.push(transaction);
4035        }
4036
4037        // AND create block for each user transaction
4038        let mut blocks = Vec::new();
4039        for (i, consensus_transaction) in user_transactions
4040            .iter()
4041            .cloned()
4042            .map(|t| ConsensusTransaction::new_user_transaction_v2_message(&state.name, t.into()))
4043            .enumerate()
4044        {
4045            let transaction_bytes = bcs::to_bytes(&consensus_transaction).unwrap();
4046            let block = VerifiedBlock::new_for_test(
4047                TestBlock::new(100 + i as u32, (i % consensus_committee.size()) as u32)
4048                    .set_transactions(vec![Transaction::new(transaction_bytes)])
4049                    .build(),
4050            );
4051
4052            blocks.push(block);
4053        }
4054
4055        // AND create the consensus commit
4056        let leader_block = blocks[0].clone();
4057        let committed_sub_dag = CommittedSubDag::new(
4058            leader_block.reference(),
4059            blocks.clone(),
4060            leader_block.timestamp_ms(),
4061            CommitRef::new(10, CommitDigest::MIN),
4062        );
4063
4064        // Test that the consensus handler respects backpressure.
4065        backpressure_manager.set_backpressure(true);
4066        // Default watermarks are 0,0 which will suppress the backpressure.
4067        backpressure_manager.update_highest_certified_checkpoint(1);
4068
4069        // AND process the consensus commit once
4070        {
4071            let waiter =
4072                consensus_handler.handle_consensus_commit_for_test(committed_sub_dag.clone());
4073            pin_mut!(waiter);
4074
4075            // waiter should not complete within 5 seconds
4076            tokio::time::timeout(std::time::Duration::from_secs(5), &mut waiter)
4077                .await
4078                .unwrap_err();
4079
4080            // lift backpressure
4081            backpressure_manager.set_backpressure(false);
4082
4083            // waiter completes now.
4084            tokio::time::timeout(std::time::Duration::from_secs(100), waiter)
4085                .await
4086                .unwrap();
4087        }
4088
4089        // THEN check the consensus stats
4090        let num_blocks = blocks.len();
4091        let num_transactions = user_transactions.len();
4092        let last_consensus_stats_1 = consensus_handler.last_consensus_stats.clone();
4093        assert_eq!(
4094            last_consensus_stats_1.index.transaction_index,
4095            num_transactions as u64
4096        );
4097        assert_eq!(last_consensus_stats_1.index.sub_dag_index, 10_u64);
4098        assert_eq!(last_consensus_stats_1.index.last_committed_round, 100_u64);
4099        assert_eq!(
4100            last_consensus_stats_1.stats.get_num_messages(0),
4101            num_blocks as u64
4102        );
4103        assert_eq!(
4104            last_consensus_stats_1.stats.get_num_user_transactions(0),
4105            num_transactions as u64
4106        );
4107
4108        // THEN check for execution status of user transactions.
4109        for (i, t) in user_transactions.iter().enumerate() {
4110            let digest = t.tx().digest();
4111            if tokio::time::timeout(
4112                std::time::Duration::from_secs(10),
4113                state.notify_read_effects_for_testing("", *digest),
4114            )
4115            .await
4116            .is_ok()
4117            {
4118                // Effects exist as expected.
4119            } else {
4120                panic!("User transaction {} {} did not execute", i, digest);
4121            }
4122        }
4123
4124        // THEN check for no inflight or suspended transactions.
4125        state.execution_scheduler().check_empty_for_testing().await;
4126    }
4127
4128    #[tokio::test(flavor = "current_thread")]
4129    async fn test_dropped_owned_object_lock_conflict_is_marked_processed() {
4130        telemetry_subscribers::init_for_testing();
4131
4132        let (sender, keypair) = deterministic_random_account_key();
4133        let gas_objects: Vec<Object> = (0..2)
4134            .map(|_| Object::with_id_owner_for_testing(ObjectID::random(), sender))
4135            .collect();
4136        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4137        let mut all_objects = gas_objects.clone();
4138        all_objects.push(owned_object.clone());
4139
4140        let state = TestAuthorityBuilder::new()
4141            .with_starting_objects(&all_objects)
4142            .skip_genesis_owner_index()
4143            .build()
4144            .await;
4145        let epoch_store = state.epoch_store_for_testing();
4146        let owned_object_ref = state
4147            .get_object(&owned_object.id())
4148            .unwrap()
4149            .compute_object_reference();
4150
4151        let winner = test_user_transaction(
4152            &state,
4153            sender,
4154            &keypair,
4155            gas_objects[0].clone(),
4156            vec![owned_object.clone()],
4157        )
4158        .await;
4159        let loser = test_user_transaction(
4160            &state,
4161            sender,
4162            &keypair,
4163            gas_objects[1].clone(),
4164            vec![owned_object.clone()],
4165        )
4166        .await;
4167
4168        let winner_digest = *winner.tx().digest();
4169        let loser_digest = *loser.tx().digest();
4170        assert_ne!(winner_digest, loser_digest);
4171
4172        let winner_consensus_tx =
4173            ConsensusTransaction::new_user_transaction_v2_message(&state.name, winner.into());
4174        let loser_consensus_tx =
4175            ConsensusTransaction::new_user_transaction_v2_message(&state.name, loser.into());
4176        let winner_key = SequencedConsensusTransactionKey::External(winner_consensus_tx.key());
4177        let loser_key = SequencedConsensusTransactionKey::External(loser_consensus_tx.key());
4178
4179        let round = 100;
4180        let commit = TestConsensusCommit::new(
4181            vec![winner_consensus_tx, loser_consensus_tx],
4182            round as u64,
4183            1_000,
4184            10,
4185        );
4186        let mut setup = setup_consensus_handler_for_testing(&state).await;
4187        setup
4188            .consensus_handler
4189            .handle_consensus_commit_for_test(commit)
4190            .await;
4191        assert_eq!(
4192            setup
4193                .consensus_handler
4194                .metrics
4195                .consensus_handler_dropped_transactions
4196                .with_label_values(&["lock_conflict"])
4197                .get(),
4198            1
4199        );
4200
4201        let block = BlockRef {
4202            author: consensus_config::AuthorityIndex::ZERO,
4203            round,
4204            digest: Default::default(),
4205        };
4206        assert!(matches!(
4207            epoch_store
4208                .consensus_tx_status_cache
4209                .notify_read_transaction_status(ConsensusPosition {
4210                    epoch: epoch_store.epoch(),
4211                    block,
4212                    index: 0,
4213                })
4214                .await,
4215            NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized)
4216        ));
4217        assert!(matches!(
4218            epoch_store
4219                .consensus_tx_status_cache
4220                .notify_read_transaction_status(ConsensusPosition {
4221                    epoch: epoch_store.epoch(),
4222                    block,
4223                    index: 1,
4224                })
4225                .await,
4226            NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Dropped)
4227        ));
4228
4229        let locks = epoch_store
4230            .get_owned_object_locks_map(&[owned_object_ref])
4231            .unwrap();
4232        assert_eq!(locks.get(&owned_object_ref), Some(&winner_digest));
4233        assert!(
4234            epoch_store
4235                .is_consensus_message_processed(&winner_key)
4236                .unwrap()
4237        );
4238        assert!(
4239            epoch_store
4240                .is_consensus_message_processed(&loser_key)
4241                .unwrap()
4242        );
4243        // The processed notification resolves for the dropped key as well — this is
4244        // the signal consensus adapter waiters block on.
4245        tokio::time::timeout(
4246            std::time::Duration::from_secs(5),
4247            epoch_store.consensus_messages_processed_notify(vec![loser_key]),
4248        )
4249        .await
4250        .expect("processed notification for dropped transaction should resolve")
4251        .unwrap();
4252    }
4253
4254    #[tokio::test(flavor = "current_thread")]
4255    async fn test_rejected_transaction_sets_status_and_is_not_marked_processed() {
4256        telemetry_subscribers::init_for_testing();
4257
4258        let (sender, keypair) = deterministic_random_account_key();
4259        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4260        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4261
4262        let state = TestAuthorityBuilder::new()
4263            .with_starting_objects(&[gas_object.clone(), owned_object.clone()])
4264            .skip_genesis_owner_index()
4265            .build()
4266            .await;
4267        let epoch_store = state.epoch_store_for_testing();
4268
4269        let transaction =
4270            test_user_transaction(&state, sender, &keypair, gas_object, vec![owned_object]).await;
4271        let consensus_tx =
4272            ConsensusTransaction::new_user_transaction_v2_message(&state.name, transaction.into());
4273        let key = SequencedConsensusTransactionKey::External(consensus_tx.key());
4274
4275        let round = 100;
4276        let commit = TestConsensusCommit::new(vec![consensus_tx], round as u64, 1_000, 10)
4277            .with_rejected_indices([0]);
4278        let mut setup = setup_consensus_handler_for_testing(&state).await;
4279        setup
4280            .consensus_handler
4281            .handle_consensus_commit_for_test(commit)
4282            .await;
4283
4284        // The rejected position receives a terminal Rejected status.
4285        let block = BlockRef {
4286            author: consensus_config::AuthorityIndex::ZERO,
4287            round,
4288            digest: Default::default(),
4289        };
4290        assert!(matches!(
4291            epoch_store
4292                .consensus_tx_status_cache
4293                .notify_read_transaction_status(ConsensusPosition {
4294                    epoch: epoch_store.epoch(),
4295                    block,
4296                    index: 0,
4297                })
4298                .await,
4299            NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Rejected)
4300        ));
4301        // Unlike dropped transactions, a rejected transaction must NOT be marked
4302        // consensus-processed: rejection is per-position, and the digest must stay
4303        // resubmittable within the epoch so a later occurrence can be finalized.
4304        assert!(!epoch_store.is_consensus_message_processed(&key).unwrap());
4305    }
4306
4307    #[tokio::test(flavor = "current_thread")]
4308    async fn test_user_transaction_ignored_at_epoch_close_sets_dropped_status() {
4309        telemetry_subscribers::init_for_testing();
4310
4311        let (sender, keypair) = deterministic_random_account_key();
4312        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4313        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4314
4315        let state = TestAuthorityBuilder::new()
4316            .with_starting_objects(&[gas_object.clone(), owned_object.clone()])
4317            .skip_genesis_owner_index()
4318            .build()
4319            .await;
4320        let epoch_store = state.epoch_store_for_testing();
4321
4322        let transaction =
4323            test_user_transaction(&state, sender, &keypair, gas_object, vec![owned_object]).await;
4324        let consensus_tx =
4325            ConsensusTransaction::new_user_transaction_v2_message(&state.name, transaction.into());
4326        let key = SequencedConsensusTransactionKey::External(consensus_tx.key());
4327
4328        // Close the epoch to the point where consensus certs are no longer accepted.
4329        {
4330            let mut guard = epoch_store.get_reconfig_state_write_lock_guard();
4331            guard.close_all_certs();
4332        }
4333
4334        let round = 100;
4335        let commit = TestConsensusCommit::new(vec![consensus_tx], round as u64, 1_000, 10);
4336        let mut setup = setup_consensus_handler_for_testing(&state).await;
4337        setup
4338            .consensus_handler
4339            .handle_consensus_commit_for_test(commit)
4340            .await;
4341
4342        // The ignored position receives a terminal Dropped status so submission and
4343        // effects waiters are not leaked until epoch termination.
4344        let block = BlockRef {
4345            author: consensus_config::AuthorityIndex::ZERO,
4346            round,
4347            digest: Default::default(),
4348        };
4349        let status = tokio::time::timeout(
4350            std::time::Duration::from_secs(5),
4351            epoch_store
4352                .consensus_tx_status_cache
4353                .notify_read_transaction_status(ConsensusPosition {
4354                    epoch: epoch_store.epoch(),
4355                    block,
4356                    index: 0,
4357                }),
4358        )
4359        .await
4360        .expect("transaction ignored at epoch close should receive a terminal status");
4361        assert!(matches!(
4362            status,
4363            NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Dropped)
4364        ));
4365        assert!(!epoch_store.is_consensus_message_processed(&key).unwrap());
4366        assert_eq!(
4367            setup
4368                .consensus_handler
4369                .metrics
4370                .consensus_handler_dropped_transactions
4371                .with_label_values(&["end_of_epoch"])
4372                .get(),
4373            1
4374        );
4375    }
4376
4377    #[tokio::test(flavor = "current_thread")]
4378    async fn test_user_transaction_after_end_of_publish_sets_dropped_status() {
4379        telemetry_subscribers::init_for_testing();
4380
4381        let (sender, keypair) = deterministic_random_account_key();
4382        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4383        let owned_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
4384
4385        let state = TestAuthorityBuilder::new()
4386            .with_starting_objects(&[gas_object.clone(), owned_object.clone()])
4387            .skip_genesis_owner_index()
4388            .build()
4389            .await;
4390        let epoch_store = state.epoch_store_for_testing();
4391
4392        let transaction =
4393            test_user_transaction(&state, sender, &keypair, gas_object, vec![owned_object]).await;
4394        let consensus_tx =
4395            ConsensusTransaction::new_user_transaction_v2_message(&state.name, transaction.into());
4396        let key = SequencedConsensusTransactionKey::External(consensus_tx.key());
4397
4398        // Record that this authority (the block author in TestConsensusCommit) already
4399        // sent EndOfPublish, without advancing the reconfig state, so the commit below
4400        // exercises the post-EndOfPublish author filter rather than the certs-closed one.
4401        epoch_store
4402            .end_of_publish
4403            .try_lock()
4404            .unwrap()
4405            .insert_generic(state.name, ());
4406
4407        let round = 100;
4408        let commit = TestConsensusCommit::new(vec![consensus_tx], round as u64, 1_000, 10);
4409        let mut setup = setup_consensus_handler_for_testing(&state).await;
4410        setup
4411            .consensus_handler
4412            .handle_consensus_commit_for_test(commit)
4413            .await;
4414
4415        let block = BlockRef {
4416            author: consensus_config::AuthorityIndex::ZERO,
4417            round,
4418            digest: Default::default(),
4419        };
4420        let status = tokio::time::timeout(
4421            std::time::Duration::from_secs(5),
4422            epoch_store
4423                .consensus_tx_status_cache
4424                .notify_read_transaction_status(ConsensusPosition {
4425                    epoch: epoch_store.epoch(),
4426                    block,
4427                    index: 0,
4428                }),
4429        )
4430        .await
4431        .expect("transaction ignored after EndOfPublish should receive a terminal status");
4432        assert!(matches!(
4433            status,
4434            NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Dropped)
4435        ));
4436        assert!(!epoch_store.is_consensus_message_processed(&key).unwrap());
4437        assert_eq!(
4438            setup
4439                .consensus_handler
4440                .metrics
4441                .consensus_handler_dropped_transactions
4442                .with_label_values(&["end_of_publish"])
4443                .get(),
4444            1
4445        );
4446    }
4447
4448    fn to_short_strings(txs: Vec<VerifiedExecutableTransactionWithAliases>) -> Vec<String> {
4449        txs.into_iter()
4450            .map(|tx| format!("transaction({})", tx.tx().transaction_data().gas_price()))
4451            .collect()
4452    }
4453
4454    #[test]
4455    fn test_order_by_gas_price() {
4456        let mut v = vec![user_txn(42), user_txn(100)];
4457        PostConsensusTxReorder::reorder(&mut v, ConsensusTransactionOrdering::ByGasPrice);
4458        assert_eq!(
4459            to_short_strings(v),
4460            vec![
4461                "transaction(100)".to_string(),
4462                "transaction(42)".to_string(),
4463            ]
4464        );
4465
4466        let mut v = vec![
4467            user_txn(1200),
4468            user_txn(12),
4469            user_txn(1000),
4470            user_txn(42),
4471            user_txn(100),
4472            user_txn(1000),
4473        ];
4474        PostConsensusTxReorder::reorder(&mut v, ConsensusTransactionOrdering::ByGasPrice);
4475        assert_eq!(
4476            to_short_strings(v),
4477            vec![
4478                "transaction(1200)".to_string(),
4479                "transaction(1000)".to_string(),
4480                "transaction(1000)".to_string(),
4481                "transaction(100)".to_string(),
4482                "transaction(42)".to_string(),
4483                "transaction(12)".to_string(),
4484            ]
4485        );
4486    }
4487
4488    #[tokio::test(flavor = "current_thread")]
4489    async fn test_checkpoint_signature_dedup() {
4490        telemetry_subscribers::init_for_testing();
4491
4492        let network_config =
4493            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
4494        let state = TestAuthorityBuilder::new()
4495            .with_network_config(&network_config, 0)
4496            .build()
4497            .await;
4498
4499        let epoch_store = state.epoch_store_for_testing().clone();
4500        let consensus_committee = epoch_store.epoch_start_state().get_consensus_committee();
4501
4502        let make_signed = || {
4503            let epoch = epoch_store.epoch();
4504            let contents =
4505                CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
4506            let summary = CheckpointSummary::new(
4507                &ProtocolConfig::get_for_max_version_UNSAFE(),
4508                epoch,
4509                42, // sequence number
4510                10, // network_total_transactions
4511                &contents,
4512                None, // previous_digest
4513                GasCostSummary::default(),
4514                None,       // end_of_epoch_data
4515                0,          // timestamp
4516                Vec::new(), // randomness_rounds
4517                Vec::new(), // checkpoint_artifact_digests
4518            );
4519            SignedCheckpointSummary::new(epoch, summary, &*state.secret, state.name)
4520        };
4521
4522        // Prepare V2 pair: same (authority, seq), different digests => different keys
4523        let v2_s1 = make_signed();
4524        let v2_s1_clone = v2_s1.clone();
4525        let v2_digest_a = v2_s1.data().digest();
4526        let v2_a =
4527            ConsensusTransaction::new_checkpoint_signature_message_v2(CheckpointSignatureMessage {
4528                summary: v2_s1,
4529            });
4530
4531        let v2_s2 = make_signed();
4532        let v2_digest_b = v2_s2.data().digest();
4533        let v2_b =
4534            ConsensusTransaction::new_checkpoint_signature_message_v2(CheckpointSignatureMessage {
4535                summary: v2_s2,
4536            });
4537
4538        assert_ne!(v2_digest_a, v2_digest_b);
4539
4540        // Create an exact duplicate with same digest to exercise valid dedup
4541        assert_eq!(v2_s1_clone.data().digest(), v2_digest_a);
4542        let v2_dup =
4543            ConsensusTransaction::new_checkpoint_signature_message_v2(CheckpointSignatureMessage {
4544                summary: v2_s1_clone,
4545            });
4546
4547        let to_tx = |ct: &ConsensusTransaction| Transaction::new(bcs::to_bytes(ct).unwrap());
4548        let block = VerifiedBlock::new_for_test(
4549            TestBlock::new(100, 0)
4550                .set_transactions(vec![to_tx(&v2_a), to_tx(&v2_b), to_tx(&v2_dup)])
4551                .build(),
4552        );
4553        let commit = CommittedSubDag::new(
4554            block.reference(),
4555            vec![block.clone()],
4556            block.timestamp_ms(),
4557            CommitRef::new(10, CommitDigest::MIN),
4558        );
4559
4560        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
4561        let throughput = ConsensusThroughputCalculator::new(None, metrics.clone());
4562        let backpressure = BackpressureManager::new_for_tests();
4563        let settlement_scheduler = SettlementScheduler::new(
4564            state.execution_scheduler().as_ref().clone(),
4565            state.get_transaction_cache_reader().clone(),
4566            state.metrics.clone(),
4567        );
4568        let mut handler = ConsensusHandler::new(
4569            epoch_store.clone(),
4570            Arc::new(CheckpointServiceNoop {}),
4571            settlement_scheduler,
4572            state.get_object_cache_reader().clone(),
4573            consensus_committee.clone(),
4574            metrics,
4575            Arc::new(throughput),
4576            backpressure.subscribe(),
4577            state.traffic_controller.clone(),
4578            None,
4579            state.consensus_gasless_counter.clone(),
4580            state.transaction_deny_config_manager().clone(),
4581        );
4582
4583        handler.handle_consensus_commit_for_test(commit).await;
4584
4585        use crate::consensus_handler::SequencedConsensusTransactionKey as SK;
4586        use sui_types::messages_consensus::ConsensusTransactionKey as CK;
4587
4588        // V2 distinct digests: both must be processed. If these were collapsed to one CheckpointSeq num, only one would process.
4589        let v2_key_a = SK::External(CK::CheckpointSignatureV2(state.name, 42, v2_digest_a));
4590        let v2_key_b = SK::External(CK::CheckpointSignatureV2(state.name, 42, v2_digest_b));
4591        assert!(
4592            epoch_store
4593                .is_consensus_message_processed(&v2_key_a)
4594                .unwrap()
4595        );
4596        assert!(
4597            epoch_store
4598                .is_consensus_message_processed(&v2_key_b)
4599                .unwrap()
4600        );
4601    }
4602
4603    #[tokio::test(flavor = "current_thread")]
4604    async fn test_verify_consensus_transaction_filters_mismatched_authorities() {
4605        telemetry_subscribers::init_for_testing();
4606
4607        let network_config =
4608            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
4609        let state = TestAuthorityBuilder::new()
4610            .with_network_config(&network_config, 0)
4611            .build()
4612            .await;
4613
4614        let epoch_store = state.epoch_store_for_testing().clone();
4615        let consensus_committee = epoch_store.epoch_start_state().get_consensus_committee();
4616
4617        // Create a different authority than our test authority
4618        use fastcrypto::traits::KeyPair;
4619        let (_, wrong_keypair) = sui_types::crypto::get_authority_key_pair();
4620        let wrong_authority: AuthorityName = wrong_keypair.public().into();
4621
4622        // Create EndOfPublish transaction with mismatched authority
4623        let mismatched_eop = ConsensusTransaction::new_end_of_publish(wrong_authority);
4624
4625        // Create valid EndOfPublish transaction with correct authority
4626        let valid_eop = ConsensusTransaction::new_end_of_publish(state.name);
4627
4628        // Create CheckpointSignature with mismatched authority
4629        let epoch = epoch_store.epoch();
4630        let contents =
4631            CheckpointContents::new_with_digests_only_for_tests([ExecutionDigests::random()]);
4632        let summary = CheckpointSummary::new(
4633            &ProtocolConfig::get_for_max_version_UNSAFE(),
4634            epoch,
4635            42, // sequence number
4636            10, // network_total_transactions
4637            &contents,
4638            None, // previous_digest
4639            GasCostSummary::default(),
4640            None,       // end_of_epoch_data
4641            0,          // timestamp
4642            Vec::new(), // randomness_rounds
4643            Vec::new(), // checkpoint commitments
4644        );
4645
4646        // Create a signed checkpoint with the wrong authority
4647        let mismatched_checkpoint_signed =
4648            SignedCheckpointSummary::new(epoch, summary.clone(), &wrong_keypair, wrong_authority);
4649        let mismatched_checkpoint_digest = mismatched_checkpoint_signed.data().digest();
4650        let mismatched_checkpoint =
4651            ConsensusTransaction::new_checkpoint_signature_message_v2(CheckpointSignatureMessage {
4652                summary: mismatched_checkpoint_signed,
4653            });
4654
4655        // Create a valid checkpoint signature with correct authority
4656        let valid_checkpoint_signed =
4657            SignedCheckpointSummary::new(epoch, summary, &*state.secret, state.name);
4658        let valid_checkpoint_digest = valid_checkpoint_signed.data().digest();
4659        let valid_checkpoint =
4660            ConsensusTransaction::new_checkpoint_signature_message_v2(CheckpointSignatureMessage {
4661                summary: valid_checkpoint_signed,
4662            });
4663
4664        let to_tx = |ct: &ConsensusTransaction| Transaction::new(bcs::to_bytes(ct).unwrap());
4665
4666        // Create a block with both valid and invalid transactions
4667        let block = VerifiedBlock::new_for_test(
4668            TestBlock::new(100, 0)
4669                .set_transactions(vec![
4670                    to_tx(&mismatched_eop),
4671                    to_tx(&valid_eop),
4672                    to_tx(&mismatched_checkpoint),
4673                    to_tx(&valid_checkpoint),
4674                ])
4675                .build(),
4676        );
4677        let commit = CommittedSubDag::new(
4678            block.reference(),
4679            vec![block.clone()],
4680            block.timestamp_ms(),
4681            CommitRef::new(10, CommitDigest::MIN),
4682        );
4683
4684        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
4685        let throughput = ConsensusThroughputCalculator::new(None, metrics.clone());
4686        let backpressure = BackpressureManager::new_for_tests();
4687        let settlement_scheduler = SettlementScheduler::new(
4688            state.execution_scheduler().as_ref().clone(),
4689            state.get_transaction_cache_reader().clone(),
4690            state.metrics.clone(),
4691        );
4692        let mut handler = ConsensusHandler::new(
4693            epoch_store.clone(),
4694            Arc::new(CheckpointServiceNoop {}),
4695            settlement_scheduler,
4696            state.get_object_cache_reader().clone(),
4697            consensus_committee.clone(),
4698            metrics,
4699            Arc::new(throughput),
4700            backpressure.subscribe(),
4701            state.traffic_controller.clone(),
4702            None,
4703            state.consensus_gasless_counter.clone(),
4704            state.transaction_deny_config_manager().clone(),
4705        );
4706
4707        handler.handle_consensus_commit_for_test(commit).await;
4708
4709        use crate::consensus_handler::SequencedConsensusTransactionKey as SK;
4710        use sui_types::messages_consensus::ConsensusTransactionKey as CK;
4711
4712        // Check that valid transactions were processed
4713        let valid_eop_key = SK::External(CK::EndOfPublish(state.name));
4714        assert!(
4715            epoch_store
4716                .is_consensus_message_processed(&valid_eop_key)
4717                .unwrap(),
4718            "Valid EndOfPublish should have been processed"
4719        );
4720
4721        let valid_checkpoint_key = SK::External(CK::CheckpointSignatureV2(
4722            state.name,
4723            42,
4724            valid_checkpoint_digest,
4725        ));
4726        assert!(
4727            epoch_store
4728                .is_consensus_message_processed(&valid_checkpoint_key)
4729                .unwrap(),
4730            "Valid CheckpointSignature should have been processed"
4731        );
4732
4733        // Check that mismatched authority transactions were NOT processed (filtered out by verify_consensus_transaction)
4734        let mismatched_eop_key = SK::External(CK::EndOfPublish(wrong_authority));
4735        assert!(
4736            !epoch_store
4737                .is_consensus_message_processed(&mismatched_eop_key)
4738                .unwrap(),
4739            "Mismatched EndOfPublish should NOT have been processed (filtered by verify_consensus_transaction)"
4740        );
4741
4742        let mismatched_checkpoint_key = SK::External(CK::CheckpointSignatureV2(
4743            wrong_authority,
4744            42,
4745            mismatched_checkpoint_digest,
4746        ));
4747        assert!(
4748            !epoch_store
4749                .is_consensus_message_processed(&mismatched_checkpoint_key)
4750                .unwrap(),
4751            "Mismatched CheckpointSignature should NOT have been processed (filtered by verify_consensus_transaction)"
4752        );
4753    }
4754
4755    /// Committed deny-config updates are applied by the commit handler even though the
4756    /// live path already applies them at block verification: a validator that is
4757    /// catching up can process commits without verifying every block. Updates are
4758    /// attributed to the consensus block author, so spoofed authority claims are
4759    /// still dropped.
4760    #[tokio::test(flavor = "current_thread")]
4761    async fn test_deny_config_updates_applied_at_commit() {
4762        telemetry_subscribers::init_for_testing();
4763
4764        let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut c| {
4765            c.set_share_transaction_deny_config_in_consensus_for_testing(true);
4766            c
4767        });
4768
4769        // A single-validator committee, so block author index 0 maps to `state.name`.
4770        let network_config =
4771            sui_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir()
4772                .committee_size(std::num::NonZeroUsize::new(1).unwrap())
4773                .build();
4774        let state = TestAuthorityBuilder::new()
4775            .with_network_config(&network_config, 0)
4776            .build()
4777            .await;
4778        let epoch_store = state.epoch_store_for_testing().clone();
4779        let consensus_committee = epoch_store.epoch_start_state().get_consensus_committee();
4780        let manager = state.transaction_deny_config_manager().clone();
4781
4782        let now_ms = crate::authority::AuthorityState::unixtime_now_ms();
4783        let make_update = |authority, generation| {
4784            ConsensusTransaction::new_update_transaction_deny_config(
4785                SharedTransactionDenyConfig::V1(
4786                    sui_types::messages_consensus::SharedTransactionDenyConfigV1 {
4787                        authority,
4788                        generation,
4789                        rules: Some(sui_types::transaction_deny_rules::TransactionDenyRules {
4790                            package_publish_disabled: true,
4791                            ..Default::default()
4792                        }),
4793                    },
4794                ),
4795            )
4796        };
4797
4798        let spoofed = make_update(AuthorityName::ZERO, now_ms + 1);
4799        let sane = make_update(state.name, now_ms);
4800
4801        let to_tx = |ct: &ConsensusTransaction| Transaction::new(bcs::to_bytes(ct).unwrap());
4802        let block = VerifiedBlock::new_for_test(
4803            TestBlock::new(100, 0)
4804                .set_transactions(vec![to_tx(&spoofed), to_tx(&sane)])
4805                .build(),
4806        );
4807        let commit = CommittedSubDag::new(
4808            block.reference(),
4809            vec![block.clone()],
4810            block.timestamp_ms(),
4811            CommitRef::new(10, CommitDigest::MIN),
4812        );
4813
4814        let metrics = Arc::new(AuthorityMetrics::new(&Registry::new()));
4815        let throughput = ConsensusThroughputCalculator::new(None, metrics.clone());
4816        let backpressure = BackpressureManager::new_for_tests();
4817        let settlement_scheduler = SettlementScheduler::new(
4818            state.execution_scheduler().as_ref().clone(),
4819            state.get_transaction_cache_reader().clone(),
4820            state.metrics.clone(),
4821        );
4822        let mut handler = ConsensusHandler::new(
4823            epoch_store.clone(),
4824            Arc::new(CheckpointServiceNoop {}),
4825            settlement_scheduler,
4826            state.get_object_cache_reader().clone(),
4827            consensus_committee.clone(),
4828            metrics,
4829            Arc::new(throughput),
4830            backpressure.subscribe(),
4831            state.traffic_controller.clone(),
4832            None,
4833            state.consensus_gasless_counter.clone(),
4834            state.transaction_deny_config_manager().clone(),
4835        );
4836
4837        handler.handle_consensus_commit_for_test(commit).await;
4838
4839        let snapshot = manager.peer_configs_snapshot();
4840        assert_eq!(
4841            snapshot.get(&state.name).map(|msg| msg.generation()),
4842            Some(now_ms),
4843            "committed update from the block author should be applied at commit time"
4844        );
4845        assert!(
4846            !snapshot.contains_key(&AuthorityName::ZERO),
4847            "spoofed authority claim should be dropped"
4848        );
4849    }
4850
4851    fn user_txn(gas_price: u64) -> VerifiedExecutableTransactionWithAliases {
4852        let (committee, keypairs) = Committee::new_simple_test_committee();
4853        let (sender, sender_keypair) = deterministic_random_account_key();
4854        let tx = sui_types::transaction::Transaction::from_data_and_signer(
4855            TransactionData::new_transfer(
4856                SuiAddress::default(),
4857                FullObjectRef::from_fastpath_ref(random_object_ref()),
4858                sender,
4859                random_object_ref(),
4860                1000 * gas_price,
4861                gas_price,
4862            ),
4863            vec![&sender_keypair],
4864        );
4865        let tx = VerifiedExecutableTransaction::new_from_certificate(
4866            VerifiedCertificate::new_unchecked(
4867                CertifiedTransaction::new_from_keypairs_for_testing(
4868                    tx.into_data(),
4869                    &keypairs,
4870                    &committee,
4871                ),
4872            ),
4873        );
4874        VerifiedExecutableTransactionWithAliases::no_aliases(tx)
4875    }
4876
4877    mod checkpoint_queue_tests {
4878        use super::*;
4879        use consensus_core::CommitRef;
4880        use sui_types::digests::Digest;
4881
4882        fn make_chunk(tx_count: usize, height: u64) -> Chunk {
4883            Chunk {
4884                schedulables: (0..tx_count)
4885                    .map(|_| Schedulable::Transaction(user_txn(1000).into_tx()))
4886                    .collect(),
4887                settlement: None,
4888                height,
4889            }
4890        }
4891
4892        fn make_commit_ref(index: u32) -> CommitRef {
4893            CommitRef {
4894                index,
4895                digest: CommitDigest::MIN,
4896            }
4897        }
4898
4899        fn default_versions() -> HashMap<TransactionKey, AssignedVersions> {
4900            HashMap::new()
4901        }
4902
4903        #[test]
4904        fn test_flush_all_checkpoint_roots() {
4905            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, 1000, 0);
4906            let versions = default_versions();
4907
4908            queue.push_chunk(
4909                make_chunk(5, 1),
4910                &versions,
4911                1000,
4912                make_commit_ref(1),
4913                Digest::default(),
4914            );
4915            queue.push_chunk(
4916                make_chunk(3, 2),
4917                &versions,
4918                1000,
4919                make_commit_ref(1),
4920                Digest::default(),
4921            );
4922
4923            let pending = queue.flush(1000, true);
4924
4925            assert!(pending.is_some());
4926            assert!(queue.pending_roots.is_empty());
4927        }
4928
4929        #[test]
4930        fn test_flush_respects_min_checkpoint_interval() {
4931            let min_interval = 200;
4932            let mut queue = CheckpointQueue::new_for_testing(1000, 0, 0, 1000, min_interval);
4933            let versions = default_versions();
4934
4935            queue.push_chunk(
4936                make_chunk(5, 1),
4937                &versions,
4938                1000,
4939                make_commit_ref(1),
4940                Digest::default(),
4941            );
4942
4943            let pending = queue.flush(1000 + min_interval - 1, false);
4944            assert!(pending.is_none());
4945            assert_eq!(queue.pending_roots.len(), 1);
4946
4947            let pending = queue.flush(1000 + min_interval, false);
4948            assert!(pending.is_some());
4949            assert!(queue.pending_roots.is_empty());
4950        }
4951
4952        #[test]
4953        fn test_push_chunk_flushes_when_exceeds_max() {
4954            let max_tx = 10;
4955            let mut queue = CheckpointQueue::new_for_testing(1000, 0, 0, max_tx, 0);
4956            let versions = default_versions();
4957
4958            queue.push_chunk(
4959                make_chunk(max_tx / 2 + 1, 1),
4960                &versions,
4961                1000,
4962                make_commit_ref(1),
4963                Digest::default(),
4964            );
4965
4966            let flushed = queue.push_chunk(
4967                make_chunk(max_tx / 2 + 1, 2),
4968                &versions,
4969                1000,
4970                make_commit_ref(2),
4971                Digest::default(),
4972            );
4973
4974            assert_eq!(flushed.len(), 1);
4975            assert_eq!(queue.pending_roots.len(), 1);
4976        }
4977
4978        #[test]
4979        fn test_multiple_chunks_merged_into_one_checkpoint() {
4980            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, 1000, 200);
4981            let versions = default_versions();
4982
4983            queue.push_chunk(
4984                make_chunk(10, 1),
4985                &versions,
4986                1000,
4987                make_commit_ref(1),
4988                Digest::default(),
4989            );
4990            queue.push_chunk(
4991                make_chunk(10, 2),
4992                &versions,
4993                1000,
4994                make_commit_ref(2),
4995                Digest::default(),
4996            );
4997            queue.push_chunk(
4998                make_chunk(10, 3),
4999                &versions,
5000                1000,
5001                make_commit_ref(3),
5002                Digest::default(),
5003            );
5004
5005            let pending = queue.flush(1000, true).unwrap();
5006
5007            assert_eq!(pending.roots.len(), 3);
5008        }
5009
5010        #[test]
5011        fn test_push_chunk_handles_overflow() {
5012            let max_tx = 10;
5013            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, max_tx, 0);
5014            let versions = default_versions();
5015
5016            let flushed1 = queue.push_chunk(
5017                make_chunk(max_tx / 2, 1),
5018                &versions,
5019                1000,
5020                make_commit_ref(1),
5021                Digest::default(),
5022            );
5023            assert!(flushed1.is_empty());
5024
5025            let flushed2 = queue.push_chunk(
5026                make_chunk(max_tx / 2, 2),
5027                &versions,
5028                1000,
5029                make_commit_ref(2),
5030                Digest::default(),
5031            );
5032            assert!(flushed2.is_empty());
5033
5034            let flushed3 = queue.push_chunk(
5035                make_chunk(max_tx / 2, 3),
5036                &versions,
5037                1000,
5038                make_commit_ref(3),
5039                Digest::default(),
5040            );
5041            assert_eq!(flushed3.len(), 1);
5042
5043            let pending = queue.flush(1000, true);
5044
5045            for p in pending.iter().chain(flushed3.iter()) {
5046                let tx_count: usize = p.roots.iter().map(|r| r.tx_roots.len()).sum();
5047                assert!(tx_count <= max_tx);
5048            }
5049        }
5050
5051        #[test]
5052        fn test_checkpoint_uses_last_chunk_height() {
5053            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, 1000, 0);
5054            let versions = default_versions();
5055
5056            queue.push_chunk(
5057                make_chunk(10, 100),
5058                &versions,
5059                1000,
5060                make_commit_ref(1),
5061                Digest::default(),
5062            );
5063            queue.push_chunk(
5064                make_chunk(10, 200),
5065                &versions,
5066                1000,
5067                make_commit_ref(2),
5068                Digest::default(),
5069            );
5070
5071            let pending = queue.flush(1000, true).unwrap();
5072
5073            assert_eq!(pending.details.checkpoint_height, 200);
5074        }
5075
5076        #[test]
5077        fn test_last_built_timestamp_updated_on_flush() {
5078            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, 1000, 0);
5079            let versions = default_versions();
5080
5081            queue.push_chunk(
5082                make_chunk(10, 1),
5083                &versions,
5084                5000,
5085                make_commit_ref(1),
5086                Digest::default(),
5087            );
5088
5089            assert_eq!(queue.last_built_timestamp, 0);
5090
5091            let _ = queue.flush(5000, true);
5092
5093            assert_eq!(queue.last_built_timestamp, 5000);
5094        }
5095
5096        #[test]
5097        fn test_settlement_info_sent_through_channel() {
5098            let mut queue = CheckpointQueue::new_for_testing(0, 0, 5, 1000, 0);
5099            let versions = default_versions();
5100
5101            let chunk1 = Chunk {
5102                schedulables: vec![
5103                    Schedulable::ConsensusCommitPrologue(0, 1, 0),
5104                    Schedulable::ConsensusCommitPrologue(0, 2, 0),
5105                    Schedulable::ConsensusCommitPrologue(0, 3, 0),
5106                ],
5107                settlement: Some(Schedulable::AccumulatorSettlement(1, 1)),
5108                height: 1,
5109            };
5110
5111            let chunk2 = Chunk {
5112                schedulables: vec![
5113                    Schedulable::ConsensusCommitPrologue(0, 4, 0),
5114                    Schedulable::ConsensusCommitPrologue(0, 5, 0),
5115                ],
5116                settlement: Some(Schedulable::AccumulatorSettlement(1, 2)),
5117                height: 2,
5118            };
5119
5120            queue.push_chunk(
5121                chunk1,
5122                &versions,
5123                1000,
5124                make_commit_ref(1),
5125                Digest::default(),
5126            );
5127            queue.push_chunk(
5128                chunk2,
5129                &versions,
5130                1000,
5131                make_commit_ref(1),
5132                Digest::default(),
5133            );
5134        }
5135
5136        #[test]
5137        fn test_settlement_checkpoint_seq_correct_after_flush() {
5138            let max_tx = 10;
5139            let initial_seq = 5;
5140            let (sender, mut receiver) = monitored_mpsc::unbounded_channel("test_settlement_seq");
5141            let mut queue =
5142                CheckpointQueue::new_for_testing_with_sender(0, 0, initial_seq, max_tx, 0, sender);
5143            let versions = default_versions();
5144
5145            // Push a chunk that partially fills the queue (no flush).
5146            let chunk1 = Chunk {
5147                schedulables: (0..max_tx / 2 + 1)
5148                    .map(|_| Schedulable::Transaction(user_txn(1000).into_tx()))
5149                    .collect(),
5150                settlement: Some(Schedulable::AccumulatorSettlement(1, 1)),
5151                height: 1,
5152            };
5153            queue.push_chunk(
5154                chunk1,
5155                &versions,
5156                1000,
5157                make_commit_ref(1),
5158                Digest::default(),
5159            );
5160
5161            // Drain the first message from the channel.
5162            let msg1 = receiver.try_recv().unwrap();
5163            let settlement1 = msg1.1.unwrap();
5164            assert_eq!(settlement1.checkpoint_seq, initial_seq);
5165
5166            // Push a second chunk that triggers a flush of chunk1's roots.
5167            let chunk2 = Chunk {
5168                schedulables: (0..max_tx / 2 + 1)
5169                    .map(|_| Schedulable::Transaction(user_txn(1000).into_tx()))
5170                    .collect(),
5171                settlement: Some(Schedulable::AccumulatorSettlement(1, 2)),
5172                height: 2,
5173            };
5174            let flushed = queue.push_chunk(
5175                chunk2,
5176                &versions,
5177                1000,
5178                make_commit_ref(2),
5179                Digest::default(),
5180            );
5181            assert_eq!(flushed.len(), 1);
5182            assert_eq!(flushed[0].details.checkpoint_seq, initial_seq);
5183
5184            // The second settlement must have checkpoint_seq = initial_seq + 1,
5185            // because the flush incremented current_checkpoint_seq.
5186            let msg2 = receiver.try_recv().unwrap();
5187            let settlement2 = msg2.1.unwrap();
5188            assert_eq!(settlement2.checkpoint_seq, initial_seq + 1);
5189
5190            // Flush the remaining roots and verify the PendingCheckpoint's seq
5191            // matches the settlement's seq.
5192            let pending = queue.flush_forced().unwrap();
5193            assert_eq!(pending.details.checkpoint_seq, settlement2.checkpoint_seq);
5194        }
5195
5196        #[test]
5197        fn test_checkpoint_seq_increments_on_flush() {
5198            let mut queue = CheckpointQueue::new_for_testing(0, 0, 10, 1000, 0);
5199            let versions = default_versions();
5200
5201            queue.push_chunk(
5202                make_chunk(5, 1),
5203                &versions,
5204                1000,
5205                make_commit_ref(1),
5206                Digest::default(),
5207            );
5208
5209            let pending = queue.flush(1000, true).unwrap();
5210
5211            assert_eq!(pending.details.checkpoint_seq, 10);
5212            assert_eq!(queue.current_checkpoint_seq, 11);
5213        }
5214
5215        #[test]
5216        fn test_multiple_chunks_with_overflow() {
5217            let max_tx = 10;
5218            let mut queue = CheckpointQueue::new_for_testing(0, 0, 0, max_tx, 0);
5219            let versions = default_versions();
5220
5221            let flushed1 = queue.push_chunk(
5222                make_chunk(max_tx / 2 + 1, 1),
5223                &versions,
5224                1000,
5225                make_commit_ref(1),
5226                Digest::default(),
5227            );
5228            let flushed2 = queue.push_chunk(
5229                make_chunk(max_tx / 2 + 1, 2),
5230                &versions,
5231                1000,
5232                make_commit_ref(1),
5233                Digest::default(),
5234            );
5235            let flushed3 = queue.push_chunk(
5236                make_chunk(max_tx / 2 + 1, 3),
5237                &versions,
5238                1000,
5239                make_commit_ref(1),
5240                Digest::default(),
5241            );
5242
5243            let all_flushed: Vec<_> = flushed1
5244                .into_iter()
5245                .chain(flushed2)
5246                .chain(flushed3)
5247                .collect();
5248            assert_eq!(all_flushed.len(), 2);
5249            assert_eq!(queue.pending_roots.len(), 1);
5250
5251            for p in &all_flushed {
5252                let tx_count: usize = p.roots.iter().map(|r| r.tx_roots.len()).sum();
5253                assert!(tx_count <= max_tx);
5254            }
5255        }
5256    }
5257}