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 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 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 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
180pub 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 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
221pub struct ConsensusAdapter {
223 consensus_client: Arc<dyn ConsensusClient>,
225 checkpoint_store: Arc<CheckpointStore>,
227 authority: AuthorityName,
229 max_pending_transactions: usize,
231 num_inflight_transactions: AtomicU64,
233 metrics: ConsensusAdapterMetrics,
235 submit_semaphore: Arc<Semaphore>,
237 inflight_slot_freed_notify: Arc<Notify>,
241}
242
243impl ConsensusAdapter {
244 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 pub fn num_inflight_transactions(&self) -> u64 {
269 self.num_inflight_transactions.load(Ordering::Relaxed)
270 }
271
272 pub fn max_pending_transactions(&self) -> usize {
274 self.max_pending_transactions
275 }
276
277 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 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 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 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 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 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 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 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 fn check_limits(&self) -> bool {
382 if self.num_inflight_transactions.load(Ordering::Relaxed) as usize
384 >= self.max_pending_transactions
385 {
386 return false;
387 }
388 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 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 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 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(); }
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 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 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 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 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 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 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 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 let submit_fut = async {
598 const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
599
600 loop {
601 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 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 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 match self
648 .wait_for_position_statuses(&consensus_positions, epoch_store)
649 .await
650 {
651 Some(statuses) => break SequencingOutcome::Sequenced(statuses),
652 None => {
653 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 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 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 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 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 let bucket = match retries {
805 0..=10 => retries.to_string(), 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 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 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 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 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 return None;
888 }
889
890 if seen_checkpoint {
891 Some(ProcessedMethod::CheckpointExecuted)
892 } else {
893 Some(ProcessedMethod::ConsensusMessageProcessed)
894 }
895 }
896
897 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 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 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 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 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 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: 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 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
1109struct InflightDropGuard<'a> {
1111 adapter: &'a ConsensusAdapter,
1112 start: Instant,
1113 submitted: bool,
1114 tx_type: &'static str,
1115 processed_method: ProcessedMethod,
1116 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 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
1211enum SequencingOutcome {
1213 Sequenced(Vec<ConsensusTxStatus>),
1216 BlockSequenced,
1218 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}