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