Skip to main content

sui_core/
consensus_handler.rs

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