1use std::net::IpAddr;
5use std::ops::Deref;
6use std::sync::Arc;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9use std::time::Instant;
10
11use consensus_core::BlockStatus;
12use futures::FutureExt;
13use futures::StreamExt;
14use futures::future::{self, Either, join_all, select};
15use futures::stream::FuturesUnordered;
16use mysten_common::debug_fatal;
17use mysten_metrics::{
18 GaugeGuard, InflightGuardFutureExt, LATENCY_SEC_BUCKETS, spawn_monitored_task,
19};
20use parking_lot::RwLockReadGuard;
21use prometheus::Histogram;
22use prometheus::HistogramVec;
23use prometheus::IntCounter;
24use prometheus::IntCounterVec;
25use prometheus::IntGauge;
26use prometheus::IntGaugeVec;
27use prometheus::Registry;
28use prometheus::{
29 register_histogram_vec_with_registry, register_histogram_with_registry,
30 register_int_counter_vec_with_registry, register_int_counter_with_registry,
31 register_int_gauge_vec_with_registry, register_int_gauge_with_registry,
32};
33use sui_types::base_types::AuthorityName;
34use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
35use sui_types::fp_ensure;
36use sui_types::messages_consensus::ConsensusPosition;
37use sui_types::messages_consensus::ConsensusTransactionKind;
38use sui_types::messages_consensus::{ConsensusTransaction, ConsensusTransactionKey};
39use tokio::sync::{Notify, Semaphore, SemaphorePermit, oneshot};
40use tokio::task::JoinHandle;
41use tokio::time::Duration;
42use tokio::time::{self};
43use tracing::{Instrument, debug, debug_span, info, instrument, warn};
44
45use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
46use crate::authority::consensus_tx_status_cache::{
47 ConsensusTxStatus, NotifyReadConsensusTxStatusResult,
48};
49use crate::checkpoints::CheckpointStore;
50use crate::consensus_handler::{SequencedConsensusTransactionKey, classify, tx_type_label};
51use crate::epoch::reconfiguration::{ReconfigState, ReconfigurationInitiator};
52use crate::staggered_submission::proposers_metric_label;
53
54#[cfg(test)]
55#[path = "unit_tests/consensus_tests.rs"]
56pub mod consensus_tests;
57
58#[derive(Clone)]
59pub struct ConsensusAdapterMetrics {
60 pub sequencing_certificate_attempt: IntCounterVec,
62 pub sequencing_certificate_success: IntCounterVec,
63 pub sequencing_certificate_failures: IntCounterVec,
64 pub sequencing_certificate_status: IntCounterVec,
65 pub sequencing_certificate_settled_status: IntCounterVec,
66 pub sequencing_certificate_inflight: IntGaugeVec,
67 pub sequencing_acknowledge_latency: HistogramVec,
68 pub sequencing_certificate_latency: HistogramVec,
69 pub sequencing_certificate_processed: IntCounterVec,
70 pub sequencing_in_flight_semaphore_wait: IntGauge,
71 pub sequencing_in_flight_submissions: IntGauge,
72 pub sequencing_best_effort_timeout: IntCounterVec,
73 pub consensus_latency: Histogram,
74 pub num_rejected_cert_in_epoch_boundary: IntCounter,
75}
76
77impl ConsensusAdapterMetrics {
78 pub fn new(registry: &Registry) -> Self {
79 Self {
80 sequencing_certificate_attempt: register_int_counter_vec_with_registry!(
81 "sequencing_certificate_attempt",
82 "Counts the number of certificates the validator attempts to sequence.",
83 &["tx_type"],
84 registry,
85 )
86 .unwrap(),
87 sequencing_certificate_success: register_int_counter_vec_with_registry!(
88 "sequencing_certificate_success",
89 "Counts the number of successfully sequenced certificates.",
90 &["tx_type"],
91 registry,
92 )
93 .unwrap(),
94 sequencing_certificate_failures: register_int_counter_vec_with_registry!(
95 "sequencing_certificate_failures",
96 "Counts the number of sequenced certificates that failed other than by timeout.",
97 &["tx_type"],
98 registry,
99 )
100 .unwrap(),
101 sequencing_certificate_status: register_int_counter_vec_with_registry!(
102 "sequencing_certificate_status",
103 "The status of the certificate sequencing as reported by consensus. The status can be either sequenced or garbage collected.",
104 &["tx_type", "status"],
105 registry,
106 )
107 .unwrap(),
108 sequencing_certificate_settled_status: register_int_counter_vec_with_registry!(
109 "sequencing_certificate_settled_status",
110 "The terminal per-position consensus status (finalized, rejected or dropped) of transactions whose submission settled via position status.",
111 &["tx_type", "status"],
112 registry,
113 )
114 .unwrap(),
115 sequencing_certificate_inflight: register_int_gauge_vec_with_registry!(
116 "sequencing_certificate_inflight",
117 "The inflight requests to sequence certificates.",
118 &["tx_type"],
119 registry,
120 )
121 .unwrap(),
122 sequencing_acknowledge_latency: register_histogram_vec_with_registry!(
123 "sequencing_acknowledge_latency",
124 "The latency for acknowledgement from sequencing engine. The overall sequencing latency is measured by the sequencing_certificate_latency metric",
125 &["retry", "tx_type"],
126 LATENCY_SEC_BUCKETS.to_vec(),
127 registry,
128 ).unwrap(),
129 sequencing_certificate_latency: register_histogram_vec_with_registry!(
130 "sequencing_certificate_latency",
131 "The latency for sequencing a certificate.",
132 &["submitted", "tx_type", "processed_method", "proposers"],
133 LATENCY_SEC_BUCKETS.to_vec(),
134 registry,
135 ).unwrap(),
136 sequencing_certificate_processed: register_int_counter_vec_with_registry!(
137 "sequencing_certificate_processed",
138 "The number of certificates that have been processed either by consensus or checkpoint.",
139 &["source"],
140 registry
141 ).unwrap(),
142 sequencing_in_flight_semaphore_wait: register_int_gauge_with_registry!(
143 "sequencing_in_flight_semaphore_wait",
144 "How many requests are blocked on submit_permit.",
145 registry,
146 )
147 .unwrap(),
148 sequencing_in_flight_submissions: register_int_gauge_with_registry!(
149 "sequencing_in_flight_submissions",
150 "Number of transactions submitted to local consensus instance and not yet sequenced",
151 registry,
152 )
153 .unwrap(),
154 sequencing_best_effort_timeout: register_int_counter_vec_with_registry!(
155 "sequencing_best_effort_timeout",
156 "The number of times the best effort submission has timed out.",
157 &["tx_type"],
158 registry,
159 ).unwrap(),
160 consensus_latency: register_histogram_with_registry!(
163 "validator_service_consensus_latency",
164 "Time spent between submitting a txn to consensus and getting back local acknowledgement. Execution and finalization time are not included.",
165 mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
166 registry,
167 ).unwrap(),
168 num_rejected_cert_in_epoch_boundary: register_int_counter_with_registry!(
169 "validator_service_num_rejected_cert_in_epoch_boundary",
170 "Number of rejected transaction certificate during epoch transitioning",
171 registry,
172 ).unwrap(),
173 }
174 }
175
176 pub fn new_test() -> Self {
177 Self::new(&Registry::default())
178 }
179}
180
181pub trait ConsensusOverloadChecker: Sync + Send + 'static {
183 fn check_consensus_overload(&self) -> SuiResult;
184}
185
186pub type BlockStatusReceiver = oneshot::Receiver<BlockStatus>;
187
188#[mockall::automock]
189pub trait SubmitToConsensus: Sync + Send + 'static {
190 fn submit_to_consensus(
191 &self,
192 transactions: &[ConsensusTransaction],
193 epoch_store: &Arc<AuthorityPerEpochStore>,
194 ) -> SuiResult;
195
196 fn submit_best_effort(
205 &self,
206 transaction: &ConsensusTransaction,
207 epoch_store: &Arc<AuthorityPerEpochStore>,
208 timeout: Duration,
209 ) -> SuiResult;
210}
211
212#[mockall::automock]
213#[async_trait::async_trait]
214pub trait ConsensusClient: Sync + Send + 'static {
215 async fn submit(
216 &self,
217 transactions: &[ConsensusTransaction],
218 epoch_store: &Arc<AuthorityPerEpochStore>,
219 ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)>;
220}
221
222pub struct ConsensusAdapter {
224 consensus_client: Arc<dyn ConsensusClient>,
226 checkpoint_store: Arc<CheckpointStore>,
228 authority: AuthorityName,
230 max_pending_transactions: usize,
232 num_inflight_transactions: AtomicU64,
234 metrics: ConsensusAdapterMetrics,
236 submit_semaphore: Arc<Semaphore>,
238 inflight_slot_freed_notify: Arc<Notify>,
242}
243
244impl ConsensusAdapter {
245 pub fn new(
247 consensus_client: Arc<dyn ConsensusClient>,
248 checkpoint_store: Arc<CheckpointStore>,
249 authority: AuthorityName,
250 max_pending_transactions: usize,
251 max_pending_local_submissions: usize,
252 metrics: ConsensusAdapterMetrics,
253 inflight_slot_freed_notify: Arc<Notify>,
254 ) -> Self {
255 let num_inflight_transactions = Default::default();
256 Self {
257 consensus_client,
258 checkpoint_store,
259 authority,
260 max_pending_transactions,
261 num_inflight_transactions,
262 metrics,
263 submit_semaphore: Arc::new(Semaphore::new(max_pending_local_submissions)),
264 inflight_slot_freed_notify,
265 }
266 }
267
268 pub fn num_inflight_transactions(&self) -> u64 {
270 self.num_inflight_transactions.load(Ordering::Relaxed)
271 }
272
273 pub fn max_pending_transactions(&self) -> usize {
275 self.max_pending_transactions
276 }
277
278 pub async fn submit_and_get_positions(
281 self: &Arc<Self>,
282 consensus_transactions: Vec<ConsensusTransaction>,
283 epoch_store: &Arc<AuthorityPerEpochStore>,
284 submitter_client_addr: Option<IpAddr>,
285 ) -> Result<Vec<ConsensusPosition>, SuiError> {
286 let (tx_consensus_positions, rx_consensus_positions) = oneshot::channel();
287
288 {
289 let reconfiguration_lock = epoch_store.get_reconfig_state_read_lock_guard();
291 if !reconfiguration_lock.should_accept_user_certs() {
292 self.metrics.num_rejected_cert_in_epoch_boundary.inc();
293 return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
294 }
295
296 let _metrics_guard = self.metrics.consensus_latency.start_timer();
300
301 self.submit_batch(
302 &consensus_transactions,
303 Some(&reconfiguration_lock),
304 epoch_store,
305 Some(tx_consensus_positions),
306 submitter_client_addr,
307 )?;
308 }
309
310 rx_consensus_positions.await.unwrap_or_else(|_| {
311 self.metrics.num_rejected_cert_in_epoch_boundary.inc();
314 Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into())
315 })
316 }
317
318 pub fn recover_end_of_publish(self: &Arc<Self>, epoch_store: &Arc<AuthorityPerEpochStore>) {
319 if epoch_store.should_send_end_of_publish() {
322 let transaction = ConsensusTransaction::new_end_of_publish(self.authority);
323 info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
324 self.submit_unchecked(&[transaction], epoch_store, None, None);
325 }
326 }
327
328 pub fn submit(
336 self: &Arc<Self>,
337 transaction: ConsensusTransaction,
338 lock: Option<&RwLockReadGuard<ReconfigState>>,
339 epoch_store: &Arc<AuthorityPerEpochStore>,
340 tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
341 submitter_client_addr: Option<IpAddr>,
342 ) -> SuiResult<JoinHandle<()>> {
343 self.submit_batch(
344 &[transaction],
345 lock,
346 epoch_store,
347 tx_consensus_position,
348 submitter_client_addr,
349 )
350 }
351
352 pub fn submit_batch(
355 self: &Arc<Self>,
356 transactions: &[ConsensusTransaction],
357 _lock: Option<&RwLockReadGuard<ReconfigState>>,
358 epoch_store: &Arc<AuthorityPerEpochStore>,
359 tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
360 submitter_client_addr: Option<IpAddr>,
361 ) -> SuiResult<JoinHandle<()>> {
362 if transactions.len() > 1 {
363 for transaction in transactions {
365 fp_ensure!(
366 transaction.is_user_transaction(),
367 SuiErrorKind::InvalidTxKindInSoftBundle.into()
368 );
369 }
370 }
371
372 Ok(self.submit_unchecked(
373 transactions,
374 epoch_store,
375 tx_consensus_position,
376 submitter_client_addr,
377 ))
378 }
379
380 fn check_limits(&self) -> bool {
383 if self.num_inflight_transactions.load(Ordering::Relaxed) as usize
385 >= self.max_pending_transactions
386 {
387 return false;
388 }
389 self.submit_semaphore.available_permits() > 0
391 }
392
393 fn submit_unchecked(
394 self: &Arc<Self>,
395 transactions: &[ConsensusTransaction],
396 epoch_store: &Arc<AuthorityPerEpochStore>,
397 tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
398 submitter_client_addr: Option<IpAddr>,
399 ) -> JoinHandle<()> {
400 let async_stage = self
402 .clone()
403 .submit_and_wait(
404 transactions.to_vec(),
405 epoch_store.clone(),
406 tx_consensus_position,
407 submitter_client_addr,
408 )
409 .in_current_span();
410 let join_handle = spawn_monitored_task!(async_stage);
413 join_handle
414 }
415
416 async fn submit_and_wait(
417 self: Arc<Self>,
418 transactions: Vec<ConsensusTransaction>,
419 epoch_store: Arc<AuthorityPerEpochStore>,
420 tx_consensus_position: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
421 submitter_client_addr: Option<IpAddr>,
422 ) {
423 epoch_store
437 .within_alive_epoch(self.submit_and_wait_inner(
438 transactions,
439 &epoch_store,
440 tx_consensus_position,
441 submitter_client_addr,
442 ))
443 .await
444 .ok(); }
446
447 #[allow(clippy::option_map_unit_fn)]
448 #[instrument(name="ConsensusAdapter::submit_and_wait_inner", level="trace", skip_all, fields(tx_count = ?transactions.len(), tx_type = tracing::field::Empty, tx_keys = tracing::field::Empty, submit_status = tracing::field::Empty, consensus_positions = tracing::field::Empty))]
449 async fn submit_and_wait_inner(
450 self: Arc<Self>,
451 transactions: Vec<ConsensusTransaction>,
452 epoch_store: &Arc<AuthorityPerEpochStore>,
453 mut tx_consensus_positions: Option<oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>>,
454 submitter_client_addr: Option<IpAddr>,
455 ) {
456 if transactions.is_empty() {
457 debug!(
460 "Performing a ping check, pinging consensus to get a consensus position in next block"
461 );
462 let (consensus_positions, _status_waiter) = self
463 .submit_inner(&transactions, epoch_store, &[], "ping")
464 .await;
465
466 if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
467 let _ = tx_consensus_positions.send(Ok(consensus_positions));
468 } else {
469 debug_fatal!("Ping check must have a consensus position channel");
470 }
471 return;
472 }
473
474 epoch_store.record_submitted_user_transactions(&transactions, submitter_client_addr);
476
477 let is_soft_bundle = transactions.len() > 1;
482 let is_system_message = !transactions[0].is_user_transaction();
483
484 let mut transaction_keys = Vec::new();
485 let mut tx_consensus_positions = tx_consensus_positions;
486
487 for transaction in &transactions {
488 if matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..)) {
489 info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
490 epoch_store.record_epoch_pending_certs_process_time_metric();
491 }
492
493 let transaction_key = SequencedConsensusTransactionKey::External(transaction.key());
494 transaction_keys.push(transaction_key);
495 }
496 let tx_type = tx_type_label(&transactions);
497 tracing::Span::current().record("tx_type", tx_type);
498 tracing::Span::current().record("tx_keys", tracing::field::debug(&transaction_keys));
499
500 let mut guard = InflightDropGuard::acquire(
501 &self,
502 tx_type,
503 proposers_metric_label(&transactions, epoch_store),
504 transactions.len() as u64,
505 );
506
507 let make_processing_error =
508 |method: ProcessedMethod| -> SuiError { processing_error(&transaction_keys, method) };
509
510 let already_processed =
513 self.check_processed_via_consensus_or_checkpoint(&transaction_keys, epoch_store);
514 if let Some(method) = already_processed {
515 guard.processed_method = method;
516 if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
517 let _ = tx_consensus_positions.send(Err(make_processing_error(method)));
518 }
519 }
520
521 let _monitor = if matches!(
523 transactions[0].kind,
524 ConsensusTransactionKind::EndOfPublish(_)
525 | ConsensusTransactionKind::CapabilityNotification(_)
526 | ConsensusTransactionKind::CapabilityNotificationV2(_)
527 | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
528 | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
529 ) {
530 assert!(
531 !is_soft_bundle,
532 "System transactions should have been submitted individually"
533 );
534 let transaction_keys = transaction_keys.clone();
535 Some(CancelOnDrop(spawn_monitored_task!(async {
536 let mut i = 0u64;
537 loop {
538 i += 1;
539 const WARN_DELAY_S: u64 = 30;
540 tokio::time::sleep(Duration::from_secs(WARN_DELAY_S)).await;
541 let total_wait = i * WARN_DELAY_S;
542 warn!(
543 "Still waiting {} seconds for transactions {:?} to commit in consensus",
544 total_wait, transaction_keys
545 );
546 }
547 })))
548 } else {
549 None
550 };
551
552 if already_processed.is_none() {
553 debug!("Submitting {:?} to consensus", transaction_keys);
554 guard.submitted = true;
555
556 let _permit: Option<SemaphorePermit> = if is_system_message {
560 None
561 } else {
562 Some(
563 self.submit_semaphore
564 .acquire()
565 .count_in_flight(self.metrics.sequencing_in_flight_semaphore_wait.clone())
566 .await
567 .expect("Consensus adapter does not close semaphore"),
568 )
569 };
570 let _in_flight_submission_guard =
571 GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
572
573 let submit_fut = async {
576 const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
577
578 loop {
579 let (consensus_positions, status_waiter) = self
581 .submit_inner(&transactions, epoch_store, &transaction_keys, tx_type)
582 .await;
583
584 if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
585 tracing::Span::current().record(
586 "consensus_positions",
587 tracing::field::debug(&consensus_positions),
588 );
589 let _ = tx_consensus_positions.send(Ok(consensus_positions.clone()));
595 }
596
597 match status_waiter.await {
598 Ok(status @ BlockStatus::Sequenced(_)) => {
599 tracing::Span::current()
600 .record("status", tracing::field::debug(&status));
601 self.metrics
602 .sequencing_certificate_status
603 .with_label_values(&[tx_type, "sequenced"])
604 .inc();
605 debug!(
606 "Transaction {transaction_keys:?} has been sequenced by consensus."
607 );
608 if is_system_message {
609 break SequencingOutcome::BlockSequenced;
614 }
615 if consensus_positions.len() != transactions.len() {
616 debug_fatal!(
617 "Consensus client returned {} positions for {} transactions",
618 consensus_positions.len(),
619 transactions.len()
620 );
621 break SequencingOutcome::BlockSequenced;
622 }
623 match self
626 .wait_for_position_statuses(&consensus_positions, epoch_store)
627 .await
628 {
629 Some(statuses) => break SequencingOutcome::Sequenced(statuses),
630 None => {
631 debug!(
641 "Transaction {transaction_keys:?} status expired before being read. Ending submission."
642 );
643 self.metrics
644 .sequencing_certificate_status
645 .with_label_values(&[tx_type, "status_expired"])
646 .inc();
647 break SequencingOutcome::StatusExpired;
648 }
649 }
650 }
651 Ok(status @ BlockStatus::GarbageCollected(_)) => {
652 tracing::Span::current()
653 .record("status", tracing::field::debug(&status));
654 self.metrics
655 .sequencing_certificate_status
656 .with_label_values(&[tx_type, "garbage_collected"])
657 .inc();
658 debug!(
662 "Transaction {transaction_keys:?} was garbage collected before being sequenced. Will be retried."
663 );
664 time::sleep(RETRY_DELAY_STEP).await;
665 continue;
666 }
667 Err(err) => {
668 warn!(
669 "Error while waiting for status from consensus for transactions {transaction_keys:?}, with error {:?}. Will be retried.",
670 err
671 );
672 time::sleep(RETRY_DELAY_STEP).await;
673 continue;
674 }
675 }
676 }
677 };
678
679 let processed_waiter = self
685 .processed_notify(transaction_keys.clone(), epoch_store)
686 .boxed();
687 let processed_via_notify;
688 guard.processed_method = match select(processed_waiter, submit_fut.boxed()).await {
689 Either::Left((observed, _submit_fut)) => {
690 processed_via_notify = true;
691 observed
692 }
693 Either::Right((SequencingOutcome::Sequenced(statuses), _processed_waiter)) => {
694 processed_via_notify = false;
695 for status in statuses {
696 self.metrics
697 .sequencing_certificate_settled_status
698 .with_label_values(&[tx_type, status.metric_label()])
699 .inc();
700 }
701 ProcessedMethod::ConsensusStatusReceived
702 }
703 Either::Right((SequencingOutcome::StatusExpired, _processed_waiter)) => {
704 processed_via_notify = false;
705 ProcessedMethod::ConsensusStatusExpired
706 }
707 Either::Right((SequencingOutcome::BlockSequenced, processed_waiter)) => {
708 debug!("Submitted {transaction_keys:?} to consensus");
709 processed_via_notify = false;
710 processed_waiter.await
711 }
712 };
713 if processed_via_notify
718 && let Some(tx_consensus_positions) = tx_consensus_positions.take()
719 {
720 let _ =
721 tx_consensus_positions.send(Err(make_processing_error(guard.processed_method)));
722 }
723 }
724 debug!(
725 "{transaction_keys:?} processed via {}",
726 guard.processed_method.method_name()
727 );
728
729 self.metrics
730 .sequencing_certificate_success
731 .with_label_values(&[tx_type])
732 .inc();
733 }
734
735 #[instrument(name = "ConsensusAdapter::submit_inner", level = "trace", skip_all)]
736 async fn submit_inner(
737 self: &Arc<Self>,
738 transactions: &[ConsensusTransaction],
739 epoch_store: &Arc<AuthorityPerEpochStore>,
740 transaction_keys: &[SequencedConsensusTransactionKey],
741 tx_type: &str,
742 ) -> (Vec<ConsensusPosition>, BlockStatusReceiver) {
743 let ack_start = Instant::now();
744 let mut retries: u32 = 0;
745 let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
746 Duration::from_millis(100),
747 Duration::from_secs(10),
748 );
749
750 let (consensus_positions, status_waiter) = loop {
751 let span = debug_span!("client_submit");
752 match self
753 .consensus_client
754 .submit(transactions, epoch_store)
755 .instrument(span)
756 .await
757 {
758 Err(err) => {
759 if cfg!(msim) || retries > 3 {
761 warn!(
762 "Failed to submit transactions {transaction_keys:?} to consensus: {err}. Retry #{retries}"
763 );
764 }
765 self.metrics
766 .sequencing_certificate_failures
767 .with_label_values(&[tx_type])
768 .inc();
769 retries += 1;
770
771 time::sleep(backoff.next().unwrap()).await;
772 }
773 Ok((consensus_positions, status_waiter)) => {
774 break (consensus_positions, status_waiter);
775 }
776 }
777 };
778
779 let bucket = match retries {
783 0..=10 => retries.to_string(), 11..=20 => "between_10_and_20".to_string(),
785 21..=50 => "between_20_and_50".to_string(),
786 51..=100 => "between_50_and_100".to_string(),
787 _ => "over_100".to_string(),
788 };
789
790 self.metrics
791 .sequencing_acknowledge_latency
792 .with_label_values(&[bucket.as_str(), tx_type])
793 .observe(ack_start.elapsed().as_secs_f64());
794
795 (consensus_positions, status_waiter)
796 }
797
798 fn check_processed_via_consensus_or_checkpoint(
807 self: &Arc<Self>,
808 transaction_keys: &[SequencedConsensusTransactionKey],
809 epoch_store: &Arc<AuthorityPerEpochStore>,
810 ) -> Option<ProcessedMethod> {
811 let mut seen_checkpoint = false;
812 for transaction_key in transaction_keys {
813 if epoch_store
816 .is_consensus_message_processed(transaction_key)
817 .expect("Storage error when checking consensus message processed")
818 {
819 self.metrics
820 .sequencing_certificate_processed
821 .with_label_values(&["consensus"])
822 .inc();
823 continue;
824 }
825
826 if let SequencedConsensusTransactionKey::External(ConsensusTransactionKey::Certificate(
829 digest,
830 )) = transaction_key
831 && epoch_store
832 .is_transaction_executed_in_checkpoint(digest)
833 .expect("Storage error when checking transaction executed in checkpoint")
834 {
835 self.metrics
836 .sequencing_certificate_processed
837 .with_label_values(&["checkpoint"])
838 .inc();
839 seen_checkpoint = true;
840 continue;
841 }
842
843 if let SequencedConsensusTransactionKey::External(
847 ConsensusTransactionKey::CheckpointSignature(_, seq)
848 | ConsensusTransactionKey::CheckpointSignatureV2(_, seq, _),
849 ) = transaction_key
850 && let Some(synced_seq) = self
851 .checkpoint_store
852 .get_highest_synced_checkpoint_seq_number()
853 .expect("Storage error when reading highest synced checkpoint")
854 && synced_seq >= *seq
855 {
856 self.metrics
857 .sequencing_certificate_processed
858 .with_label_values(&["synced_checkpoint"])
859 .inc();
860 seen_checkpoint = true;
861 continue;
862 }
863
864 return None;
866 }
867
868 if seen_checkpoint {
869 Some(ProcessedMethod::CheckpointExecuted)
870 } else {
871 Some(ProcessedMethod::ConsensusMessageProcessed)
872 }
873 }
874
875 async fn processed_notify(
882 self: &Arc<Self>,
883 transaction_keys: Vec<SequencedConsensusTransactionKey>,
884 epoch_store: &Arc<AuthorityPerEpochStore>,
885 ) -> ProcessedMethod {
886 let notifications = FuturesUnordered::new();
887 for transaction_key in transaction_keys {
888 let transaction_digests = match transaction_key {
889 SequencedConsensusTransactionKey::External(
890 ConsensusTransactionKey::Certificate(digest),
891 ) => vec![digest],
892 _ => vec![],
893 };
894
895 let checkpoint_synced_future = if let SequencedConsensusTransactionKey::External(
896 ConsensusTransactionKey::CheckpointSignature(_, checkpoint_sequence_number)
897 | ConsensusTransactionKey::CheckpointSignatureV2(_, checkpoint_sequence_number, _),
898 ) = transaction_key
899 {
900 Either::Left(
903 self.checkpoint_store
904 .notify_read_synced_checkpoint(checkpoint_sequence_number),
905 )
906 } else {
907 Either::Right(future::pending())
908 };
909
910 notifications.push(async move {
913 tokio::select! {
914 _ = epoch_store.consensus_messages_processed_notify(vec![transaction_key]) => {
915 self.metrics.sequencing_certificate_processed.with_label_values(&["consensus"]).inc();
916 return ProcessedMethod::ConsensusMessageProcessed;
917 },
918 _ = epoch_store.transactions_executed_in_checkpoint_notify(transaction_digests), if !transaction_digests.is_empty() => {
919 self.metrics.sequencing_certificate_processed.with_label_values(&["checkpoint"]).inc();
920 }
921 _ = checkpoint_synced_future => {
922 self.metrics.sequencing_certificate_processed.with_label_values(&["synced_checkpoint"]).inc();
923 }
924 }
925 ProcessedMethod::CheckpointExecuted
926 });
927 }
928
929 let processed_methods = notifications.collect::<Vec<ProcessedMethod>>().await;
930 for method in processed_methods {
931 if method == ProcessedMethod::CheckpointExecuted {
932 return ProcessedMethod::CheckpointExecuted;
933 }
934 }
935 ProcessedMethod::ConsensusMessageProcessed
936 }
937
938 async fn wait_for_position_statuses(
943 &self,
944 consensus_positions: &[ConsensusPosition],
945 epoch_store: &Arc<AuthorityPerEpochStore>,
946 ) -> Option<Vec<ConsensusTxStatus>> {
947 join_all(consensus_positions.iter().map(|position| {
948 epoch_store
949 .consensus_tx_status_cache
950 .notify_read_transaction_status(*position)
951 }))
952 .await
953 .into_iter()
954 .map(|result| match result {
955 NotifyReadConsensusTxStatusResult::Status(status) => Some(status),
956 NotifyReadConsensusTxStatusResult::Expired(_) => None,
957 })
958 .collect()
959 }
960}
961
962impl ConsensusOverloadChecker for ConsensusAdapter {
963 fn check_consensus_overload(&self) -> SuiResult {
964 fp_ensure!(
965 self.check_limits(),
966 SuiErrorKind::TooManyTransactionsPendingConsensus.into()
967 );
968 Ok(())
969 }
970}
971
972pub struct NoopConsensusOverloadChecker {}
973
974impl ConsensusOverloadChecker for NoopConsensusOverloadChecker {
975 fn check_consensus_overload(&self) -> SuiResult {
976 Ok(())
977 }
978}
979
980impl ReconfigurationInitiator for Arc<ConsensusAdapter> {
981 fn close_epoch(&self, epoch_store: &Arc<AuthorityPerEpochStore>) {
985 {
986 let reconfig_guard = epoch_store.get_reconfig_state_write_lock_guard();
987 if !reconfig_guard.should_accept_user_certs() {
988 return;
990 }
991 epoch_store.close_user_certs_for_manual_epoch_close(reconfig_guard);
992 }
993 if epoch_store.should_send_end_of_publish() {
994 if let Err(err) = self.submit(
995 ConsensusTransaction::new_end_of_publish(self.authority),
996 None,
997 epoch_store,
998 None,
999 None,
1000 ) {
1001 warn!("Error when sending end of publish message: {:?}", err);
1002 } else {
1003 info!(epoch=?epoch_store.epoch(), "Sending EndOfPublish message to consensus");
1004 }
1005 }
1006 }
1007}
1008
1009impl SubmitToConsensus for Arc<ConsensusAdapter> {
1010 fn submit_to_consensus(
1011 &self,
1012 transactions: &[ConsensusTransaction],
1013 epoch_store: &Arc<AuthorityPerEpochStore>,
1014 ) -> SuiResult {
1015 self.submit_batch(transactions, None, epoch_store, None, None)
1016 .map(|_| ())
1017 }
1018
1019 fn submit_best_effort(
1020 &self,
1021 transaction: &ConsensusTransaction,
1022 epoch_store: &Arc<AuthorityPerEpochStore>,
1023 timeout: Duration,
1025 ) -> SuiResult {
1026 if transaction.is_user_transaction() {
1027 debug_fatal!("submit_best_effort called with a user transaction");
1028 return Err(SuiErrorKind::GenericAuthorityError {
1029 error: "submit_best_effort does not accept user transactions".to_string(),
1030 }
1031 .into());
1032 }
1033
1034 let _in_flight_submission_guard =
1036 GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
1037
1038 let key = SequencedConsensusTransactionKey::External(transaction.key());
1039 let tx_type = classify(transaction);
1040
1041 let async_stage = {
1042 let transaction = transaction.clone();
1043 let epoch_store = epoch_store.clone();
1044 let this = self.clone();
1045
1046 async move {
1047 let result = tokio::time::timeout(
1048 timeout,
1049 this.submit_inner(&[transaction], &epoch_store, &[key], tx_type),
1050 )
1051 .await;
1052
1053 if let Err(e) = result {
1054 warn!("Consensus submission timed out: {e:?}");
1055 this.metrics
1056 .sequencing_best_effort_timeout
1057 .with_label_values(&[tx_type])
1058 .inc();
1059 }
1060 }
1061 };
1062
1063 let epoch_store = epoch_store.clone();
1064 spawn_monitored_task!(epoch_store.within_alive_epoch(async_stage));
1065 Ok(())
1066 }
1067}
1068
1069struct CancelOnDrop<T>(JoinHandle<T>);
1070
1071impl<T> Deref for CancelOnDrop<T> {
1072 type Target = JoinHandle<T>;
1073
1074 fn deref(&self) -> &Self::Target {
1075 &self.0
1076 }
1077}
1078
1079impl<T> Drop for CancelOnDrop<T> {
1080 fn drop(&mut self) {
1081 self.0.abort();
1082 }
1083}
1084
1085struct InflightDropGuard<'a> {
1087 adapter: &'a ConsensusAdapter,
1088 start: Instant,
1089 submitted: bool,
1090 tx_type: &'static str,
1091 proposers: &'static str,
1093 processed_method: ProcessedMethod,
1094 inflight_count: u64,
1097}
1098
1099impl<'a> InflightDropGuard<'a> {
1100 pub fn acquire(
1101 adapter: &'a ConsensusAdapter,
1102 tx_type: &'static str,
1103 proposers: &'static str,
1104 inflight_count: u64,
1105 ) -> Self {
1106 adapter
1107 .num_inflight_transactions
1108 .fetch_add(inflight_count, Ordering::SeqCst);
1109 adapter
1110 .metrics
1111 .sequencing_certificate_inflight
1112 .with_label_values(&[tx_type])
1113 .inc();
1114 adapter
1115 .metrics
1116 .sequencing_certificate_attempt
1117 .with_label_values(&[tx_type])
1118 .inc();
1119 Self {
1120 adapter,
1121 start: Instant::now(),
1122 submitted: false,
1123 tx_type,
1124 proposers,
1125 processed_method: ProcessedMethod::ConsensusMessageProcessed,
1126 inflight_count,
1127 }
1128 }
1129}
1130
1131impl Drop for InflightDropGuard<'_> {
1132 fn drop(&mut self) {
1133 self.adapter
1134 .num_inflight_transactions
1135 .fetch_sub(self.inflight_count, Ordering::SeqCst);
1136 self.adapter
1137 .metrics
1138 .sequencing_certificate_inflight
1139 .with_label_values(&[self.tx_type])
1140 .dec();
1141 self.adapter.inflight_slot_freed_notify.notify_one();
1143
1144 let latency = self.start.elapsed();
1145 let submitted = if self.submitted {
1146 "submitted"
1147 } else {
1148 "skipped"
1149 };
1150
1151 self.adapter
1152 .metrics
1153 .sequencing_certificate_latency
1154 .with_label_values(&[
1155 submitted,
1156 self.tx_type,
1157 self.processed_method.metric_label(),
1158 self.proposers,
1159 ])
1160 .observe(latency.as_secs_f64());
1161 }
1162}
1163
1164pub(crate) fn processing_error<'a>(
1170 keys: impl IntoIterator<Item = &'a SequencedConsensusTransactionKey>,
1171 method: ProcessedMethod,
1172) -> SuiError {
1173 let digest = keys
1174 .into_iter()
1175 .find_map(SequencedConsensusTransactionKey::user_transaction_digest)
1176 .unwrap_or_default();
1177 SuiErrorKind::TransactionProcessing {
1178 digest,
1179 status: format!("processed via {}", method.method_name()),
1180 }
1181 .into()
1182}
1183
1184#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1187pub(crate) enum ProcessedMethod {
1188 ConsensusMessageProcessed,
1189 ConsensusStatusReceived,
1190 ConsensusStatusExpired,
1191 CheckpointExecuted,
1192}
1193
1194impl ProcessedMethod {
1195 fn method_name(self) -> &'static str {
1196 match self {
1197 ProcessedMethod::ConsensusMessageProcessed => "consensus (processed message)",
1198 ProcessedMethod::ConsensusStatusReceived => "consensus (transaction status)",
1199 ProcessedMethod::ConsensusStatusExpired => "consensus (status expired)",
1200 ProcessedMethod::CheckpointExecuted => "checkpoint execution",
1201 }
1202 }
1203
1204 pub(crate) fn metric_label(self) -> &'static str {
1205 match self {
1206 ProcessedMethod::ConsensusMessageProcessed => "consensus_message",
1207 ProcessedMethod::ConsensusStatusReceived => "consensus_status",
1208 ProcessedMethod::ConsensusStatusExpired => "consensus_status_expired",
1209 ProcessedMethod::CheckpointExecuted => "checkpoint_execution",
1210 }
1211 }
1212}
1213
1214enum SequencingOutcome {
1216 Sequenced(Vec<ConsensusTxStatus>),
1219 BlockSequenced,
1221 StatusExpired,
1225}