Skip to main content

sui_core/
consensus_adapter.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::net::IpAddr;
5use std::ops::Deref;
6use std::sync::Arc;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9use std::time::Instant;
10
11use consensus_core::BlockStatus;
12use futures::FutureExt;
13use futures::StreamExt;
14use futures::future::{self, Either, join_all, select};
15use futures::stream::FuturesUnordered;
16use mysten_common::debug_fatal;
17use mysten_metrics::{
18    GaugeGuard, InflightGuardFutureExt, LATENCY_SEC_BUCKETS, spawn_monitored_task,
19};
20use parking_lot::RwLockReadGuard;
21use prometheus::Histogram;
22use prometheus::HistogramVec;
23use prometheus::IntCounter;
24use prometheus::IntCounterVec;
25use prometheus::IntGauge;
26use prometheus::IntGaugeVec;
27use prometheus::Registry;
28use prometheus::{
29    register_histogram_vec_with_registry, register_histogram_with_registry,
30    register_int_counter_vec_with_registry, register_int_counter_with_registry,
31    register_int_gauge_vec_with_registry, register_int_gauge_with_registry,
32};
33use sui_types::base_types::AuthorityName;
34use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
35use sui_types::fp_ensure;
36use sui_types::messages_consensus::ConsensusPosition;
37use sui_types::messages_consensus::ConsensusTransactionKind;
38use sui_types::messages_consensus::{ConsensusTransaction, ConsensusTransactionKey};
39use tokio::sync::{Notify, Semaphore, SemaphorePermit, oneshot};
40use tokio::task::JoinHandle;
41use tokio::time::Duration;
42use tokio::time::{self};
43use tracing::{Instrument, debug, debug_span, info, instrument, warn};
44
45use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
46use crate::authority::consensus_tx_status_cache::{
47    ConsensusTxStatus, NotifyReadConsensusTxStatusResult,
48};
49use crate::checkpoints::CheckpointStore;
50use crate::consensus_handler::{SequencedConsensusTransactionKey, classify, tx_type_label};
51use crate::epoch::reconfiguration::{ReconfigState, ReconfigurationInitiator};
52use crate::staggered_submission::proposers_metric_label;
53
54#[cfg(test)]
55#[path = "unit_tests/consensus_tests.rs"]
56pub mod consensus_tests;
57
58#[derive(Clone)]
59pub struct ConsensusAdapterMetrics {
60    // Certificate sequencing metrics
61    pub sequencing_certificate_attempt: IntCounterVec,
62    pub sequencing_certificate_success: IntCounterVec,
63    pub sequencing_certificate_failures: IntCounterVec,
64    pub sequencing_certificate_status: IntCounterVec,
65    pub sequencing_certificate_settled_status: IntCounterVec,
66    pub sequencing_certificate_inflight: IntGaugeVec,
67    pub sequencing_acknowledge_latency: HistogramVec,
68    pub sequencing_certificate_latency: HistogramVec,
69    pub sequencing_certificate_processed: IntCounterVec,
70    pub sequencing_in_flight_semaphore_wait: IntGauge,
71    pub sequencing_in_flight_submissions: IntGauge,
72    pub sequencing_best_effort_timeout: IntCounterVec,
73    pub consensus_latency: Histogram,
74    pub num_rejected_cert_in_epoch_boundary: IntCounter,
75}
76
77impl ConsensusAdapterMetrics {
78    pub fn new(registry: &Registry) -> Self {
79        Self {
80            sequencing_certificate_attempt: register_int_counter_vec_with_registry!(
81                "sequencing_certificate_attempt",
82                "Counts the number of certificates the validator attempts to sequence.",
83                &["tx_type"],
84                registry,
85            )
86                .unwrap(),
87            sequencing_certificate_success: register_int_counter_vec_with_registry!(
88                "sequencing_certificate_success",
89                "Counts the number of successfully sequenced certificates.",
90                &["tx_type"],
91                registry,
92            )
93                .unwrap(),
94            sequencing_certificate_failures: register_int_counter_vec_with_registry!(
95                "sequencing_certificate_failures",
96                "Counts the number of sequenced certificates that failed other than by timeout.",
97                &["tx_type"],
98                registry,
99            )
100                .unwrap(),
101                sequencing_certificate_status: register_int_counter_vec_with_registry!(
102                "sequencing_certificate_status",
103                "The status of the certificate sequencing as reported by consensus. The status can be either sequenced or garbage collected.",
104                &["tx_type", "status"],
105                registry,
106            )
107                .unwrap(),
108            sequencing_certificate_settled_status: register_int_counter_vec_with_registry!(
109                "sequencing_certificate_settled_status",
110                "The terminal per-position consensus status (finalized, rejected or dropped) of transactions whose submission settled via position status.",
111                &["tx_type", "status"],
112                registry,
113            )
114                .unwrap(),
115            sequencing_certificate_inflight: register_int_gauge_vec_with_registry!(
116                "sequencing_certificate_inflight",
117                "The inflight requests to sequence certificates.",
118                &["tx_type"],
119                registry,
120            )
121                .unwrap(),
122            sequencing_acknowledge_latency: register_histogram_vec_with_registry!(
123                "sequencing_acknowledge_latency",
124                "The latency for acknowledgement from sequencing engine. The overall sequencing latency is measured by the sequencing_certificate_latency metric",
125                &["retry", "tx_type"],
126                LATENCY_SEC_BUCKETS.to_vec(),
127                registry,
128            ).unwrap(),
129            sequencing_certificate_latency: register_histogram_vec_with_registry!(
130                "sequencing_certificate_latency",
131                "The latency for sequencing a certificate.",
132                &["submitted", "tx_type", "processed_method", "proposers"],
133                LATENCY_SEC_BUCKETS.to_vec(),
134                registry,
135            ).unwrap(),
136            sequencing_certificate_processed: register_int_counter_vec_with_registry!(
137                "sequencing_certificate_processed",
138                "The number of certificates that have been processed either by consensus or checkpoint.",
139                &["source"],
140                registry
141            ).unwrap(),
142            sequencing_in_flight_semaphore_wait: register_int_gauge_with_registry!(
143                "sequencing_in_flight_semaphore_wait",
144                "How many requests are blocked on submit_permit.",
145                registry,
146            )
147                .unwrap(),
148            sequencing_in_flight_submissions: register_int_gauge_with_registry!(
149                "sequencing_in_flight_submissions",
150                "Number of transactions submitted to local consensus instance and not yet sequenced",
151                registry,
152            )
153                .unwrap(),
154            sequencing_best_effort_timeout: register_int_counter_vec_with_registry!(
155                "sequencing_best_effort_timeout",
156                "The number of times the best effort submission has timed out.",
157                &["tx_type"],
158                registry,
159            ).unwrap(),
160            // These two metrics originally lived in ValidatorServiceMetrics (authority_server.rs)
161            // and keep their legacy names for dashboard compatibility.
162            consensus_latency: register_histogram_with_registry!(
163                "validator_service_consensus_latency",
164                "Time spent between submitting a txn to consensus and getting back local acknowledgement. Execution and finalization time are not included.",
165                mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
166                registry,
167            ).unwrap(),
168            num_rejected_cert_in_epoch_boundary: register_int_counter_with_registry!(
169                "validator_service_num_rejected_cert_in_epoch_boundary",
170                "Number of rejected transaction certificate during epoch transitioning",
171                registry,
172            ).unwrap(),
173        }
174    }
175
176    pub fn new_test() -> Self {
177        Self::new(&Registry::default())
178    }
179}
180
181/// An object that can be used to check if the consensus is overloaded.
182pub trait ConsensusOverloadChecker: Sync + Send + 'static {
183    fn check_consensus_overload(&self) -> SuiResult;
184}
185
186pub type BlockStatusReceiver = oneshot::Receiver<BlockStatus>;
187
188#[mockall::automock]
189pub trait SubmitToConsensus: Sync + Send + 'static {
190    fn submit_to_consensus(
191        &self,
192        transactions: &[ConsensusTransaction],
193        epoch_store: &Arc<AuthorityPerEpochStore>,
194    ) -> SuiResult;
195
196    /// Submits a system transaction to consensus once, without waiting for it to
197    /// be sequenced and without retrying if it is garbage collected, bounded by
198    /// `timeout`. Suits periodic, self-superseding messages (e.g. execution time
199    /// observations) where a missed submission is replaced by the next one.
200    ///
201    /// For system transactions only. User transactions are rejected:
202    /// this fire-and-forget, no-retry, backpressure-free path would
203    /// silently mishandle them.
204    fn submit_best_effort(
205        &self,
206        transaction: &ConsensusTransaction,
207        epoch_store: &Arc<AuthorityPerEpochStore>,
208        timeout: Duration,
209    ) -> SuiResult;
210}
211
212#[mockall::automock]
213#[async_trait::async_trait]
214pub trait ConsensusClient: Sync + Send + 'static {
215    async fn submit(
216        &self,
217        transactions: &[ConsensusTransaction],
218        epoch_store: &Arc<AuthorityPerEpochStore>,
219    ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)>;
220}
221
222/// Submit Sui certificates to the consensus.
223pub struct ConsensusAdapter {
224    /// The network client connecting to the consensus node of this authority.
225    consensus_client: Arc<dyn ConsensusClient>,
226    /// The checkpoint store for the validator
227    checkpoint_store: Arc<CheckpointStore>,
228    /// Authority pubkey.
229    authority: AuthorityName,
230    /// The limit to number of inflight transactions at this node.
231    max_pending_transactions: usize,
232    /// Number of submitted transactions still inflight at this node.
233    num_inflight_transactions: AtomicU64,
234    /// A structure to register metrics
235    metrics: ConsensusAdapterMetrics,
236    /// Semaphore limiting parallel submissions to consensus
237    submit_semaphore: Arc<Semaphore>,
238    /// Notified when an inflight slot is freed (`InflightDropGuard` dropped).
239    /// Used by the admission queue drainer to wake up and submit more
240    /// transactions.
241    inflight_slot_freed_notify: Arc<Notify>,
242}
243
244impl ConsensusAdapter {
245    /// Make a new Consensus adapter instance.
246    pub fn new(
247        consensus_client: Arc<dyn ConsensusClient>,
248        checkpoint_store: Arc<CheckpointStore>,
249        authority: AuthorityName,
250        max_pending_transactions: usize,
251        max_pending_local_submissions: usize,
252        metrics: ConsensusAdapterMetrics,
253        inflight_slot_freed_notify: Arc<Notify>,
254    ) -> Self {
255        let num_inflight_transactions = Default::default();
256        Self {
257            consensus_client,
258            checkpoint_store,
259            authority,
260            max_pending_transactions,
261            num_inflight_transactions,
262            metrics,
263            submit_semaphore: Arc::new(Semaphore::new(max_pending_local_submissions)),
264            inflight_slot_freed_notify,
265        }
266    }
267
268    /// Get the current number of in-flight transactions
269    pub fn num_inflight_transactions(&self) -> u64 {
270        self.num_inflight_transactions.load(Ordering::Relaxed)
271    }
272
273    /// Get the maximum number of pending transactions (consensus capacity limit).
274    pub fn max_pending_transactions(&self) -> usize {
275        self.max_pending_transactions
276    }
277
278    /// Submits transactions to consensus within the reconfiguration lock and
279    /// returns their consensus positions.
280    pub async fn submit_and_get_positions(
281        self: &Arc<Self>,
282        consensus_transactions: Vec<ConsensusTransaction>,
283        epoch_store: &Arc<AuthorityPerEpochStore>,
284        submitter_client_addr: Option<IpAddr>,
285    ) -> Result<Vec<ConsensusPosition>, SuiError> {
286        let (tx_consensus_positions, rx_consensus_positions) = oneshot::channel();
287
288        {
289            // code block within reconfiguration lock
290            let reconfiguration_lock = epoch_store.get_reconfig_state_read_lock_guard();
291            if !reconfiguration_lock.should_accept_user_certs() {
292                self.metrics.num_rejected_cert_in_epoch_boundary.inc();
293                return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
294            }
295
296            // Submit to consensus and wait for the position. If the transaction has
297            // already been processed via consensus output or a checkpoint, the adapter
298            // skips submission and reports `TransactionProcessing` instead of a position.
299            let _metrics_guard = self.metrics.consensus_latency.start_timer();
300
301            self.submit_batch(
302                &consensus_transactions,
303                Some(&reconfiguration_lock),
304                epoch_store,
305                Some(tx_consensus_positions),
306                submitter_client_addr,
307            )?;
308        }
309
310        rx_consensus_positions.await.unwrap_or_else(|_| {
311            // The sender is dropped without a reply only when within_alive_epoch
312            // cancels the submission task at epoch end.
313            self.metrics.num_rejected_cert_in_epoch_boundary.inc();
314            Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into())
315        })
316    }
317
318    pub fn recover_end_of_publish(self: &Arc<Self>, epoch_store: &Arc<AuthorityPerEpochStore>) {
319        // This handles the case where the node crashed after setting reconfig lock state
320        // but before the EndOfPublish message was sent to consensus.
321        if epoch_store.should_send_end_of_publish() {
322            let transaction = ConsensusTransaction::new_end_of_publish(self.authority);
323            info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
324            self.submit_unchecked(&[transaction], epoch_store, None, None);
325        }
326    }
327
328    /// This method blocks until transaction is persisted in local database
329    /// It then returns handle to async task, user can join this handle to await while transaction is processed by consensus
330    ///
331    /// This method guarantees that once submit(but not returned async handle) returns,
332    /// transaction is persisted and will eventually be sent to consensus even after restart
333    ///
334    /// When submitting a certificate caller **must** provide a ReconfigState lock guard
335    pub fn submit(
336        self: &Arc<Self>,
337        transaction: ConsensusTransaction,
338        lock: Option<&RwLockReadGuard<ReconfigState>>,
339        epoch_store: &Arc<AuthorityPerEpochStore>,
340        tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
341        submitter_client_addr: Option<IpAddr>,
342    ) -> SuiResult<JoinHandle<()>> {
343        self.submit_batch(
344            &[transaction],
345            lock,
346            epoch_store,
347            tx_consensus_position,
348            submitter_client_addr,
349        )
350    }
351
352    // Submits the provided transactions to consensus in a batched fashion. The `transactions` vector can be also empty in case of a ping check.
353    // In this case the system will simulate a transaction submission to consensus and return the consensus position.
354    pub fn submit_batch(
355        self: &Arc<Self>,
356        transactions: &[ConsensusTransaction],
357        _lock: Option<&RwLockReadGuard<ReconfigState>>,
358        epoch_store: &Arc<AuthorityPerEpochStore>,
359        tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
360        submitter_client_addr: Option<IpAddr>,
361    ) -> SuiResult<JoinHandle<()>> {
362        if transactions.len() > 1 {
363            // Soft bundles must contain only UserTransactionV2 transactions.
364            for transaction in transactions {
365                fp_ensure!(
366                    transaction.is_user_transaction(),
367                    SuiErrorKind::InvalidTxKindInSoftBundle.into()
368                );
369            }
370        }
371
372        Ok(self.submit_unchecked(
373            transactions,
374            epoch_store,
375            tx_consensus_position,
376            submitter_client_addr,
377        ))
378    }
379
380    /// Performs weakly consistent checks on internal buffers to quickly
381    /// discard transactions if we are overloaded
382    fn check_limits(&self) -> bool {
383        // First check total transactions (waiting and in submission)
384        if self.num_inflight_transactions.load(Ordering::Relaxed) as usize
385            >= self.max_pending_transactions
386        {
387            return false;
388        }
389        // Then check if submit_semaphore has permits
390        self.submit_semaphore.available_permits() > 0
391    }
392
393    fn submit_unchecked(
394        self: &Arc<Self>,
395        transactions: &[ConsensusTransaction],
396        epoch_store: &Arc<AuthorityPerEpochStore>,
397        tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
398        submitter_client_addr: Option<IpAddr>,
399    ) -> JoinHandle<()> {
400        // Reconfiguration lock is dropped when pending_consensus_transactions is persisted, before it is handled by consensus
401        let async_stage = self
402            .clone()
403            .submit_and_wait(
404                transactions.to_vec(),
405                epoch_store.clone(),
406                tx_consensus_position,
407                submitter_client_addr,
408            )
409            .in_current_span();
410        // Number of these tasks is weakly limited based on `num_inflight_transactions`.
411        // (Limit is not applied atomically, and only to user transactions.)
412        let join_handle = spawn_monitored_task!(async_stage);
413        join_handle
414    }
415
416    async fn submit_and_wait(
417        self: Arc<Self>,
418        transactions: Vec<ConsensusTransaction>,
419        epoch_store: Arc<AuthorityPerEpochStore>,
420        tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
421        submitter_client_addr: Option<IpAddr>,
422    ) {
423        // When epoch_terminated signal is received all pending submit_and_wait_inner are dropped.
424        //
425        // This is needed because submit_and_wait_inner waits on read_notify for consensus message to be processed,
426        // which may never happen on epoch boundary.
427        //
428        // In addition to that, within_alive_epoch ensures that all pending consensus
429        // adapter tasks are stopped before reconfiguration can proceed.
430        //
431        // This is essential because after epoch change, this validator may exit the committee and become a full node.
432        // So it is no longer able to submit to consensus.
433        //
434        // Also, submission to consensus is not gated on epoch. Although it is ok to submit user transactions
435        // to the new epoch, we want to cancel system transaction submissions from the current epoch to the new epoch.
436        epoch_store
437            .within_alive_epoch(self.submit_and_wait_inner(
438                transactions,
439                &epoch_store,
440                tx_consensus_position,
441                submitter_client_addr,
442            ))
443            .await
444            .ok(); // result here indicates if epoch ended earlier, we don't care about it
445    }
446
447    #[allow(clippy::option_map_unit_fn)]
448    #[instrument(name="ConsensusAdapter::submit_and_wait_inner", level="trace", skip_all, fields(tx_count = ?transactions.len(), tx_type = tracing::field::Empty, tx_keys = tracing::field::Empty, submit_status = tracing::field::Empty, consensus_positions = tracing::field::Empty))]
449    async fn submit_and_wait_inner(
450        self: Arc<Self>,
451        transactions: Vec<ConsensusTransaction>,
452        epoch_store: &Arc<AuthorityPerEpochStore>,
453        mut tx_consensus_positions: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
454        submitter_client_addr: Option<IpAddr>,
455    ) {
456        if transactions.is_empty() {
457            // If transactions are empty, then we attempt to ping consensus and simulate a transaction submission to consensus.
458            // We intentionally do not wait for the block status, as we are only interested in the consensus position and return it immediately.
459            debug!(
460                "Performing a ping check, pinging consensus to get a consensus position in next block"
461            );
462            let (consensus_positions, _status_waiter) = self
463                .submit_inner(&transactions, epoch_store, &[], "ping")
464                .await;
465
466            if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
467                let _ = tx_consensus_positions.send(Ok(consensus_positions));
468            } else {
469                debug_fatal!("Ping check must have a consensus position channel");
470            }
471            return;
472        }
473
474        // Record submitted transactions early for DoS protection
475        epoch_store.record_submitted_user_transactions(&transactions, submitter_client_addr);
476
477        // Current code path ensures:
478        // - If transactions.len() > 1, it is a soft bundle. System transactions should have been submitted individually.
479        // - If is_soft_bundle, then all transactions are of CertifiedTransaction or UserTransaction kind.
480        // - If not is_soft_bundle, then transactions must contain exactly 1 tx, and transactions[0] can be of any kind.
481        let is_soft_bundle = transactions.len() > 1;
482        let is_system_message = !transactions[0].is_user_transaction();
483
484        let mut transaction_keys = Vec::new();
485        let mut tx_consensus_positions = tx_consensus_positions;
486
487        for transaction in &transactions {
488            if matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..)) {
489                info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
490                epoch_store.record_epoch_pending_certs_process_time_metric();
491            }
492
493            let transaction_key = SequencedConsensusTransactionKey::External(transaction.key());
494            transaction_keys.push(transaction_key);
495        }
496        let tx_type = tx_type_label(&transactions);
497        tracing::Span::current().record("tx_type", tx_type);
498        tracing::Span::current().record("tx_keys", tracing::field::debug(&transaction_keys));
499
500        let mut guard = InflightDropGuard::acquire(
501            &self,
502            tx_type,
503            proposers_metric_label(&transactions, epoch_store),
504            transactions.len() as u64,
505        );
506
507        let make_processing_error =
508            |method: ProcessedMethod| -> SuiError { processing_error(&transaction_keys, method) };
509
510        // Skip submission if the tx is already processed via consensus output or
511        // checkpoint state sync.
512        let already_processed =
513            self.check_processed_via_consensus_or_checkpoint(&transaction_keys, epoch_store);
514        if let Some(method) = already_processed {
515            guard.processed_method = method;
516            if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
517                let _ = tx_consensus_positions.send(Err(make_processing_error(method)));
518            }
519        }
520
521        // Log warnings for administrative transactions that fail to get sequenced
522        let _monitor = if matches!(
523            transactions[0].kind,
524            ConsensusTransactionKind::EndOfPublish(_)
525                | ConsensusTransactionKind::CapabilityNotification(_)
526                | ConsensusTransactionKind::CapabilityNotificationV2(_)
527                | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
528                | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
529        ) {
530            assert!(
531                !is_soft_bundle,
532                "System transactions should have been submitted individually"
533            );
534            let transaction_keys = transaction_keys.clone();
535            Some(CancelOnDrop(spawn_monitored_task!(async {
536                let mut i = 0u64;
537                loop {
538                    i += 1;
539                    const WARN_DELAY_S: u64 = 30;
540                    tokio::time::sleep(Duration::from_secs(WARN_DELAY_S)).await;
541                    let total_wait = i * WARN_DELAY_S;
542                    warn!(
543                        "Still waiting {} seconds for transactions {:?} to commit in consensus",
544                        total_wait, transaction_keys
545                    );
546                }
547            })))
548        } else {
549            None
550        };
551
552        if already_processed.is_none() {
553            debug!("Submitting {:?} to consensus", transaction_keys);
554            guard.submitted = true;
555
556            // System messages (checkpoint signatures, EndOfPublish, capability
557            // notifications, randomness DKG, etc.) are not buffered behind user
558            // tx; they are excluded from the semaphore.
559            let _permit: Option<SemaphorePermit> = if is_system_message {
560                None
561            } else {
562                Some(
563                    self.submit_semaphore
564                        .acquire()
565                        .count_in_flight(self.metrics.sequencing_in_flight_semaphore_wait.clone())
566                        .await
567                        .expect("Consensus adapter does not close semaphore"),
568                )
569            };
570            let _in_flight_submission_guard =
571                GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
572
573            // Submit the transaction to consensus, racing against the processed waiter in
574            // case another validator sequences the transaction first.
575            let submit_fut = async {
576                const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
577
578                loop {
579                    // Submit the transaction to consensus and return the submit result with a status waiter
580                    let (consensus_positions, status_waiter) = self
581                        .submit_inner(&transactions, epoch_store, &transaction_keys, tx_type)
582                        .await;
583
584                    if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
585                        tracing::Span::current().record(
586                            "consensus_positions",
587                            tracing::field::debug(&consensus_positions),
588                        );
589                        // We send the first consensus position returned by consensus
590                        // to the submitting client even if it is retried internally within
591                        // consensus adapter due to an error or GC. They can handle retries
592                        // as needed if the consensus position does not return the desired
593                        // results (e.g. not sequenced due to garbage collection).
594                        let _ = tx_consensus_positions.send(Ok(consensus_positions.clone()));
595                    }
596
597                    match status_waiter.await {
598                        Ok(status @ BlockStatus::Sequenced(_)) => {
599                            tracing::Span::current()
600                                .record("status", tracing::field::debug(&status));
601                            self.metrics
602                                .sequencing_certificate_status
603                                .with_label_values(&[tx_type, "sequenced"])
604                                .inc();
605                            debug!(
606                                "Transaction {transaction_keys:?} has been sequenced by consensus."
607                            );
608                            if is_system_message {
609                                // System messages have consensus positions too, but the
610                                // commit handler only assigns per-position statuses to
611                                // user transactions, so their completion is signaled by
612                                // the processed flag instead.
613                                break SequencingOutcome::BlockSequenced;
614                            }
615                            if consensus_positions.len() != transactions.len() {
616                                debug_fatal!(
617                                    "Consensus client returned {} positions for {} transactions",
618                                    consensus_positions.len(),
619                                    transactions.len()
620                                );
621                                break SequencingOutcome::BlockSequenced;
622                            }
623                            // The block is committed, and the commit handler assigns every
624                            // user transaction position a terminal status.
625                            match self
626                                .wait_for_position_statuses(&consensus_positions, epoch_store)
627                                .await
628                            {
629                                Some(statuses) => break SequencingOutcome::Sequenced(statuses),
630                                None => {
631                                    // A position expired from the status cache before it
632                                    // was read: the block was committed and its commit
633                                    // processed more than the retention window ago, so a
634                                    // terminal status existed and was merely missed. End
635                                    // the submission instead of resubmitting — a missed
636                                    // Finalized outcome needs nothing further from this
637                                    // task (the digest is durably recorded as processed
638                                    // and will execute), the other outcomes are terminal,
639                                    // and transaction-level retries belong to the client.
640                                    debug!(
641                                        "Transaction {transaction_keys:?} status expired before being read. Ending submission."
642                                    );
643                                    self.metrics
644                                        .sequencing_certificate_status
645                                        .with_label_values(&[tx_type, "status_expired"])
646                                        .inc();
647                                    break SequencingOutcome::StatusExpired;
648                                }
649                            }
650                        }
651                        Ok(status @ BlockStatus::GarbageCollected(_)) => {
652                            tracing::Span::current()
653                                .record("status", tracing::field::debug(&status));
654                            self.metrics
655                                .sequencing_certificate_status
656                                .with_label_values(&[tx_type, "garbage_collected"])
657                                .inc();
658                            // Block has been garbage collected and we have no guarantees that the transaction will appear in consensus output. We'll
659                            // resubmit the transaction to consensus. If the transaction has been already "processed", then probably someone else has submitted
660                            // the transaction and managed to get sequenced. Then this future will have been cancelled anyways so no need to check here on the processed output.
661                            debug!(
662                                "Transaction {transaction_keys:?} was garbage collected before being sequenced. Will be retried."
663                            );
664                            time::sleep(RETRY_DELAY_STEP).await;
665                            continue;
666                        }
667                        Err(err) => {
668                            warn!(
669                                "Error while waiting for status from consensus for transactions {transaction_keys:?}, with error {:?}. Will be retried.",
670                                err
671                            );
672                            time::sleep(RETRY_DELAY_STEP).await;
673                            continue;
674                        }
675                    }
676                }
677            };
678
679            // Race `processed_notify` against the submit loop. If the tx is
680            // processed via another path (consensus output from another
681            // validator's submission, or checkpoint state sync) while we're
682            // inside the submit loop, the submission future is dropped and
683            // the retry loop is cancelled cleanly.
684            let processed_waiter = self
685                .processed_notify(transaction_keys.clone(), epoch_store)
686                .boxed();
687            let processed_via_notify;
688            guard.processed_method = match select(processed_waiter, submit_fut.boxed()).await {
689                Either::Left((observed, _submit_fut)) => {
690                    processed_via_notify = true;
691                    observed
692                }
693                Either::Right((SequencingOutcome::Sequenced(statuses), _processed_waiter)) => {
694                    processed_via_notify = false;
695                    for status in statuses {
696                        self.metrics
697                            .sequencing_certificate_settled_status
698                            .with_label_values(&[tx_type, status.metric_label()])
699                            .inc();
700                    }
701                    ProcessedMethod::ConsensusStatusReceived
702                }
703                Either::Right((SequencingOutcome::StatusExpired, _processed_waiter)) => {
704                    processed_via_notify = false;
705                    ProcessedMethod::ConsensusStatusExpired
706                }
707                Either::Right((SequencingOutcome::BlockSequenced, processed_waiter)) => {
708                    debug!("Submitted {transaction_keys:?} to consensus");
709                    processed_via_notify = false;
710                    processed_waiter.await
711                }
712            };
713            // If processing was observed before a position was sent to a waiting caller,
714            // report that the transaction is already processing so the caller returns a
715            // retriable error. If a position was already sent, the channel is taken and this
716            // is a no-op.
717            if processed_via_notify
718                && let Some(tx_consensus_positions) = tx_consensus_positions.take()
719            {
720                let _ =
721                    tx_consensus_positions.send(Err(make_processing_error(guard.processed_method)));
722            }
723        }
724        debug!(
725            "{transaction_keys:?} processed via {}",
726            guard.processed_method.method_name()
727        );
728
729        self.metrics
730            .sequencing_certificate_success
731            .with_label_values(&[tx_type])
732            .inc();
733    }
734
735    #[instrument(name = "ConsensusAdapter::submit_inner", level = "trace", skip_all)]
736    async fn submit_inner(
737        self: &Arc<Self>,
738        transactions: &[ConsensusTransaction],
739        epoch_store: &Arc<AuthorityPerEpochStore>,
740        transaction_keys: &[SequencedConsensusTransactionKey],
741        tx_type: &str,
742    ) -> (Vec<ConsensusPosition>, BlockStatusReceiver) {
743        let ack_start = Instant::now();
744        let mut retries: u32 = 0;
745        let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
746            Duration::from_millis(100),
747            Duration::from_secs(10),
748        );
749
750        let (consensus_positions, status_waiter) = loop {
751            let span = debug_span!("client_submit");
752            match self
753                .consensus_client
754                .submit(transactions, epoch_store)
755                .instrument(span)
756                .await
757            {
758                Err(err) => {
759                    // This can happen during reconfig, so keep retrying until succeed.
760                    if cfg!(msim) || retries > 3 {
761                        warn!(
762                            "Failed to submit transactions {transaction_keys:?} to consensus: {err}. Retry #{retries}"
763                        );
764                    }
765                    self.metrics
766                        .sequencing_certificate_failures
767                        .with_label_values(&[tx_type])
768                        .inc();
769                    retries += 1;
770
771                    time::sleep(backoff.next().unwrap()).await;
772                }
773                Ok((consensus_positions, status_waiter)) => {
774                    break (consensus_positions, status_waiter);
775                }
776            }
777        };
778
779        // we want to record the num of retries when reporting latency but to avoid label
780        // cardinality we do some simple bucketing to give us a good enough idea of how
781        // many retries happened associated with the latency.
782        let bucket = match retries {
783            0..=10 => retries.to_string(), // just report the retry count as is
784            11..=20 => "between_10_and_20".to_string(),
785            21..=50 => "between_20_and_50".to_string(),
786            51..=100 => "between_50_and_100".to_string(),
787            _ => "over_100".to_string(),
788        };
789
790        self.metrics
791            .sequencing_acknowledge_latency
792            .with_label_values(&[bucket.as_str(), tx_type])
793            .observe(ack_start.elapsed().as_secs_f64());
794
795        (consensus_positions, status_waiter)
796    }
797
798    /// Sync check for whether `transaction_keys` are already processed via
799    /// consensus output or checkpoint state sync. Returns `Some(method)` if
800    /// every key is already processed (Checkpoint dominates when any key was
801    /// processed via checkpoint or synced-checkpoint), else `None`.
802    ///
803    /// Also increments `sequencing_certificate_processed` with the matching
804    /// label for each key found processed, mirroring what `processed_notify`
805    /// emits for its async wake-ups.
806    fn check_processed_via_consensus_or_checkpoint(
807        self: &Arc<Self>,
808        transaction_keys: &[SequencedConsensusTransactionKey],
809        epoch_store: &Arc<AuthorityPerEpochStore>,
810    ) -> Option<ProcessedMethod> {
811        let mut seen_checkpoint = false;
812        for transaction_key in transaction_keys {
813            // Check consensus-processed first; if already visible in consensus
814            // output we don't need to submit again.
815            if epoch_store
816                .is_consensus_message_processed(transaction_key)
817                .expect("Storage error when checking consensus message processed")
818            {
819                self.metrics
820                    .sequencing_certificate_processed
821                    .with_label_values(&["consensus"])
822                    .inc();
823                continue;
824            }
825
826            // For a cert-shaped key, check whether state sync executed the tx
827            // via a checkpoint.
828            if let SequencedConsensusTransactionKey::External(ConsensusTransactionKey::Certificate(
829                digest,
830            )) = transaction_key
831                && epoch_store
832                    .is_transaction_executed_in_checkpoint(digest)
833                    .expect("Storage error when checking transaction executed in checkpoint")
834            {
835                self.metrics
836                    .sequencing_certificate_processed
837                    .with_label_values(&["checkpoint"])
838                    .inc();
839                seen_checkpoint = true;
840                continue;
841            }
842
843            // For a checkpoint-signature key, check whether a checkpoint at
844            // or above the target sequence number has already been synced —
845            // in which case the signature is redundant.
846            if let SequencedConsensusTransactionKey::External(
847                ConsensusTransactionKey::CheckpointSignature(_, seq)
848                | ConsensusTransactionKey::CheckpointSignatureV2(_, seq, _),
849            ) = transaction_key
850                && let Some(synced_seq) = self
851                    .checkpoint_store
852                    .get_highest_synced_checkpoint_seq_number()
853                    .expect("Storage error when reading highest synced checkpoint")
854                && synced_seq >= *seq
855            {
856                self.metrics
857                    .sequencing_certificate_processed
858                    .with_label_values(&["synced_checkpoint"])
859                    .inc();
860                seen_checkpoint = true;
861                continue;
862            }
863
864            // Not processed via any path — caller must submit.
865            return None;
866        }
867
868        if seen_checkpoint {
869            Some(ProcessedMethod::CheckpointExecuted)
870        } else {
871            Some(ProcessedMethod::ConsensusMessageProcessed)
872        }
873    }
874
875    /// Async wait for any of `transaction_keys` to become processed via
876    /// consensus output or a checkpoint (either state-synced or executed
877    /// locally). Used in the in-flight race against submission: cancelling
878    /// the submit future when we learn the tx is processed by another path.
879    /// Returns `Checkpoint` if any key resolves via a checkpoint path, else
880    /// `Consensus`.
881    async fn processed_notify(
882        self: &Arc<Self>,
883        transaction_keys: Vec<SequencedConsensusTransactionKey>,
884        epoch_store: &Arc<AuthorityPerEpochStore>,
885    ) -> ProcessedMethod {
886        let notifications = FuturesUnordered::new();
887        for transaction_key in transaction_keys {
888            let transaction_digests = match transaction_key {
889                SequencedConsensusTransactionKey::External(
890                    ConsensusTransactionKey::Certificate(digest),
891                ) => vec![digest],
892                _ => vec![],
893            };
894
895            let checkpoint_synced_future = if let SequencedConsensusTransactionKey::External(
896                ConsensusTransactionKey::CheckpointSignature(_, checkpoint_sequence_number)
897                | ConsensusTransactionKey::CheckpointSignatureV2(_, checkpoint_sequence_number, _),
898            ) = transaction_key
899            {
900                // If the transaction is a checkpoint signature, we can also wait to get notified when a checkpoint with equal or higher sequence
901                // number has been already synced. This way we don't try to unnecessarily sequence the signature for an already verified checkpoint.
902                Either::Left(
903                    self.checkpoint_store
904                        .notify_read_synced_checkpoint(checkpoint_sequence_number),
905                )
906            } else {
907                Either::Right(future::pending())
908            };
909
910            // Wait for each key individually so soft bundles can complete even
911            // when different transactions are observed through different paths.
912            notifications.push(async move {
913                tokio::select! {
914                    _ = epoch_store.consensus_messages_processed_notify(vec![transaction_key]) => {
915                        self.metrics.sequencing_certificate_processed.with_label_values(&["consensus"]).inc();
916                        return ProcessedMethod::ConsensusMessageProcessed;
917                    },
918                    _ = epoch_store.transactions_executed_in_checkpoint_notify(transaction_digests), if !transaction_digests.is_empty() => {
919                        self.metrics.sequencing_certificate_processed.with_label_values(&["checkpoint"]).inc();
920                    }
921                    _ = checkpoint_synced_future => {
922                        self.metrics.sequencing_certificate_processed.with_label_values(&["synced_checkpoint"]).inc();
923                    }
924                }
925                ProcessedMethod::CheckpointExecuted
926            });
927        }
928
929        let processed_methods = notifications.collect::<Vec<ProcessedMethod>>().await;
930        for method in processed_methods {
931            if method == ProcessedMethod::CheckpointExecuted {
932                return ProcessedMethod::CheckpointExecuted;
933            }
934        }
935        ProcessedMethod::ConsensusMessageProcessed
936    }
937
938    /// Waits until every consensus position reaches a terminal status (Finalized, Rejected or Dropped).
939    /// Returns `None` if any position expired from the status cache before its status was read,
940    /// in which case the submit loop treats the sequenced block as already handled and settles with
941    /// `StatusExpired`.
942    async fn wait_for_position_statuses(
943        &self,
944        consensus_positions: &[ConsensusPosition],
945        epoch_store: &Arc<AuthorityPerEpochStore>,
946    ) -> Option<Vec<ConsensusTxStatus>> {
947        join_all(consensus_positions.iter().map(|position| {
948            epoch_store
949                .consensus_tx_status_cache
950                .notify_read_transaction_status(*position)
951        }))
952        .await
953        .into_iter()
954        .map(|result| match result {
955            NotifyReadConsensusTxStatusResult::Status(status) => Some(status),
956            NotifyReadConsensusTxStatusResult::Expired(_) => None,
957        })
958        .collect()
959    }
960}
961
962impl ConsensusOverloadChecker for ConsensusAdapter {
963    fn check_consensus_overload(&self) -> SuiResult {
964        fp_ensure!(
965            self.check_limits(),
966            SuiErrorKind::TooManyTransactionsPendingConsensus.into()
967        );
968        Ok(())
969    }
970}
971
972pub struct NoopConsensusOverloadChecker {}
973
974impl ConsensusOverloadChecker for NoopConsensusOverloadChecker {
975    fn check_consensus_overload(&self) -> SuiResult {
976        Ok(())
977    }
978}
979
980impl ReconfigurationInitiator for Arc<ConsensusAdapter> {
981    /// This method is called externally to begin reconfiguration.
982    /// It persists a reconfig state that rejects new user transactions,
983    /// then immediately submits an EndOfPublish message to consensus.
984    fn close_epoch(&self, epoch_store: &Arc<AuthorityPerEpochStore>) {
985        {
986            let reconfig_guard = epoch_store.get_reconfig_state_write_lock_guard();
987            if !reconfig_guard.should_accept_user_certs() {
988                // Allow caller to call this method multiple times
989                return;
990            }
991            epoch_store.close_user_certs_for_manual_epoch_close(reconfig_guard);
992        }
993        if epoch_store.should_send_end_of_publish() {
994            if let Err(err) = self.submit(
995                ConsensusTransaction::new_end_of_publish(self.authority),
996                None,
997                epoch_store,
998                None,
999                None,
1000            ) {
1001                warn!("Error when sending end of publish message: {:?}", err);
1002            } else {
1003                info!(epoch=?epoch_store.epoch(), "Sending EndOfPublish message to consensus");
1004            }
1005        }
1006    }
1007}
1008
1009impl SubmitToConsensus for Arc<ConsensusAdapter> {
1010    fn submit_to_consensus(
1011        &self,
1012        transactions: &[ConsensusTransaction],
1013        epoch_store: &Arc<AuthorityPerEpochStore>,
1014    ) -> SuiResult {
1015        self.submit_batch(transactions, None, epoch_store, None, None)
1016            .map(|_| ())
1017    }
1018
1019    fn submit_best_effort(
1020        &self,
1021        transaction: &ConsensusTransaction,
1022        epoch_store: &Arc<AuthorityPerEpochStore>,
1023        // timeout is required, or the spawned task can run forever
1024        timeout: Duration,
1025    ) -> SuiResult {
1026        if transaction.is_user_transaction() {
1027            debug_fatal!("submit_best_effort called with a user transaction");
1028            return Err(SuiErrorKind::GenericAuthorityError {
1029                error: "submit_best_effort does not accept user transactions".to_string(),
1030            }
1031            .into());
1032        }
1033
1034        // There is no submit semaphone on this path as it services system msgs only.
1035        let _in_flight_submission_guard =
1036            GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
1037
1038        let key = SequencedConsensusTransactionKey::External(transaction.key());
1039        let tx_type = classify(transaction);
1040
1041        let async_stage = {
1042            let transaction = transaction.clone();
1043            let epoch_store = epoch_store.clone();
1044            let this = self.clone();
1045
1046            async move {
1047                let result = tokio::time::timeout(
1048                    timeout,
1049                    this.submit_inner(&[transaction], &epoch_store, &[key], tx_type),
1050                )
1051                .await;
1052
1053                if let Err(e) = result {
1054                    warn!("Consensus submission timed out: {e:?}");
1055                    this.metrics
1056                        .sequencing_best_effort_timeout
1057                        .with_label_values(&[tx_type])
1058                        .inc();
1059                }
1060            }
1061        };
1062
1063        let epoch_store = epoch_store.clone();
1064        spawn_monitored_task!(epoch_store.within_alive_epoch(async_stage));
1065        Ok(())
1066    }
1067}
1068
1069struct CancelOnDrop<T>(JoinHandle<T>);
1070
1071impl<T> Deref for CancelOnDrop<T> {
1072    type Target = JoinHandle<T>;
1073
1074    fn deref(&self) -> &Self::Target {
1075        &self.0
1076    }
1077}
1078
1079impl<T> Drop for CancelOnDrop<T> {
1080    fn drop(&mut self) {
1081        self.0.abort();
1082    }
1083}
1084
1085/// Tracks number of inflight consensus requests and relevant metrics
1086struct InflightDropGuard<'a> {
1087    adapter: &'a ConsensusAdapter,
1088    start: Instant,
1089    submitted: bool,
1090    tx_type: &'static str,
1091    /// See `proposers_metric_label`.
1092    proposers: &'static str,
1093    processed_method: ProcessedMethod,
1094    /// Number of transactions this guard accounts for.
1095    /// > 1 for soft bundles.
1096    inflight_count: u64,
1097}
1098
1099impl<'a> InflightDropGuard<'a> {
1100    pub fn acquire(
1101        adapter: &'a ConsensusAdapter,
1102        tx_type: &'static str,
1103        proposers: &'static str,
1104        inflight_count: u64,
1105    ) -> Self {
1106        adapter
1107            .num_inflight_transactions
1108            .fetch_add(inflight_count, Ordering::SeqCst);
1109        adapter
1110            .metrics
1111            .sequencing_certificate_inflight
1112            .with_label_values(&[tx_type])
1113            .inc();
1114        adapter
1115            .metrics
1116            .sequencing_certificate_attempt
1117            .with_label_values(&[tx_type])
1118            .inc();
1119        Self {
1120            adapter,
1121            start: Instant::now(),
1122            submitted: false,
1123            tx_type,
1124            proposers,
1125            processed_method: ProcessedMethod::ConsensusMessageProcessed,
1126            inflight_count,
1127        }
1128    }
1129}
1130
1131impl Drop for InflightDropGuard<'_> {
1132    fn drop(&mut self) {
1133        self.adapter
1134            .num_inflight_transactions
1135            .fetch_sub(self.inflight_count, Ordering::SeqCst);
1136        self.adapter
1137            .metrics
1138            .sequencing_certificate_inflight
1139            .with_label_values(&[self.tx_type])
1140            .dec();
1141        // Wake the admission queue drainer so it can submit more transactions.
1142        self.adapter.inflight_slot_freed_notify.notify_one();
1143
1144        let latency = self.start.elapsed();
1145        let submitted = if self.submitted {
1146            "submitted"
1147        } else {
1148            "skipped"
1149        };
1150
1151        self.adapter
1152            .metrics
1153            .sequencing_certificate_latency
1154            .with_label_values(&[
1155                submitted,
1156                self.tx_type,
1157                self.processed_method.metric_label(),
1158                self.proposers,
1159            ])
1160            .observe(latency.as_secs_f64());
1161    }
1162}
1163
1164/// The error reported to a position-waiting caller (mfp) when the transaction is
1165/// already being processed and (re)submission is therefore skipped. The caller
1166/// surfaces this as a retriable error so the client waits for effects / retries
1167/// instead of receiving a meaningless consensus position. Shared with the pull-based
1168/// transaction pool, which skips the same submissions at proposal time.
1169pub(crate) fn processing_error<'a>(
1170    keys: impl IntoIterator<Item = &'a SequencedConsensusTransactionKey>,
1171    method: ProcessedMethod,
1172) -> SuiError {
1173    let digest = keys
1174        .into_iter()
1175        .find_map(SequencedConsensusTransactionKey::user_transaction_digest)
1176        .unwrap_or_default();
1177    SuiErrorKind::TransactionProcessing {
1178        digest,
1179        status: format!("processed via {}", method.method_name()),
1180    }
1181    .into()
1182}
1183
1184/// Variant order is reporting priority: when a group is processed through several
1185/// paths, the greatest variant is reported, so checkpoint execution dominates.
1186#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1187pub(crate) enum ProcessedMethod {
1188    ConsensusMessageProcessed,
1189    ConsensusStatusReceived,
1190    ConsensusStatusExpired,
1191    CheckpointExecuted,
1192}
1193
1194impl ProcessedMethod {
1195    fn method_name(self) -> &'static str {
1196        match self {
1197            ProcessedMethod::ConsensusMessageProcessed => "consensus (processed message)",
1198            ProcessedMethod::ConsensusStatusReceived => "consensus (transaction status)",
1199            ProcessedMethod::ConsensusStatusExpired => "consensus (status expired)",
1200            ProcessedMethod::CheckpointExecuted => "checkpoint execution",
1201        }
1202    }
1203
1204    pub(crate) fn metric_label(self) -> &'static str {
1205        match self {
1206            ProcessedMethod::ConsensusMessageProcessed => "consensus_message",
1207            ProcessedMethod::ConsensusStatusReceived => "consensus_status",
1208            ProcessedMethod::ConsensusStatusExpired => "consensus_status_expired",
1209            ProcessedMethod::CheckpointExecuted => "checkpoint_execution",
1210        }
1211    }
1212}
1213
1214/// Outcome of the submit loop in `submit_and_wait_inner`.
1215enum SequencingOutcome {
1216    /// A user-transaction submission that was sequenced and whose positions all
1217    /// reached a terminal consensus status; nothing further to wait for.
1218    Sequenced(Vec<ConsensusTxStatus>),
1219    /// A system-message submission whose block was sequenced.
1220    BlockSequenced,
1221    /// A user-transaction submission whose block was sequenced, but at least one
1222    /// position expired from the status cache before its status was read. The
1223    /// terminal outcome existed and was missed; nothing further to wait for.
1224    StatusExpired,
1225}