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};
52
53#[cfg(test)]
54#[path = "unit_tests/consensus_tests.rs"]
55pub mod consensus_tests;
56
57#[derive(Clone)]
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 epoch_store.record_submitted_user_transactions(&transactions, submitter_client_addr);
475
476 let is_soft_bundle = transactions.len() > 1;
481 let is_system_message = !transactions[0].is_user_transaction();
482
483 let mut transaction_keys = Vec::new();
484 let mut tx_consensus_positions = tx_consensus_positions;
485
486 for transaction in &transactions {
487 if matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..)) {
488 info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
489 epoch_store.record_epoch_pending_certs_process_time_metric();
490 }
491
492 let transaction_key = SequencedConsensusTransactionKey::External(transaction.key());
493 transaction_keys.push(transaction_key);
494 }
495 let tx_type = tx_type_label(&transactions);
496 tracing::Span::current().record("tx_type", tx_type);
497 tracing::Span::current().record("tx_keys", tracing::field::debug(&transaction_keys));
498
499 let mut guard = InflightDropGuard::acquire(&self, tx_type, transactions.len() as u64);
500
501 let make_processing_error =
502 |method: ProcessedMethod| -> SuiError { processing_error(&transaction_keys, method) };
503
504 let already_processed =
507 self.check_processed_via_consensus_or_checkpoint(&transaction_keys, epoch_store);
508 if let Some(method) = already_processed {
509 guard.processed_method = method;
510 if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
511 let _ = tx_consensus_positions.send(Err(make_processing_error(method)));
512 }
513 }
514
515 let _monitor = if matches!(
517 transactions[0].kind,
518 ConsensusTransactionKind::EndOfPublish(_)
519 | ConsensusTransactionKind::CapabilityNotification(_)
520 | ConsensusTransactionKind::CapabilityNotificationV2(_)
521 | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
522 | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
523 ) {
524 assert!(
525 !is_soft_bundle,
526 "System transactions should have been submitted individually"
527 );
528 let transaction_keys = transaction_keys.clone();
529 Some(CancelOnDrop(spawn_monitored_task!(async {
530 let mut i = 0u64;
531 loop {
532 i += 1;
533 const WARN_DELAY_S: u64 = 30;
534 tokio::time::sleep(Duration::from_secs(WARN_DELAY_S)).await;
535 let total_wait = i * WARN_DELAY_S;
536 warn!(
537 "Still waiting {} seconds for transactions {:?} to commit in consensus",
538 total_wait, transaction_keys
539 );
540 }
541 })))
542 } else {
543 None
544 };
545
546 if already_processed.is_none() {
547 debug!("Submitting {:?} to consensus", transaction_keys);
548 guard.submitted = true;
549
550 let _permit: Option<SemaphorePermit> = if is_system_message {
554 None
555 } else {
556 Some(
557 self.submit_semaphore
558 .acquire()
559 .count_in_flight(self.metrics.sequencing_in_flight_semaphore_wait.clone())
560 .await
561 .expect("Consensus adapter does not close semaphore"),
562 )
563 };
564 let _in_flight_submission_guard =
565 GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
566
567 let submit_fut = async {
570 const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
571
572 loop {
573 let (consensus_positions, status_waiter) = self
575 .submit_inner(&transactions, epoch_store, &transaction_keys, tx_type)
576 .await;
577
578 if let Some(tx_consensus_positions) = tx_consensus_positions.take() {
579 tracing::Span::current().record(
580 "consensus_positions",
581 tracing::field::debug(&consensus_positions),
582 );
583 let _ = tx_consensus_positions.send(Ok(consensus_positions.clone()));
589 }
590
591 match status_waiter.await {
592 Ok(status @ BlockStatus::Sequenced(_)) => {
593 tracing::Span::current()
594 .record("status", tracing::field::debug(&status));
595 self.metrics
596 .sequencing_certificate_status
597 .with_label_values(&[tx_type, "sequenced"])
598 .inc();
599 debug!(
600 "Transaction {transaction_keys:?} has been sequenced by consensus."
601 );
602 if is_system_message {
603 break SequencingOutcome::BlockSequenced;
608 }
609 if consensus_positions.len() != transactions.len() {
610 debug_fatal!(
611 "Consensus client returned {} positions for {} transactions",
612 consensus_positions.len(),
613 transactions.len()
614 );
615 break SequencingOutcome::BlockSequenced;
616 }
617 match self
620 .wait_for_position_statuses(&consensus_positions, epoch_store)
621 .await
622 {
623 Some(statuses) => break SequencingOutcome::Sequenced(statuses),
624 None => {
625 debug!(
635 "Transaction {transaction_keys:?} status expired before being read. Ending submission."
636 );
637 self.metrics
638 .sequencing_certificate_status
639 .with_label_values(&[tx_type, "status_expired"])
640 .inc();
641 break SequencingOutcome::StatusExpired;
642 }
643 }
644 }
645 Ok(status @ BlockStatus::GarbageCollected(_)) => {
646 tracing::Span::current()
647 .record("status", tracing::field::debug(&status));
648 self.metrics
649 .sequencing_certificate_status
650 .with_label_values(&[tx_type, "garbage_collected"])
651 .inc();
652 debug!(
656 "Transaction {transaction_keys:?} was garbage collected before being sequenced. Will be retried."
657 );
658 time::sleep(RETRY_DELAY_STEP).await;
659 continue;
660 }
661 Err(err) => {
662 warn!(
663 "Error while waiting for status from consensus for transactions {transaction_keys:?}, with error {:?}. Will be retried.",
664 err
665 );
666 time::sleep(RETRY_DELAY_STEP).await;
667 continue;
668 }
669 }
670 }
671 };
672
673 let processed_waiter = self
679 .processed_notify(transaction_keys.clone(), epoch_store)
680 .boxed();
681 let processed_via_notify;
682 guard.processed_method = match select(processed_waiter, submit_fut.boxed()).await {
683 Either::Left((observed, _submit_fut)) => {
684 processed_via_notify = true;
685 observed
686 }
687 Either::Right((SequencingOutcome::Sequenced(statuses), _processed_waiter)) => {
688 processed_via_notify = false;
689 for status in statuses {
690 self.metrics
691 .sequencing_certificate_settled_status
692 .with_label_values(&[tx_type, status.metric_label()])
693 .inc();
694 }
695 ProcessedMethod::ConsensusStatusReceived
696 }
697 Either::Right((SequencingOutcome::StatusExpired, _processed_waiter)) => {
698 processed_via_notify = false;
699 ProcessedMethod::ConsensusStatusExpired
700 }
701 Either::Right((SequencingOutcome::BlockSequenced, processed_waiter)) => {
702 debug!("Submitted {transaction_keys:?} to consensus");
703 processed_via_notify = false;
704 processed_waiter.await
705 }
706 };
707 if processed_via_notify
712 && let Some(tx_consensus_positions) = tx_consensus_positions.take()
713 {
714 let _ =
715 tx_consensus_positions.send(Err(make_processing_error(guard.processed_method)));
716 }
717 }
718 debug!(
719 "{transaction_keys:?} processed via {}",
720 guard.processed_method.method_name()
721 );
722
723 self.metrics
724 .sequencing_certificate_success
725 .with_label_values(&[tx_type])
726 .inc();
727 }
728
729 #[instrument(name = "ConsensusAdapter::submit_inner", level = "trace", skip_all)]
730 async fn submit_inner(
731 self: &Arc<Self>,
732 transactions: &[ConsensusTransaction],
733 epoch_store: &Arc<AuthorityPerEpochStore>,
734 transaction_keys: &[SequencedConsensusTransactionKey],
735 tx_type: &str,
736 ) -> (Vec<ConsensusPosition>, BlockStatusReceiver) {
737 let ack_start = Instant::now();
738 let mut retries: u32 = 0;
739 let mut backoff = mysten_common::backoff::ExponentialBackoff::new(
740 Duration::from_millis(100),
741 Duration::from_secs(10),
742 );
743
744 let (consensus_positions, status_waiter) = loop {
745 let span = debug_span!("client_submit");
746 match self
747 .consensus_client
748 .submit(transactions, epoch_store)
749 .instrument(span)
750 .await
751 {
752 Err(err) => {
753 if cfg!(msim) || retries > 3 {
755 warn!(
756 "Failed to submit transactions {transaction_keys:?} to consensus: {err}. Retry #{retries}"
757 );
758 }
759 self.metrics
760 .sequencing_certificate_failures
761 .with_label_values(&[tx_type])
762 .inc();
763 retries += 1;
764
765 time::sleep(backoff.next().unwrap()).await;
766 }
767 Ok((consensus_positions, status_waiter)) => {
768 break (consensus_positions, status_waiter);
769 }
770 }
771 };
772
773 let bucket = match retries {
777 0..=10 => retries.to_string(), 11..=20 => "between_10_and_20".to_string(),
779 21..=50 => "between_20_and_50".to_string(),
780 51..=100 => "between_50_and_100".to_string(),
781 _ => "over_100".to_string(),
782 };
783
784 self.metrics
785 .sequencing_acknowledge_latency
786 .with_label_values(&[bucket.as_str(), tx_type])
787 .observe(ack_start.elapsed().as_secs_f64());
788
789 (consensus_positions, status_waiter)
790 }
791
792 fn check_processed_via_consensus_or_checkpoint(
801 self: &Arc<Self>,
802 transaction_keys: &[SequencedConsensusTransactionKey],
803 epoch_store: &Arc<AuthorityPerEpochStore>,
804 ) -> Option<ProcessedMethod> {
805 let mut seen_checkpoint = false;
806 for transaction_key in transaction_keys {
807 if epoch_store
810 .is_consensus_message_processed(transaction_key)
811 .expect("Storage error when checking consensus message processed")
812 {
813 self.metrics
814 .sequencing_certificate_processed
815 .with_label_values(&["consensus"])
816 .inc();
817 continue;
818 }
819
820 if let SequencedConsensusTransactionKey::External(ConsensusTransactionKey::Certificate(
823 digest,
824 )) = transaction_key
825 && epoch_store
826 .is_transaction_executed_in_checkpoint(digest)
827 .expect("Storage error when checking transaction executed in checkpoint")
828 {
829 self.metrics
830 .sequencing_certificate_processed
831 .with_label_values(&["checkpoint"])
832 .inc();
833 seen_checkpoint = true;
834 continue;
835 }
836
837 if let SequencedConsensusTransactionKey::External(
841 ConsensusTransactionKey::CheckpointSignature(_, seq)
842 | ConsensusTransactionKey::CheckpointSignatureV2(_, seq, _),
843 ) = transaction_key
844 && let Some(synced_seq) = self
845 .checkpoint_store
846 .get_highest_synced_checkpoint_seq_number()
847 .expect("Storage error when reading highest synced checkpoint")
848 && synced_seq >= *seq
849 {
850 self.metrics
851 .sequencing_certificate_processed
852 .with_label_values(&["synced_checkpoint"])
853 .inc();
854 seen_checkpoint = true;
855 continue;
856 }
857
858 return None;
860 }
861
862 if seen_checkpoint {
863 Some(ProcessedMethod::CheckpointExecuted)
864 } else {
865 Some(ProcessedMethod::ConsensusMessageProcessed)
866 }
867 }
868
869 async fn processed_notify(
876 self: &Arc<Self>,
877 transaction_keys: Vec<SequencedConsensusTransactionKey>,
878 epoch_store: &Arc<AuthorityPerEpochStore>,
879 ) -> ProcessedMethod {
880 let notifications = FuturesUnordered::new();
881 for transaction_key in transaction_keys {
882 let transaction_digests = match transaction_key {
883 SequencedConsensusTransactionKey::External(
884 ConsensusTransactionKey::Certificate(digest),
885 ) => vec![digest],
886 _ => vec![],
887 };
888
889 let checkpoint_synced_future = if let SequencedConsensusTransactionKey::External(
890 ConsensusTransactionKey::CheckpointSignature(_, checkpoint_sequence_number)
891 | ConsensusTransactionKey::CheckpointSignatureV2(_, checkpoint_sequence_number, _),
892 ) = transaction_key
893 {
894 Either::Left(
897 self.checkpoint_store
898 .notify_read_synced_checkpoint(checkpoint_sequence_number),
899 )
900 } else {
901 Either::Right(future::pending())
902 };
903
904 notifications.push(async move {
907 tokio::select! {
908 processed = epoch_store.consensus_messages_processed_notify(vec![transaction_key]) => {
909 processed.expect("Storage error when waiting for consensus message processed");
910 self.metrics.sequencing_certificate_processed.with_label_values(&["consensus"]).inc();
911 return ProcessedMethod::ConsensusMessageProcessed;
912 },
913 processed = epoch_store.transactions_executed_in_checkpoint_notify(transaction_digests), if !transaction_digests.is_empty() => {
914 processed.expect("Storage error when waiting for transaction executed in checkpoint");
915 self.metrics.sequencing_certificate_processed.with_label_values(&["checkpoint"]).inc();
916 }
917 _ = checkpoint_synced_future => {
918 self.metrics.sequencing_certificate_processed.with_label_values(&["synced_checkpoint"]).inc();
919 }
920 }
921 ProcessedMethod::CheckpointExecuted
922 });
923 }
924
925 let processed_methods = notifications.collect::<Vec<ProcessedMethod>>().await;
926 for method in processed_methods {
927 if method == ProcessedMethod::CheckpointExecuted {
928 return ProcessedMethod::CheckpointExecuted;
929 }
930 }
931 ProcessedMethod::ConsensusMessageProcessed
932 }
933
934 async fn wait_for_position_statuses(
939 &self,
940 consensus_positions: &[ConsensusPosition],
941 epoch_store: &Arc<AuthorityPerEpochStore>,
942 ) -> Option<Vec<ConsensusTxStatus>> {
943 join_all(consensus_positions.iter().map(|position| {
944 epoch_store
945 .consensus_tx_status_cache
946 .notify_read_transaction_status(*position)
947 }))
948 .await
949 .into_iter()
950 .map(|result| match result {
951 NotifyReadConsensusTxStatusResult::Status(status) => Some(status),
952 NotifyReadConsensusTxStatusResult::Expired(_) => None,
953 })
954 .collect()
955 }
956}
957
958impl ConsensusOverloadChecker for ConsensusAdapter {
959 fn check_consensus_overload(&self) -> SuiResult {
960 fp_ensure!(
961 self.check_limits(),
962 SuiErrorKind::TooManyTransactionsPendingConsensus.into()
963 );
964 Ok(())
965 }
966}
967
968pub struct NoopConsensusOverloadChecker {}
969
970impl ConsensusOverloadChecker for NoopConsensusOverloadChecker {
971 fn check_consensus_overload(&self) -> SuiResult {
972 Ok(())
973 }
974}
975
976impl ReconfigurationInitiator for Arc<ConsensusAdapter> {
977 fn close_epoch(&self, epoch_store: &Arc<AuthorityPerEpochStore>) {
981 {
982 let reconfig_guard = epoch_store.get_reconfig_state_write_lock_guard();
983 if !reconfig_guard.should_accept_user_certs() {
984 return;
986 }
987 epoch_store.close_user_certs_for_manual_epoch_close(reconfig_guard);
988 }
989 if epoch_store.should_send_end_of_publish() {
990 if let Err(err) = self.submit(
991 ConsensusTransaction::new_end_of_publish(self.authority),
992 None,
993 epoch_store,
994 None,
995 None,
996 ) {
997 warn!("Error when sending end of publish message: {:?}", err);
998 } else {
999 info!(epoch=?epoch_store.epoch(), "Sending EndOfPublish message to consensus");
1000 }
1001 }
1002 }
1003}
1004
1005impl SubmitToConsensus for Arc<ConsensusAdapter> {
1006 fn submit_to_consensus(
1007 &self,
1008 transactions: &[ConsensusTransaction],
1009 epoch_store: &Arc<AuthorityPerEpochStore>,
1010 ) -> SuiResult {
1011 self.submit_batch(transactions, None, epoch_store, None, None)
1012 .map(|_| ())
1013 }
1014
1015 fn submit_best_effort(
1016 &self,
1017 transaction: &ConsensusTransaction,
1018 epoch_store: &Arc<AuthorityPerEpochStore>,
1019 timeout: Duration,
1021 ) -> SuiResult {
1022 if transaction.is_user_transaction() {
1023 debug_fatal!("submit_best_effort called with a user transaction");
1024 return Err(SuiErrorKind::GenericAuthorityError {
1025 error: "submit_best_effort does not accept user transactions".to_string(),
1026 }
1027 .into());
1028 }
1029
1030 let _in_flight_submission_guard =
1032 GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
1033
1034 let key = SequencedConsensusTransactionKey::External(transaction.key());
1035 let tx_type = classify(transaction);
1036
1037 let async_stage = {
1038 let transaction = transaction.clone();
1039 let epoch_store = epoch_store.clone();
1040 let this = self.clone();
1041
1042 async move {
1043 let result = tokio::time::timeout(
1044 timeout,
1045 this.submit_inner(&[transaction], &epoch_store, &[key], tx_type),
1046 )
1047 .await;
1048
1049 if let Err(e) = result {
1050 warn!("Consensus submission timed out: {e:?}");
1051 this.metrics
1052 .sequencing_best_effort_timeout
1053 .with_label_values(&[tx_type])
1054 .inc();
1055 }
1056 }
1057 };
1058
1059 let epoch_store = epoch_store.clone();
1060 spawn_monitored_task!(epoch_store.within_alive_epoch(async_stage));
1061 Ok(())
1062 }
1063}
1064
1065struct CancelOnDrop<T>(JoinHandle<T>);
1066
1067impl<T> Deref for CancelOnDrop<T> {
1068 type Target = JoinHandle<T>;
1069
1070 fn deref(&self) -> &Self::Target {
1071 &self.0
1072 }
1073}
1074
1075impl<T> Drop for CancelOnDrop<T> {
1076 fn drop(&mut self) {
1077 self.0.abort();
1078 }
1079}
1080
1081struct InflightDropGuard<'a> {
1083 adapter: &'a ConsensusAdapter,
1084 start: Instant,
1085 submitted: bool,
1086 tx_type: &'static str,
1087 processed_method: ProcessedMethod,
1088 inflight_count: u64,
1091}
1092
1093impl<'a> InflightDropGuard<'a> {
1094 pub fn acquire(
1095 adapter: &'a ConsensusAdapter,
1096 tx_type: &'static str,
1097 inflight_count: u64,
1098 ) -> Self {
1099 adapter
1100 .num_inflight_transactions
1101 .fetch_add(inflight_count, Ordering::SeqCst);
1102 adapter
1103 .metrics
1104 .sequencing_certificate_inflight
1105 .with_label_values(&[tx_type])
1106 .inc();
1107 adapter
1108 .metrics
1109 .sequencing_certificate_attempt
1110 .with_label_values(&[tx_type])
1111 .inc();
1112 Self {
1113 adapter,
1114 start: Instant::now(),
1115 submitted: false,
1116 tx_type,
1117 processed_method: ProcessedMethod::ConsensusMessageProcessed,
1118 inflight_count,
1119 }
1120 }
1121}
1122
1123impl Drop for InflightDropGuard<'_> {
1124 fn drop(&mut self) {
1125 self.adapter
1126 .num_inflight_transactions
1127 .fetch_sub(self.inflight_count, Ordering::SeqCst);
1128 self.adapter
1129 .metrics
1130 .sequencing_certificate_inflight
1131 .with_label_values(&[self.tx_type])
1132 .dec();
1133 self.adapter.inflight_slot_freed_notify.notify_one();
1135
1136 let latency = self.start.elapsed();
1137 let submitted = if self.submitted {
1138 "submitted"
1139 } else {
1140 "skipped"
1141 };
1142
1143 self.adapter
1144 .metrics
1145 .sequencing_certificate_latency
1146 .with_label_values(&[
1147 submitted,
1148 self.tx_type,
1149 self.processed_method.metric_label(),
1150 ])
1151 .observe(latency.as_secs_f64());
1152 }
1153}
1154
1155pub(crate) fn processing_error<'a>(
1161 keys: impl IntoIterator<Item = &'a SequencedConsensusTransactionKey>,
1162 method: ProcessedMethod,
1163) -> SuiError {
1164 let digest = keys
1165 .into_iter()
1166 .find_map(SequencedConsensusTransactionKey::user_transaction_digest)
1167 .unwrap_or_default();
1168 SuiErrorKind::TransactionProcessing {
1169 digest,
1170 status: format!("processed via {}", method.method_name()),
1171 }
1172 .into()
1173}
1174
1175#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1178pub(crate) enum ProcessedMethod {
1179 ConsensusMessageProcessed,
1180 ConsensusStatusReceived,
1181 ConsensusStatusExpired,
1182 CheckpointExecuted,
1183}
1184
1185impl ProcessedMethod {
1186 fn method_name(self) -> &'static str {
1187 match self {
1188 ProcessedMethod::ConsensusMessageProcessed => "consensus (processed message)",
1189 ProcessedMethod::ConsensusStatusReceived => "consensus (transaction status)",
1190 ProcessedMethod::ConsensusStatusExpired => "consensus (status expired)",
1191 ProcessedMethod::CheckpointExecuted => "checkpoint execution",
1192 }
1193 }
1194
1195 pub(crate) fn metric_label(self) -> &'static str {
1196 match self {
1197 ProcessedMethod::ConsensusMessageProcessed => "consensus_message",
1198 ProcessedMethod::ConsensusStatusReceived => "consensus_status",
1199 ProcessedMethod::ConsensusStatusExpired => "consensus_status_expired",
1200 ProcessedMethod::CheckpointExecuted => "checkpoint_execution",
1201 }
1202 }
1203}
1204
1205enum SequencingOutcome {
1207 Sequenced(Vec<ConsensusTxStatus>),
1210 BlockSequenced,
1212 StatusExpired,
1216}