Skip to main content

sui_core/
consensus_handler.rs

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