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 sui_types::transaction::TransactionDataAPI;
40use tokio::sync::{Notify, Semaphore, SemaphorePermit, oneshot};
41use tokio::task::JoinHandle;
42use tokio::time::Duration;
43use tokio::time::{self};
44use tracing::{Instrument, debug, debug_span, info, instrument, warn};
45
46use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
47use crate::authority::consensus_tx_status_cache::{
48    ConsensusTxStatus, NotifyReadConsensusTxStatusResult,
49};
50use crate::checkpoints::CheckpointStore;
51use crate::consensus_handler::{SequencedConsensusTransactionKey, classify};
52use crate::epoch::reconfiguration::{ReconfigState, ReconfigurationInitiator};
53
54#[cfg(test)]
55#[path = "unit_tests/consensus_tests.rs"]
56pub mod consensus_tests;
57
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        for transaction in &transactions {
475            if let Some(tx) = transaction.kind.as_user_transaction() {
476                let amplification_factor = (tx.data().transaction_data().gas_price()
477                    / epoch_store.reference_gas_price().max(1))
478                .max(1);
479                epoch_store.submitted_transaction_cache.record_submitted_tx(
480                    tx.digest(),
481                    amplification_factor as u32,
482                    submitter_client_addr,
483                );
484            }
485        }
486
487        // Current code path ensures:
488        // - If transactions.len() > 1, it is a soft bundle. System transactions should have been submitted individually.
489        // - If is_soft_bundle, then all transactions are of CertifiedTransaction or UserTransaction kind.
490        // - If not is_soft_bundle, then transactions must contain exactly 1 tx, and transactions[0] can be of any kind.
491        let is_soft_bundle = transactions.len() > 1;
492        let is_system_message = !transactions[0].is_user_transaction();
493
494        let mut transaction_keys = Vec::new();
495        let mut tx_consensus_positions = tx_consensus_positions;
496
497        for transaction in &transactions {
498            if matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..)) {
499                info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
500                epoch_store.record_epoch_pending_certs_process_time_metric();
501            }
502
503            let transaction_key = SequencedConsensusTransactionKey::External(transaction.key());
504            transaction_keys.push(transaction_key);
505        }
506        let tx_type = if is_soft_bundle {
507            "soft_bundle"
508        } else {
509            classify(&transactions[0])
510        };
511        tracing::Span::current().record("tx_type", tx_type);
512        tracing::Span::current().record("tx_keys", tracing::field::debug(&transaction_keys));
513
514        let mut guard = InflightDropGuard::acquire(&self, tx_type, transactions.len() as u64);
515
516        // Builds the error reported to a position-waiting caller (mfp) when the
517        // transaction is already being processed and we therefore skip (re)submission.
518        // The caller surfaces this as a retriable error so the client waits for
519        // effects / retries instead of receiving a meaningless consensus position.
520        let make_processing_error = |method: ProcessedMethod| -> SuiError {
521            let digest = transactions
522                .iter()
523                .find_map(|t| t.kind.as_user_transaction().map(|tx| *tx.digest()))
524                .unwrap_or_default();
525            SuiErrorKind::TransactionProcessing {
526                digest,
527                status: format!("processed via {}", method.method_name()),
528            }
529            .into()
530        };
531
532        // Skip submission if the tx is already processed via consensus output or
533        // checkpoint state sync.
534        let already_processed =
535            self.check_processed_via_consensus_or_checkpoint(&transaction_keys, epoch_store);
536        if let Some(method) = already_processed {
537            guard.processed_method = method;
538            if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
539                let _ = tx_consensus_positions.send(Err(make_processing_error(method)));
540            }
541        }
542
543        // Log warnings for administrative transactions that fail to get sequenced
544        let _monitor = if matches!(
545            transactions[0].kind,
546            ConsensusTransactionKind::EndOfPublish(_)
547                | ConsensusTransactionKind::CapabilityNotification(_)
548                | ConsensusTransactionKind::CapabilityNotificationV2(_)
549                | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
550                | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
551        ) {
552            assert!(
553                !is_soft_bundle,
554                "System transactions should have been submitted individually"
555            );
556            let transaction_keys = transaction_keys.clone();
557            Some(CancelOnDrop(spawn_monitored_task!(async {
558                let mut i = 0u64;
559                loop {
560                    i += 1;
561                    const WARN_DELAY_S: u64 = 30;
562                    tokio::time::sleep(Duration::from_secs(WARN_DELAY_S)).await;
563                    let total_wait = i * WARN_DELAY_S;
564                    warn!(
565                        "Still waiting {} seconds for transactions {:?} to commit in consensus",
566                        total_wait, transaction_keys
567                    );
568                }
569            })))
570        } else {
571            None
572        };
573
574        if already_processed.is_none() {
575            debug!("Submitting {:?} to consensus", transaction_keys);
576            guard.submitted = true;
577
578            // System messages (checkpoint signatures, EndOfPublish, capability
579            // notifications, randomness DKG, etc.) are not buffered behind user
580            // tx; they are excluded from the semaphore.
581            let _permit: Option<SemaphorePermit> = if is_system_message {
582                None
583            } else {
584                Some(
585                    self.submit_semaphore
586                        .acquire()
587                        .count_in_flight(self.metrics.sequencing_in_flight_semaphore_wait.clone())
588                        .await
589                        .expect("Consensus adapter does not close semaphore"),
590                )
591            };
592            let _in_flight_submission_guard =
593                GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
594
595            // Submit the transaction to consensus, racing against the processed waiter in
596            // case another validator sequences the transaction first.
597            let submit_fut = async {
598                const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
599
600                loop {
601                    // Submit the transaction to consensus and return the submit result with a status waiter
602                    let (consensus_positions, status_waiter) = self
603                        .submit_inner(&transactions, epoch_store, &transaction_keys, tx_type)
604                        .await;
605
606                    if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
607                        tracing::Span::current().record(
608                            "consensus_positions",
609                            tracing::field::debug(&consensus_positions),
610                        );
611                        // We send the first consensus position returned by consensus
612                        // to the submitting client even if it is retried internally within
613                        // consensus adapter due to an error or GC. They can handle retries
614                        // as needed if the consensus position does not return the desired
615                        // results (e.g. not sequenced due to garbage collection).
616                        let _ = tx_consensus_positions.send(Ok(consensus_positions.clone()));
617                    }
618
619                    match status_waiter.await {
620                        Ok(status @ BlockStatus::Sequenced(_)) => {
621                            tracing::Span::current()
622                                .record("status", tracing::field::debug(&status));
623                            self.metrics
624                                .sequencing_certificate_status
625                                .with_label_values(&[tx_type, "sequenced"])
626                                .inc();
627                            debug!(
628                                "Transaction {transaction_keys:?} has been sequenced by consensus."
629                            );
630                            if is_system_message {
631                                // System messages have consensus positions too, but the
632                                // commit handler only assigns per-position statuses to
633                                // user transactions, so their completion is signaled by
634                                // the processed flag instead.
635                                break SequencingOutcome::BlockSequenced;
636                            }
637                            if consensus_positions.len() != transactions.len() {
638                                debug_fatal!(
639                                    "Consensus client returned {} positions for {} transactions",
640                                    consensus_positions.len(),
641                                    transactions.len()
642                                );
643                                break SequencingOutcome::BlockSequenced;
644                            }
645                            // The block is committed, and the commit handler assigns every
646                            // user transaction position a terminal status.
647                            match self
648                                .wait_for_position_statuses(&consensus_positions, epoch_store)
649                                .await
650                            {
651                                Some(statuses) => break SequencingOutcome::Sequenced(statuses),
652                                None => {
653                                    // A position expired from the status cache before it
654                                    // was read: the block was committed and its commit
655                                    // processed more than the retention window ago, so a
656                                    // terminal status existed and was merely missed. End
657                                    // the submission instead of resubmitting — a missed
658                                    // Finalized outcome needs nothing further from this
659                                    // task (the digest is durably recorded as processed
660                                    // and will execute), the other outcomes are terminal,
661                                    // and transaction-level retries belong to the client.
662                                    debug!(
663                                        "Transaction {transaction_keys:?} status expired before being read. Ending submission."
664                                    );
665                                    self.metrics
666                                        .sequencing_certificate_status
667                                        .with_label_values(&[tx_type, "status_expired"])
668                                        .inc();
669                                    break SequencingOutcome::StatusExpired;
670                                }
671                            }
672                        }
673                        Ok(status @ BlockStatus::GarbageCollected(_)) => {
674                            tracing::Span::current()
675                                .record("status", tracing::field::debug(&status));
676                            self.metrics
677                                .sequencing_certificate_status
678                                .with_label_values(&[tx_type, "garbage_collected"])
679                                .inc();
680                            // Block has been garbage collected and we have no guarantees that the transaction will appear in consensus output. We'll
681                            // resubmit the transaction to consensus. If the transaction has been already "processed", then probably someone else has submitted
682                            // 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.
683                            debug!(
684                                "Transaction {transaction_keys:?} was garbage collected before being sequenced. Will be retried."
685                            );
686                            time::sleep(RETRY_DELAY_STEP).await;
687                            continue;
688                        }
689                        Err(err) => {
690                            warn!(
691                                "Error while waiting for status from consensus for transactions {transaction_keys:?}, with error {:?}. Will be retried.",
692                                err
693                            );
694                            time::sleep(RETRY_DELAY_STEP).await;
695                            continue;
696                        }
697                    }
698                }
699            };
700
701            // Race `processed_notify` against the submit loop. If the tx is
702            // processed via another path (consensus output from another
703            // validator's submission, or checkpoint state sync) while we're
704            // inside the submit loop, the submission future is dropped and
705            // the retry loop is cancelled cleanly.
706            let processed_waiter = self
707                .processed_notify(transaction_keys.clone(), epoch_store)
708                .boxed();
709            let processed_via_notify;
710            guard.processed_method = match select(processed_waiter, submit_fut.boxed()).await {
711                Either::Left((observed, _submit_fut)) => {
712                    processed_via_notify = true;
713                    observed
714                }
715                Either::Right((SequencingOutcome::Sequenced(statuses), _processed_waiter)) => {
716                    processed_via_notify = false;
717                    for status in statuses {
718                        self.metrics
719                            .sequencing_certificate_settled_status
720                            .with_label_values(&[tx_type, status_label(status)])
721                            .inc();
722                    }
723                    ProcessedMethod::ConsensusStatusReceived
724                }
725                Either::Right((SequencingOutcome::StatusExpired, _processed_waiter)) => {
726                    processed_via_notify = false;
727                    ProcessedMethod::ConsensusStatusExpired
728                }
729                Either::Right((SequencingOutcome::BlockSequenced, processed_waiter)) => {
730                    debug!("Submitted {transaction_keys:?} to consensus");
731                    processed_via_notify = false;
732                    processed_waiter.await
733                }
734            };
735            // If processing was observed before a position was sent to a waiting caller,
736            // report that the transaction is already processing so the caller returns a
737            // retriable error. If a position was already sent, the channel is taken and this
738            // is a no-op.
739            if processed_via_notify
740                && let Some(tx_consensus_positions) = tx_consensus_positions.take()
741            {
742                let _ =
743                    tx_consensus_positions.send(Err(make_processing_error(guard.processed_method)));
744            }
745        }
746        debug!(
747            "{transaction_keys:?} processed via {}",
748            guard.processed_method.method_name()
749        );
750
751        self.metrics
752            .sequencing_certificate_success
753            .with_label_values(&[tx_type])
754            .inc();
755    }
756
757    #[instrument(name = "ConsensusAdapter::submit_inner", level = "trace", skip_all)]
758    async fn submit_inner(
759        self: &Arc<Self>,
760        transactions: &[ConsensusTransaction],
761        epoch_store: &Arc<AuthorityPerEpochStore>,
762        transaction_keys: &[SequencedConsensusTransactionKey],
763        tx_type: &str,
764    ) -> (Vec<ConsensusPosition>, BlockStatusReceiver) {
765        let ack_start = Instant::now();
766        let mut retries: u32 = 0;
767        let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
768            Duration::from_millis(100),
769            Duration::from_secs(10),
770        );
771
772        let (consensus_positions, status_waiter) = loop {
773            let span = debug_span!("client_submit");
774            match self
775                .consensus_client
776                .submit(transactions, epoch_store)
777                .instrument(span)
778                .await
779            {
780                Err(err) => {
781                    // This can happen during reconfig, so keep retrying until succeed.
782                    if cfg!(msim) || retries > 3 {
783                        warn!(
784                            "Failed to submit transactions {transaction_keys:?} to consensus: {err}. Retry #{retries}"
785                        );
786                    }
787                    self.metrics
788                        .sequencing_certificate_failures
789                        .with_label_values(&[tx_type])
790                        .inc();
791                    retries += 1;
792
793                    time::sleep(backoff.next().unwrap()).await;
794                }
795                Ok((consensus_positions, status_waiter)) => {
796                    break (consensus_positions, status_waiter);
797                }
798            }
799        };
800
801        // we want to record the num of retries when reporting latency but to avoid label
802        // cardinality we do some simple bucketing to give us a good enough idea of how
803        // many retries happened associated with the latency.
804        let bucket = match retries {
805            0..=10 => retries.to_string(), // just report the retry count as is
806            11..=20 => "between_10_and_20".to_string(),
807            21..=50 => "between_20_and_50".to_string(),
808            51..=100 => "between_50_and_100".to_string(),
809            _ => "over_100".to_string(),
810        };
811
812        self.metrics
813            .sequencing_acknowledge_latency
814            .with_label_values(&[bucket.as_str(), tx_type])
815            .observe(ack_start.elapsed().as_secs_f64());
816
817        (consensus_positions, status_waiter)
818    }
819
820    /// Sync check for whether `transaction_keys` are already processed via
821    /// consensus output or checkpoint state sync. Returns `Some(method)` if
822    /// every key is already processed (Checkpoint dominates when any key was
823    /// processed via checkpoint or synced-checkpoint), else `None`.
824    ///
825    /// Also increments `sequencing_certificate_processed` with the matching
826    /// label for each key found processed, mirroring what `processed_notify`
827    /// emits for its async wake-ups.
828    fn check_processed_via_consensus_or_checkpoint(
829        self: &Arc<Self>,
830        transaction_keys: &[SequencedConsensusTransactionKey],
831        epoch_store: &Arc<AuthorityPerEpochStore>,
832    ) -> Option<ProcessedMethod> {
833        let mut seen_checkpoint = false;
834        for transaction_key in transaction_keys {
835            // Check consensus-processed first; if already visible in consensus
836            // output we don't need to submit again.
837            if epoch_store
838                .is_consensus_message_processed(transaction_key)
839                .expect("Storage error when checking consensus message processed")
840            {
841                self.metrics
842                    .sequencing_certificate_processed
843                    .with_label_values(&["consensus"])
844                    .inc();
845                continue;
846            }
847
848            // For a cert-shaped key, check whether state sync executed the tx
849            // via a checkpoint.
850            if let SequencedConsensusTransactionKey::External(ConsensusTransactionKey::Certificate(
851                digest,
852            )) = transaction_key
853                && epoch_store
854                    .is_transaction_executed_in_checkpoint(digest)
855                    .expect("Storage error when checking transaction executed in checkpoint")
856            {
857                self.metrics
858                    .sequencing_certificate_processed
859                    .with_label_values(&["checkpoint"])
860                    .inc();
861                seen_checkpoint = true;
862                continue;
863            }
864
865            // For a checkpoint-signature key, check whether a checkpoint at
866            // or above the target sequence number has already been synced —
867            // in which case the signature is redundant.
868            if let SequencedConsensusTransactionKey::External(
869                ConsensusTransactionKey::CheckpointSignature(_, seq)
870                | ConsensusTransactionKey::CheckpointSignatureV2(_, seq, _),
871            ) = transaction_key
872                && let Some(synced_seq) = self
873                    .checkpoint_store
874                    .get_highest_synced_checkpoint_seq_number()
875                    .expect("Storage error when reading highest synced checkpoint")
876                && synced_seq >= *seq
877            {
878                self.metrics
879                    .sequencing_certificate_processed
880                    .with_label_values(&["synced_checkpoint"])
881                    .inc();
882                seen_checkpoint = true;
883                continue;
884            }
885
886            // Not processed via any path — caller must submit.
887            return None;
888        }
889
890        if seen_checkpoint {
891            Some(ProcessedMethod::CheckpointExecuted)
892        } else {
893            Some(ProcessedMethod::ConsensusMessageProcessed)
894        }
895    }
896
897    /// Async wait for any of `transaction_keys` to become processed via
898    /// consensus output or a checkpoint (either state-synced or executed
899    /// locally). Used in the in-flight race against submission: cancelling
900    /// the submit future when we learn the tx is processed by another path.
901    /// Returns `Checkpoint` if any key resolves via a checkpoint path, else
902    /// `Consensus`.
903    async fn processed_notify(
904        self: &Arc<Self>,
905        transaction_keys: Vec<SequencedConsensusTransactionKey>,
906        epoch_store: &Arc<AuthorityPerEpochStore>,
907    ) -> ProcessedMethod {
908        let notifications = FuturesUnordered::new();
909        for transaction_key in transaction_keys {
910            let transaction_digests = match transaction_key {
911                SequencedConsensusTransactionKey::External(
912                    ConsensusTransactionKey::Certificate(digest),
913                ) => vec![digest],
914                _ => vec![],
915            };
916
917            let checkpoint_synced_future = if let SequencedConsensusTransactionKey::External(
918                ConsensusTransactionKey::CheckpointSignature(_, checkpoint_sequence_number)
919                | ConsensusTransactionKey::CheckpointSignatureV2(_, checkpoint_sequence_number, _),
920            ) = transaction_key
921            {
922                // If the transaction is a checkpoint signature, we can also wait to get notified when a checkpoint with equal or higher sequence
923                // number has been already synced. This way we don't try to unnecessarily sequence the signature for an already verified checkpoint.
924                Either::Left(
925                    self.checkpoint_store
926                        .notify_read_synced_checkpoint(checkpoint_sequence_number),
927                )
928            } else {
929                Either::Right(future::pending())
930            };
931
932            // Wait for each key individually so soft bundles can complete even
933            // when different transactions are observed through different paths.
934            notifications.push(async move {
935                tokio::select! {
936                    processed = epoch_store.consensus_messages_processed_notify(vec![transaction_key]) => {
937                        processed.expect("Storage error when waiting for consensus message processed");
938                        self.metrics.sequencing_certificate_processed.with_label_values(&["consensus"]).inc();
939                        return ProcessedMethod::ConsensusMessageProcessed;
940                    },
941                    processed = epoch_store.transactions_executed_in_checkpoint_notify(transaction_digests), if !transaction_digests.is_empty() => {
942                        processed.expect("Storage error when waiting for transaction executed in checkpoint");
943                        self.metrics.sequencing_certificate_processed.with_label_values(&["checkpoint"]).inc();
944                    }
945                    _ = checkpoint_synced_future => {
946                        self.metrics.sequencing_certificate_processed.with_label_values(&["synced_checkpoint"]).inc();
947                    }
948                }
949                ProcessedMethod::CheckpointExecuted
950            });
951        }
952
953        let processed_methods = notifications.collect::<Vec<ProcessedMethod>>().await;
954        for method in processed_methods {
955            if method == ProcessedMethod::CheckpointExecuted {
956                return ProcessedMethod::CheckpointExecuted;
957            }
958        }
959        ProcessedMethod::ConsensusMessageProcessed
960    }
961
962    /// Waits until every consensus position reaches a terminal status (Finalized, Rejected or Dropped).
963    /// Returns `None` if any position expired from the status cache before its status was read,
964    /// in which case the submit loop treats the sequenced block as already handled and settles with
965    /// `StatusExpired`.
966    async fn wait_for_position_statuses(
967        &self,
968        consensus_positions: &[ConsensusPosition],
969        epoch_store: &Arc<AuthorityPerEpochStore>,
970    ) -> Option<Vec<ConsensusTxStatus>> {
971        join_all(consensus_positions.iter().map(|position| {
972            epoch_store
973                .consensus_tx_status_cache
974                .notify_read_transaction_status(*position)
975        }))
976        .await
977        .into_iter()
978        .map(|result| match result {
979            NotifyReadConsensusTxStatusResult::Status(status) => Some(status),
980            NotifyReadConsensusTxStatusResult::Expired(_) => None,
981        })
982        .collect()
983    }
984}
985
986impl ConsensusOverloadChecker for ConsensusAdapter {
987    fn check_consensus_overload(&self) -> SuiResult {
988        fp_ensure!(
989            self.check_limits(),
990            SuiErrorKind::TooManyTransactionsPendingConsensus.into()
991        );
992        Ok(())
993    }
994}
995
996pub struct NoopConsensusOverloadChecker {}
997
998impl ConsensusOverloadChecker for NoopConsensusOverloadChecker {
999    fn check_consensus_overload(&self) -> SuiResult {
1000        Ok(())
1001    }
1002}
1003
1004impl ReconfigurationInitiator for Arc<ConsensusAdapter> {
1005    /// This method is called externally to begin reconfiguration.
1006    /// It persists a reconfig state that rejects new user transactions,
1007    /// then immediately submits an EndOfPublish message to consensus.
1008    fn close_epoch(&self, epoch_store: &Arc<AuthorityPerEpochStore>) {
1009        {
1010            let reconfig_guard = epoch_store.get_reconfig_state_write_lock_guard();
1011            if !reconfig_guard.should_accept_user_certs() {
1012                // Allow caller to call this method multiple times
1013                return;
1014            }
1015            epoch_store.close_user_certs_for_manual_epoch_close(reconfig_guard);
1016        }
1017        if epoch_store.should_send_end_of_publish() {
1018            if let Err(err) = self.submit(
1019                ConsensusTransaction::new_end_of_publish(self.authority),
1020                None,
1021                epoch_store,
1022                None,
1023                None,
1024            ) {
1025                warn!("Error when sending end of publish message: {:?}", err);
1026            } else {
1027                info!(epoch=?epoch_store.epoch(), "Sending EndOfPublish message to consensus");
1028            }
1029        }
1030    }
1031}
1032
1033impl SubmitToConsensus for Arc<ConsensusAdapter> {
1034    fn submit_to_consensus(
1035        &self,
1036        transactions: &[ConsensusTransaction],
1037        epoch_store: &Arc<AuthorityPerEpochStore>,
1038    ) -> SuiResult {
1039        self.submit_batch(transactions, None, epoch_store, None, None)
1040            .map(|_| ())
1041    }
1042
1043    fn submit_best_effort(
1044        &self,
1045        transaction: &ConsensusTransaction,
1046        epoch_store: &Arc<AuthorityPerEpochStore>,
1047        // timeout is required, or the spawned task can run forever
1048        timeout: Duration,
1049    ) -> SuiResult {
1050        if transaction.is_user_transaction() {
1051            debug_fatal!("submit_best_effort called with a user transaction");
1052            return Err(SuiErrorKind::GenericAuthorityError {
1053                error: "submit_best_effort does not accept user transactions".to_string(),
1054            }
1055            .into());
1056        }
1057
1058        // There is no submit semaphone on this path as it services system msgs only.
1059        let _in_flight_submission_guard =
1060            GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
1061
1062        let key = SequencedConsensusTransactionKey::External(transaction.key());
1063        let tx_type = classify(transaction);
1064
1065        let async_stage = {
1066            let transaction = transaction.clone();
1067            let epoch_store = epoch_store.clone();
1068            let this = self.clone();
1069
1070            async move {
1071                let result = tokio::time::timeout(
1072                    timeout,
1073                    this.submit_inner(&[transaction], &epoch_store, &[key], tx_type),
1074                )
1075                .await;
1076
1077                if let Err(e) = result {
1078                    warn!("Consensus submission timed out: {e:?}");
1079                    this.metrics
1080                        .sequencing_best_effort_timeout
1081                        .with_label_values(&[tx_type])
1082                        .inc();
1083                }
1084            }
1085        };
1086
1087        let epoch_store = epoch_store.clone();
1088        spawn_monitored_task!(epoch_store.within_alive_epoch(async_stage));
1089        Ok(())
1090    }
1091}
1092
1093struct CancelOnDrop<T>(JoinHandle<T>);
1094
1095impl<T> Deref for CancelOnDrop<T> {
1096    type Target = JoinHandle<T>;
1097
1098    fn deref(&self) -> &Self::Target {
1099        &self.0
1100    }
1101}
1102
1103impl<T> Drop for CancelOnDrop<T> {
1104    fn drop(&mut self) {
1105        self.0.abort();
1106    }
1107}
1108
1109/// Tracks number of inflight consensus requests and relevant metrics
1110struct InflightDropGuard<'a> {
1111    adapter: &'a ConsensusAdapter,
1112    start: Instant,
1113    submitted: bool,
1114    tx_type: &'static str,
1115    processed_method: ProcessedMethod,
1116    /// Number of transactions this guard accounts for.
1117    /// > 1 for soft bundles.
1118    inflight_count: u64,
1119}
1120
1121impl<'a> InflightDropGuard<'a> {
1122    pub fn acquire(
1123        adapter: &'a ConsensusAdapter,
1124        tx_type: &'static str,
1125        inflight_count: u64,
1126    ) -> Self {
1127        adapter
1128            .num_inflight_transactions
1129            .fetch_add(inflight_count, Ordering::SeqCst);
1130        adapter
1131            .metrics
1132            .sequencing_certificate_inflight
1133            .with_label_values(&[tx_type])
1134            .inc();
1135        adapter
1136            .metrics
1137            .sequencing_certificate_attempt
1138            .with_label_values(&[tx_type])
1139            .inc();
1140        Self {
1141            adapter,
1142            start: Instant::now(),
1143            submitted: false,
1144            tx_type,
1145            processed_method: ProcessedMethod::ConsensusMessageProcessed,
1146            inflight_count,
1147        }
1148    }
1149}
1150
1151impl Drop for InflightDropGuard<'_> {
1152    fn drop(&mut self) {
1153        self.adapter
1154            .num_inflight_transactions
1155            .fetch_sub(self.inflight_count, Ordering::SeqCst);
1156        self.adapter
1157            .metrics
1158            .sequencing_certificate_inflight
1159            .with_label_values(&[self.tx_type])
1160            .dec();
1161        // Wake the admission queue drainer so it can submit more transactions.
1162        self.adapter.inflight_slot_freed_notify.notify_one();
1163
1164        let latency = self.start.elapsed();
1165        let submitted = if self.submitted {
1166            "submitted"
1167        } else {
1168            "skipped"
1169        };
1170
1171        self.adapter
1172            .metrics
1173            .sequencing_certificate_latency
1174            .with_label_values(&[
1175                submitted,
1176                self.tx_type,
1177                self.processed_method.metric_label(),
1178            ])
1179            .observe(latency.as_secs_f64());
1180    }
1181}
1182
1183#[derive(Copy, Clone, PartialEq, Eq)]
1184enum ProcessedMethod {
1185    ConsensusMessageProcessed,
1186    ConsensusStatusReceived,
1187    ConsensusStatusExpired,
1188    CheckpointExecuted,
1189}
1190
1191impl ProcessedMethod {
1192    fn method_name(self) -> &'static str {
1193        match self {
1194            ProcessedMethod::ConsensusMessageProcessed => "consensus (processed message)",
1195            ProcessedMethod::ConsensusStatusReceived => "consensus (transaction status)",
1196            ProcessedMethod::ConsensusStatusExpired => "consensus (status expired)",
1197            ProcessedMethod::CheckpointExecuted => "checkpoint execution",
1198        }
1199    }
1200
1201    fn metric_label(self) -> &'static str {
1202        match self {
1203            ProcessedMethod::ConsensusMessageProcessed => "consensus_message",
1204            ProcessedMethod::ConsensusStatusReceived => "consensus_status",
1205            ProcessedMethod::ConsensusStatusExpired => "consensus_status_expired",
1206            ProcessedMethod::CheckpointExecuted => "checkpoint_execution",
1207        }
1208    }
1209}
1210
1211/// Outcome of the submit loop in `submit_and_wait_inner`.
1212enum SequencingOutcome {
1213    /// A user-transaction submission that was sequenced and whose positions all
1214    /// reached a terminal consensus status; nothing further to wait for.
1215    Sequenced(Vec<ConsensusTxStatus>),
1216    /// A system-message submission whose block was sequenced.
1217    BlockSequenced,
1218    /// A user-transaction submission whose block was sequenced, but at least one
1219    /// position expired from the status cache before its status was read. The
1220    /// terminal outcome existed and was missed; nothing further to wait for.
1221    StatusExpired,
1222}
1223
1224fn status_label(status: ConsensusTxStatus) -> &'static str {
1225    match status {
1226        ConsensusTxStatus::Finalized => "finalized",
1227        ConsensusTxStatus::Rejected => "rejected",
1228        ConsensusTxStatus::Dropped => "dropped",
1229    }
1230}