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