1use std::{
6 collections::HashMap,
7 future::Future,
8 ops::Deref,
9 sync::{
10 Arc,
11 atomic::{AtomicU64, Ordering},
12 },
13 time::Instant,
14};
15
16use arc_swap::ArcSwap;
17use dashmap::{DashMap, try_result::TryResult};
18use futures::{
19 FutureExt, StreamExt,
20 future::{self, Either, select},
21 pin_mut,
22 stream::FuturesUnordered,
23};
24use iota_metrics::{GaugeGuard, InflightGuardFutureExt, LATENCY_SEC_BUCKETS, spawn_monitored_task};
25use iota_sdk_types::TransactionDigest;
26use iota_simulator::anemo::PeerId;
27use iota_types::{
28 base_types::AuthorityName,
29 committee::Committee,
30 error::{IotaError, IotaResult},
31 fp_ensure,
32 messages_consensus::{ConsensusTransaction, ConsensusTransactionKey, ConsensusTransactionKind},
33};
34use itertools::Itertools;
35use parking_lot::RwLockReadGuard;
36use prometheus_filtered::{
37 Histogram, HistogramVec, IntCounterVec, IntGauge, IntGaugeVec, MetricLevel, Registry,
38 register_histogram_vec_with_registry, register_histogram_with_registry,
39 register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry,
40 register_int_gauge_with_registry,
41};
42use tokio::{
43 sync::{Semaphore, SemaphorePermit, oneshot},
44 task::JoinHandle,
45 time::{
46 Duration, {self},
47 },
48};
49use tracing::{debug, info, trace, warn};
50
51use crate::{
52 authority::authority_per_epoch_store::AuthorityPerEpochStore,
53 checkpoints::CheckpointStore,
54 connection_monitor::ConnectionStatus,
55 consensus_handler::{SequencedConsensusTransactionKey, classify},
56 epoch::reconfiguration::{ReconfigState, ReconfigurationInitiator},
57 metrics::LatencyObserver,
58};
59
60#[cfg(test)]
61#[path = "unit_tests/consensus_tests.rs"]
62pub mod consensus_tests;
63
64const SEQUENCING_CERTIFICATE_POSITION_BUCKETS: &[f64] = &[
65 0., 1., 2., 3., 5., 10., 15., 20., 25., 30., 50., 100., 150., 200.,
66];
67
68pub struct ConsensusAdapterMetrics {
69 pub sequencing_certificate_attempt: IntCounterVec,
71 pub sequencing_certificate_success: IntCounterVec,
72 pub sequencing_certificate_failures: IntCounterVec,
73 pub sequencing_certificate_status: IntCounterVec,
74 pub sequencing_certificate_inflight: IntGaugeVec,
75 pub sequencing_acknowledge_latency: HistogramVec,
76 pub sequencing_certificate_latency: HistogramVec,
77 pub sequencing_certificate_authority_position: Histogram,
78 pub sequencing_certificate_positions_moved: Histogram,
79 pub sequencing_certificate_preceding_disconnected: Histogram,
80 pub sequencing_certificate_processed: IntCounterVec,
81 pub sequencing_in_flight_semaphore_wait: IntGauge,
82 pub sequencing_in_flight_submissions: IntGauge,
83 pub sequencing_estimated_latency: IntGauge,
84 pub sequencing_resubmission_interval_ms: IntGauge,
85}
86
87impl ConsensusAdapterMetrics {
88 pub fn new(registry: &Registry) -> Self {
89 Self {
90 sequencing_certificate_attempt: register_int_counter_vec_with_registry!(
91 "sequencing_certificate_attempt",
92 "Counts the number of certificates the validator attempts to sequence.",
93 &["tx_type"],
94 registry;
95 MetricLevel::Warn,
96 )
97 .unwrap(),
98 sequencing_certificate_success: register_int_counter_vec_with_registry!(
99 "sequencing_certificate_success",
100 "Counts the number of successfully sequenced certificates.",
101 &["tx_type"],
102 registry,
103 )
104 .unwrap(),
105 sequencing_certificate_failures: register_int_counter_vec_with_registry!(
106 "sequencing_certificate_failures",
107 "Counts the number of sequenced certificates that failed other than by timeout.",
108 &["tx_type"],
109 registry,
110 )
111 .unwrap(),
112 sequencing_certificate_status: register_int_counter_vec_with_registry!(
113 "sequencing_certificate_status",
114 "The status of the certificate sequencing as reported by consensus. The status can be either sequenced or garbage collected.",
115 &["tx_type", "status"],
116 registry;
117 MetricLevel::Warn,
118 )
119 .unwrap(),
120 sequencing_certificate_inflight: register_int_gauge_vec_with_registry!(
121 "sequencing_certificate_inflight",
122 "The inflight requests to sequence certificates.",
123 &["tx_type"],
124 registry;
125 MetricLevel::Warn,
126 )
127 .unwrap(),
128 sequencing_acknowledge_latency: register_histogram_vec_with_registry!(
129 "sequencing_acknowledge_latency",
130 "The latency for acknowledgement from sequencing engine. The overall sequencing latency is measured by the sequencing_certificate_latency metric",
131 &["retry", "tx_type"],
132 LATENCY_SEC_BUCKETS.to_vec(),
133 registry,
134 )
135 .unwrap(),
136 sequencing_certificate_latency: register_histogram_vec_with_registry!(
137 "sequencing_certificate_latency",
138 "The latency for sequencing a certificate.",
139 &["position", "tx_type", "processed_method"],
140 LATENCY_SEC_BUCKETS.to_vec(),
141 registry;
142 MetricLevel::Warn,
143 )
144 .unwrap(),
145 sequencing_certificate_authority_position: register_histogram_with_registry!(
146 "sequencing_certificate_authority_position",
147 "The position of the authority when submitted a certificate to consensus.",
148 SEQUENCING_CERTIFICATE_POSITION_BUCKETS.to_vec(),
149 registry,
150 )
151 .unwrap(),
152 sequencing_certificate_positions_moved: register_histogram_with_registry!(
153 "sequencing_certificate_positions_moved",
154 "The number of authorities ahead of ourselves that were filtered out when submitting a certificate to consensus.",
155 SEQUENCING_CERTIFICATE_POSITION_BUCKETS.to_vec(),
156 registry,
157 )
158 .unwrap(),
159 sequencing_certificate_preceding_disconnected: register_histogram_with_registry!(
160 "sequencing_certificate_preceding_disconnected",
161 "The number of authorities that were hashed to an earlier position that were filtered out due to being disconnected when submitting to consensus.",
162 SEQUENCING_CERTIFICATE_POSITION_BUCKETS.to_vec(),
163 registry,
164 )
165 .unwrap(),
166 sequencing_certificate_processed: register_int_counter_vec_with_registry!(
167 "sequencing_certificate_processed",
168 "The number of certificates that have been processed either by consensus or checkpoint.",
169 &["source"],
170 registry;
171 MetricLevel::Warn,
172 )
173 .unwrap(),
174 sequencing_in_flight_semaphore_wait: register_int_gauge_with_registry!(
175 "sequencing_in_flight_semaphore_wait",
176 "How many requests are blocked on submit_permit.",
177 registry,
178 )
179 .unwrap(),
180 sequencing_in_flight_submissions: register_int_gauge_with_registry!(
181 "sequencing_in_flight_submissions",
182 "Number of transactions submitted to local consensus instance and not yet sequenced",
183 registry,
184 )
185 .unwrap(),
186 sequencing_estimated_latency: register_int_gauge_with_registry!(
187 "sequencing_estimated_latency",
188 "Consensus latency estimated by consensus adapter in milliseconds",
189 registry,
190 )
191 .unwrap(),
192 sequencing_resubmission_interval_ms: register_int_gauge_with_registry!(
193 "sequencing_resubmission_interval_ms",
194 "Resubmission interval used by consensus adapter in milliseconds",
195 registry,
196 )
197 .unwrap(),
198 }
199 }
200
201 pub fn new_test() -> Self {
202 Self::new(&Registry::default())
203 }
204}
205
206pub enum BlockStatusInternal {
208 Sequenced,
209 GarbageCollected,
210}
211
212impl From<starfish_core::BlockStatus> for BlockStatusInternal {
213 fn from(status: starfish_core::BlockStatus) -> Self {
214 match status {
215 starfish_core::BlockStatus::Sequenced(_) => BlockStatusInternal::Sequenced,
216 starfish_core::BlockStatus::GarbageCollected(_) => {
217 BlockStatusInternal::GarbageCollected
218 }
219 }
220 }
221}
222
223pub type BlockStatusReceiver = oneshot::Receiver<BlockStatusInternal>;
224
225#[mockall::automock]
226pub trait SubmitToConsensus: Sync + Send + 'static {
227 fn submit_to_consensus(
228 &self,
229 transactions: &[ConsensusTransaction],
230 epoch_store: &Arc<AuthorityPerEpochStore>,
231 ) -> IotaResult;
232}
233
234#[mockall::automock]
235#[async_trait::async_trait]
236pub trait ConsensusClient: Sync + Send + 'static {
237 async fn submit(
238 &self,
239 transactions: &[ConsensusTransaction],
240 epoch_store: &Arc<AuthorityPerEpochStore>,
241 ) -> IotaResult<BlockStatusReceiver>;
242}
243
244pub struct ConsensusAdapter {
246 consensus_client: Arc<dyn ConsensusClient>,
248
249 checkpoint_store: Arc<CheckpointStore>,
251
252 authority: AuthorityName,
254
255 max_pending_transactions: usize,
262
263 num_inflight_transactions: AtomicU64,
265
266 max_submit_position: Option<usize>,
270
271 submit_delay_step_override: Option<Duration>,
274
275 connection_monitor_status: Arc<dyn CheckConnection>,
278
279 low_scoring_authorities: ArcSwap<Arc<ArcSwap<HashMap<AuthorityName, u64>>>>,
281
282 metrics: ConsensusAdapterMetrics,
284
285 submit_semaphore: Semaphore,
287
288 latency_observer: LatencyObserver,
290
291 graduated_load_shedding_soft_limit_pct: u32,
297}
298
299pub trait CheckConnection: Send + Sync {
300 fn check_connection(
301 &self,
302 ourself: &AuthorityName,
303 authority: &AuthorityName,
304 ) -> Option<ConnectionStatus>;
305 fn update_mapping_for_epoch(&self, authority_names_to_peer_ids: HashMap<AuthorityName, PeerId>);
306}
307
308pub struct ConnectionMonitorStatus {
309 pub connection_statuses: Arc<DashMap<PeerId, ConnectionStatus>>,
311 pub authority_names_to_peer_ids: ArcSwap<HashMap<AuthorityName, PeerId>>,
313}
314
315pub struct ConnectionMonitorStatusForTests {}
316
317impl ConsensusAdapter {
318 pub fn new(
320 consensus_client: Arc<dyn ConsensusClient>,
321 checkpoint_store: Arc<CheckpointStore>,
322 authority: AuthorityName,
323 connection_monitor_status: Arc<dyn CheckConnection>,
324 max_pending_transactions: usize,
325 max_pending_local_submissions: usize,
326 max_submit_position: Option<usize>,
327 submit_delay_step_override: Option<Duration>,
328 metrics: ConsensusAdapterMetrics,
329 graduated_load_shedding_soft_limit_pct: u32,
330 ) -> Self {
331 let num_inflight_transactions = Default::default();
332 let low_scoring_authorities =
333 ArcSwap::from_pointee(Arc::new(ArcSwap::from_pointee(HashMap::new())));
334 Self {
335 consensus_client,
336 checkpoint_store,
337 authority,
338 max_pending_transactions,
339 max_submit_position,
340 submit_delay_step_override,
341 num_inflight_transactions,
342 connection_monitor_status,
343 low_scoring_authorities,
344 metrics,
345 submit_semaphore: Semaphore::new(max_pending_local_submissions),
346 latency_observer: LatencyObserver::new(),
347 graduated_load_shedding_soft_limit_pct,
348 }
349 }
350
351 #[cfg(test)]
354 pub fn new_for_testing_with_authority_name(authority_name: AuthorityName) -> Self {
355 Self::new(
356 Arc::new(MockConsensusClient::new()),
357 CheckpointStore::new_for_tests(),
358 authority_name,
359 Arc::new(ConnectionMonitorStatusForTests {}),
360 100_000,
361 100_000,
362 None,
363 None,
364 ConsensusAdapterMetrics::new_test(),
365 50,
366 )
367 }
368
369 pub fn swap_low_scoring_authorities(
370 &self,
371 new_low_scoring: Arc<ArcSwap<HashMap<AuthorityName, u64>>>,
372 ) {
373 self.low_scoring_authorities.swap(Arc::new(new_low_scoring));
374 }
375
376 pub fn submit_recovered(self: &Arc<Self>, epoch_store: &Arc<AuthorityPerEpochStore>) {
377 let mut recovered = epoch_store.get_all_pending_consensus_transactions();
383
384 let is_pending_consensus_certificates_empty =
385 if epoch_store.protocol_config().enable_pcool_flow() {
386 true
389 } else {
390 epoch_store.pending_consensus_certificates_empty()
391 };
392
393 if epoch_store
394 .get_reconfig_state_read_lock_guard()
395 .is_reject_user_certs()
396 && is_pending_consensus_certificates_empty
397 {
398 if !recovered
400 .iter()
401 .any(ConsensusTransaction::is_end_of_publish)
402 {
403 recovered.push(ConsensusTransaction::new_end_of_publish(self.authority));
412 }
413 }
414 debug!(
415 "Submitting {:?} recovered pending consensus transactions to consensus",
416 recovered.len()
417 );
418 for transaction in recovered {
419 if transaction.is_end_of_publish() {
420 info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
421 }
422 self.submit_unchecked(&[transaction], epoch_store);
423 }
424 }
425
426 fn await_submit_delay(
427 &self,
428 committee: &Committee,
429 transactions: &[ConsensusTransaction],
430 ) -> (impl Future<Output = ()>, usize, usize, usize) {
431 let min_digest = transactions
433 .iter()
434 .filter_map(|tx| match &tx.kind {
435 ConsensusTransactionKind::CertifiedTransaction(certificate) => {
436 Some(certificate.digest())
437 }
438 ConsensusTransactionKind::UserTransactionV1(_) => {
439 None
442 }
443 _ => None,
444 })
445 .min();
446
447 let (duration, position, positions_moved, preceding_disconnected) = match min_digest {
448 Some(digest) => self.await_submit_delay_user_transaction(committee, digest),
449 _ => (Duration::ZERO, 0, 0, 0),
450 };
451 (
452 tokio::time::sleep(duration),
453 position,
454 positions_moved,
455 preceding_disconnected,
456 )
457 }
458
459 fn await_submit_delay_user_transaction(
460 &self,
461 committee: &Committee,
462 tx_digest: &TransactionDigest,
463 ) -> (Duration, usize, usize, usize) {
464 let (position, positions_moved, preceding_disconnected) =
465 self.submission_position(committee, tx_digest);
466
467 const DEFAULT_LATENCY: Duration = Duration::from_secs(1); const MIN_LATENCY: Duration = Duration::from_millis(150);
469 const MAX_LATENCY: Duration = Duration::from_secs(3);
470
471 let latency = self.latency_observer.latency().unwrap_or(DEFAULT_LATENCY);
472 self.metrics
473 .sequencing_estimated_latency
474 .set(latency.as_millis() as i64);
475
476 let latency = std::cmp::max(latency, MIN_LATENCY);
477 let latency = std::cmp::min(latency, MAX_LATENCY);
478 let latency = latency * 2;
479 let (delay_step, position) =
480 self.override_by_max_submit_position_settings(latency, position);
481
482 self.metrics
483 .sequencing_resubmission_interval_ms
484 .set(delay_step.as_millis() as i64);
485
486 (
487 delay_step * position as u32,
488 position,
489 positions_moved,
490 preceding_disconnected,
491 )
492 }
493
494 fn override_by_max_submit_position_settings(
500 &self,
501 latency: Duration,
502 mut position: usize,
503 ) -> (Duration, usize) {
504 if let Some(max_submit_position) = self.max_submit_position {
506 position = std::cmp::min(position, max_submit_position);
507 }
508
509 let delay_step = self.submit_delay_step_override.unwrap_or(latency);
510 (delay_step, position)
511 }
512
513 fn submission_position(
523 &self,
524 committee: &Committee,
525 tx_digest: &TransactionDigest,
526 ) -> (usize, usize, usize) {
527 let positions = committee.shuffle_by_stake_from_tx_digest(tx_digest);
528
529 self.check_submission_wrt_connectivity_and_scores(positions)
530 }
531
532 fn check_submission_wrt_connectivity_and_scores(
559 &self,
560 positions: Vec<AuthorityName>,
561 ) -> (usize, usize, usize) {
562 let low_scoring_authorities = self.low_scoring_authorities.load().load_full();
563 if low_scoring_authorities.get(&self.authority).is_some() {
564 return (positions.len(), 0, 0);
565 }
566 let initial_position = get_position_in_list(self.authority, positions.clone());
567 let mut preceding_disconnected = 0;
568 let mut before_our_position = true;
569
570 let filtered_positions: Vec<_> = positions
571 .into_iter()
572 .filter(|authority| {
573 let keep = self.authority == *authority; if keep {
575 before_our_position = false;
576 }
577
578 let connected = self
580 .connection_monitor_status
581 .check_connection(&self.authority, authority)
582 .unwrap_or(ConnectionStatus::Disconnected)
583 == ConnectionStatus::Connected;
584 if !connected && before_our_position {
585 preceding_disconnected += 1; }
587
588 let high_scoring = low_scoring_authorities.get(authority).is_none();
590
591 keep || (connected && high_scoring)
592 })
593 .collect();
594
595 let position = get_position_in_list(self.authority, filtered_positions);
596
597 (
598 position,
599 initial_position - position,
600 preceding_disconnected,
601 )
602 }
603
604 pub fn submit(
615 self: &Arc<Self>,
616 transaction: ConsensusTransaction,
617 lock: Option<&RwLockReadGuard<ReconfigState>>,
618 epoch_store: &Arc<AuthorityPerEpochStore>,
619 ) -> IotaResult<JoinHandle<()>> {
620 self.submit_batch(&[transaction], lock, epoch_store)
621 }
622
623 pub fn submit_batch(
624 self: &Arc<Self>,
625 transactions: &[ConsensusTransaction],
626 lock: Option<&RwLockReadGuard<ReconfigState>>,
627 epoch_store: &Arc<AuthorityPerEpochStore>,
628 ) -> IotaResult<JoinHandle<()>> {
629 if transactions.len() > 1 {
630 for transaction in transactions {
635 fp_ensure!(
640 transaction.is_user_certificate() || transaction.kind.is_user_transaction(),
641 IotaError::InvalidTxKindInSoftBundle
642 );
643 }
644 }
645
646 epoch_store.insert_pending_consensus_transactions(transactions, lock)?;
647
648 Ok(self.submit_unchecked(transactions, epoch_store))
649 }
650
651 pub(super) fn max_pending_transactions(&self) -> usize {
653 self.max_pending_transactions
654 }
655
656 pub(super) fn graduated_load_shedding_soft_limit_pct(&self) -> u32 {
661 self.graduated_load_shedding_soft_limit_pct
662 }
663
664 pub(super) fn num_inflight_transactions(&self) -> u64 {
666 self.num_inflight_transactions.load(Ordering::Relaxed)
667 }
668
669 #[cfg(test)]
671 pub(super) fn set_num_inflight_transactions_for_testing(&self, value: u64) {
672 self.num_inflight_transactions
673 .store(value, Ordering::Relaxed);
674 }
675
676 fn check_consensus_hard_limits(&self) -> bool {
682 if self.num_inflight_transactions.load(Ordering::Relaxed) as usize
690 > self.max_pending_transactions
691 {
692 return false;
693 }
694
695 self.submit_semaphore.available_permits() > 0
697 }
698
699 pub(crate) fn check_consensus_overload(&self) -> IotaResult {
702 fp_ensure!(
703 self.check_consensus_hard_limits(),
704 IotaError::TooManyTransactionsPendingConsensus
705 );
706
707 Ok(())
708 }
709
710 fn submit_unchecked(
711 self: &Arc<Self>,
712 transactions: &[ConsensusTransaction],
713 epoch_store: &Arc<AuthorityPerEpochStore>,
714 ) -> JoinHandle<()> {
715 let async_stage = self
718 .clone()
719 .submit_and_wait(transactions.to_vec(), epoch_store.clone());
720 let join_handle = spawn_monitored_task!(async_stage);
723 join_handle
724 }
725
726 async fn submit_and_wait(
727 self: Arc<Self>,
728 transactions: Vec<ConsensusTransaction>,
729 epoch_store: Arc<AuthorityPerEpochStore>,
730 ) {
731 epoch_store
745 .within_alive_epoch(self.submit_and_wait_inner(transactions, &epoch_store))
746 .await
747 .ok(); }
750
751 async fn submit_and_wait_inner(
752 self: Arc<Self>,
753 transactions: Vec<ConsensusTransaction>,
754 epoch_store: &Arc<AuthorityPerEpochStore>,
755 ) {
756 if transactions.is_empty() {
757 return;
758 }
759
760 let is_soft_bundle = transactions.len() > 1;
764
765 let mut transaction_keys = Vec::new();
766
767 for transaction in &transactions {
768 if matches!(transaction.kind, ConsensusTransactionKind::EndOfPublish(..)) {
769 info!(epoch=?epoch_store.epoch(), "Submitting EndOfPublish message to consensus");
770 epoch_store.record_epoch_pending_certs_process_time_metric();
771 }
772
773 let transaction_key = SequencedConsensusTransactionKey::External(transaction.key());
774 transaction_keys.push(transaction_key);
775 }
776 let tx_type = if !is_soft_bundle {
777 classify(&transactions[0])
778 } else {
779 "soft_bundle"
780 };
781
782 let mut guard = InflightDropGuard::acquire(&self, tx_type);
783
784 let (await_submit, position, positions_moved, preceding_disconnected) =
786 self.await_submit_delay(epoch_store.committee(), &transactions[..]);
787
788 let processed_via_consensus_or_checkpoint =
791 self.await_consensus_or_checkpoint(transaction_keys.clone(), epoch_store);
792 pin_mut!(processed_via_consensus_or_checkpoint);
793
794 let processed_waiter = tokio::select! {
795 _ = await_submit => Some(processed_via_consensus_or_checkpoint),
797
798 _ = epoch_store.user_certs_closed_notify() => {
800 warn!(epoch = ?epoch_store.epoch(), "Epoch ended, skipping submission delay");
801 Some(processed_via_consensus_or_checkpoint)
802 }
803
804 method = &mut processed_via_consensus_or_checkpoint => {
811 guard.processed_method = method;
812 None
813 }
814 };
815
816 let _monitor = if !is_soft_bundle
818 && matches!(
819 transactions[0].kind,
820 ConsensusTransactionKind::EndOfPublish(_)
821 | ConsensusTransactionKind::CapabilityNotificationV1(_)
822 | ConsensusTransactionKind::RandomnessDkgMessage(_, _)
823 | ConsensusTransactionKind::RandomnessDkgConfirmation(_, _)
824 | ConsensusTransactionKind::OverloadNotificationV1(_, _, _)
825 | ConsensusTransactionKind::TransactionDenyRuleProposal(_)
826 ) {
827 let transaction_keys = transaction_keys.clone();
828 Some(CancelOnDrop(spawn_monitored_task!(async {
829 let mut i = 0u64;
830 loop {
831 i += 1;
832 const WARN_DELAY_S: u64 = 30;
833 tokio::time::sleep(Duration::from_secs(WARN_DELAY_S)).await;
834 let total_wait = i * WARN_DELAY_S;
835 warn!(
836 "Still waiting {} seconds for transactions {:?} to commit in consensus",
837 total_wait, transaction_keys
838 );
839 }
840 })))
841 } else {
842 None
843 };
844
845 if let Some(processed_waiter) = processed_waiter {
846 debug!("Submitting {:?} to consensus", transaction_keys);
847
848 guard.position = Some(position);
851 guard.positions_moved = Some(positions_moved);
852 guard.preceding_disconnected = Some(preceding_disconnected);
853
854 let _permit: SemaphorePermit = self
855 .submit_semaphore
856 .acquire()
857 .count_in_flight(self.metrics.sequencing_in_flight_semaphore_wait.clone())
858 .await
859 .expect("Consensus adapter does not close semaphore");
860 let _in_flight_submission_guard =
861 GaugeGuard::acquire(&self.metrics.sequencing_in_flight_submissions);
862
863 let submit_inner = async {
867 const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
868
869 loop {
870 let status_waiter = self
873 .submit_inner(
874 &transactions,
875 epoch_store,
876 &transaction_keys,
877 tx_type,
878 is_soft_bundle,
879 )
880 .await;
881
882 match status_waiter.await {
883 Ok(BlockStatusInternal::Sequenced) => {
884 self.metrics
885 .sequencing_certificate_status
886 .with_label_values(&[tx_type, "sequenced"])
887 .inc();
888 trace!(
891 "Transaction {transaction_keys:?} has been sequenced by consensus."
892 );
893 break;
894 }
895 Ok(BlockStatusInternal::GarbageCollected) => {
896 self.metrics
897 .sequencing_certificate_status
898 .with_label_values(&[tx_type, "garbage_collected"])
899 .inc();
900 debug!(
908 "Transaction {transaction_keys:?} was garbage collected before being sequenced. Will be retried."
909 );
910 time::sleep(RETRY_DELAY_STEP).await;
911 continue;
912 }
913 Err(err) => {
914 warn!(
915 "Error while waiting for status from consensus for transactions {transaction_keys:?}, with error {:?}. Will be retried.",
916 err
917 );
918 time::sleep(RETRY_DELAY_STEP).await;
919 continue;
920 }
921 }
922 }
923 };
924
925 guard.processed_method = match select(processed_waiter, submit_inner.boxed()).await {
926 Either::Left((observed_via_consensus, _submit_inner)) => observed_via_consensus,
927 Either::Right(((), processed_waiter)) => {
928 debug!("Submitted {transaction_keys:?} to consensus");
929 processed_waiter.await
930 }
931 };
932 }
933 debug!("{transaction_keys:?} processed by consensus");
934
935 let consensus_keys: Vec<_> = transactions.iter().map(|t| t.key()).collect();
936 epoch_store
937 .remove_pending_consensus_transactions(&consensus_keys)
938 .expect("Storage error when removing consensus transaction");
939
940 let is_user_tx = is_soft_bundle
941 || if epoch_store.protocol_config().enable_pcool_flow() {
942 transactions[0].kind.is_user_transaction()
946 } else {
947 transactions[0].is_user_certificate()
950 };
951 let send_end_of_publish = if is_user_tx {
952 if epoch_store.protocol_config().enable_pcool_flow() {
953 false
960 } else {
961 if epoch_store
976 .get_reconfig_state_read_lock_guard()
977 .is_reject_user_certs()
978 {
979 let pending_count = epoch_store.pending_consensus_certificates_count();
980 debug!(epoch=?epoch_store.epoch(), ?pending_count, "Deciding whether to send EndOfPublish");
981
982 pending_count == 0 } else {
984 false
985 }
986 }
987 } else {
988 false
989 };
990 if send_end_of_publish {
991 let adapter = self.clone();
997 let epoch_store = epoch_store.clone();
998 spawn_monitored_task!(async move {
999 if epoch_store
1000 .within_alive_epoch(adapter.submit_end_of_publish_with_retry(&epoch_store))
1001 .await
1002 .is_err()
1003 {
1004 warn!(
1005 epoch = ?epoch_store.epoch(),
1006 "EndOfPublish submission cancelled: epoch has ended",
1007 );
1008 }
1009 });
1010 }
1011 self.metrics
1012 .sequencing_certificate_success
1013 .with_label_values(&[tx_type])
1014 .inc();
1015 }
1016
1017 async fn submit_inner(
1018 self: &Arc<Self>,
1019 transactions: &[ConsensusTransaction],
1020 epoch_store: &Arc<AuthorityPerEpochStore>,
1021 transaction_keys: &[SequencedConsensusTransactionKey],
1022 tx_type: &str,
1023 is_soft_bundle: bool,
1024 ) -> BlockStatusReceiver {
1025 let ack_start = Instant::now();
1026 let mut retries: u32 = 0;
1027
1028 let status_waiter = loop {
1029 match self
1030 .consensus_client
1031 .submit(transactions, epoch_store)
1032 .await
1033 {
1034 Err(err) => {
1035 if retries > 30
1038 || (retries > 3 && (is_soft_bundle || !transactions[0].kind.is_dkg()))
1039 {
1040 warn!(
1041 "Failed to submit transactions {transaction_keys:?} to consensus: {err:?}. Retry #{retries}"
1042 );
1043 }
1044 self.metrics
1045 .sequencing_certificate_failures
1046 .with_label_values(&[tx_type])
1047 .inc();
1048 retries += 1;
1049
1050 if !is_soft_bundle && transactions[0].kind.is_dkg() {
1051 time::sleep(Duration::from_millis(100)).await;
1054 } else {
1055 time::sleep(Duration::from_secs(10)).await;
1056 };
1057 }
1058 Ok(status_waiter) => {
1059 break status_waiter;
1060 }
1061 }
1062 };
1063
1064 let bucket = match retries {
1068 0..=10 => retries.to_string(), 11..=20 => "between_10_and_20".to_string(),
1070 21..=50 => "between_20_and_50".to_string(),
1071 51..=100 => "between_50_and_100".to_string(),
1072 _ => "over_100".to_string(),
1073 };
1074
1075 self.metrics
1076 .sequencing_acknowledge_latency
1077 .with_label_values(&[bucket.as_str(), tx_type])
1078 .observe(ack_start.elapsed().as_secs_f64());
1079
1080 status_waiter
1081 }
1082
1083 async fn await_consensus_or_checkpoint(
1088 self: &Arc<Self>,
1089 transaction_keys: Vec<SequencedConsensusTransactionKey>,
1090 epoch_store: &Arc<AuthorityPerEpochStore>,
1091 ) -> ProcessedMethod {
1092 let notifications = FuturesUnordered::new();
1093 for transaction_key in transaction_keys {
1094 let transaction_digests = match transaction_key {
1095 SequencedConsensusTransactionKey::External(
1096 ConsensusTransactionKey::Certificate(digest),
1097 )
1098 | SequencedConsensusTransactionKey::External(
1099 ConsensusTransactionKey::UserTransaction(digest),
1100 ) => vec![digest],
1101 _ => vec![],
1102 };
1103
1104 let checkpoint_synced_future = if let SequencedConsensusTransactionKey::External(
1105 ConsensusTransactionKey::CheckpointSignature(_, checkpoint_sequence_number),
1106 ) = transaction_key
1107 {
1108 Either::Left(
1113 self.checkpoint_store
1114 .notify_read_synced_checkpoint(checkpoint_sequence_number),
1115 )
1116 } else {
1117 Either::Right(future::pending())
1118 };
1119
1120 let dropped_digest = transaction_digests.first().copied();
1127 notifications.push(async move {
1128 tokio::select! {
1129 processed = epoch_store.consensus_messages_processed_notify(vec![transaction_key]) => {
1130 processed.expect("Storage error when waiting for consensus message processed");
1131 self.metrics.sequencing_certificate_processed.with_label_values(&["consensus"]).inc();
1132 return ProcessedMethod::Consensus;
1133 },
1134 processed = epoch_store.transactions_executed_in_checkpoint_notify(transaction_digests), if !transaction_digests.is_empty() => {
1135 processed.expect("Storage error when waiting for transaction executed in checkpoint");
1136 self.metrics.sequencing_certificate_processed.with_label_values(&["checkpoint"]).inc();
1137 }
1138 _ = checkpoint_synced_future => {
1139 self.metrics.sequencing_certificate_processed.with_label_values(&["synced_checkpoint"]).inc();
1140 }
1141 _ = async move {
1142 if let Some(d) = dropped_digest {
1143 let _err = epoch_store.notify_read_any_drop(d).await;
1144 } else {
1145 future::pending::<()>().await;
1146 }
1147 } => {
1148 self.metrics.sequencing_certificate_processed.with_label_values(&["dropped"]).inc();
1149 return ProcessedMethod::Dropped;
1150 }
1151 }
1152 ProcessedMethod::Checkpoint
1153 });
1154 }
1155
1156 let processed_methods = notifications.collect::<Vec<ProcessedMethod>>().await;
1157 if processed_methods.contains(&ProcessedMethod::Dropped) {
1158 ProcessedMethod::Dropped
1159 } else if processed_methods.contains(&ProcessedMethod::Checkpoint) {
1160 ProcessedMethod::Checkpoint
1161 } else {
1162 ProcessedMethod::Consensus
1163 }
1164 }
1165
1166 async fn submit_end_of_publish_with_retry(
1177 self: &Arc<Self>,
1178 epoch_store: &Arc<AuthorityPerEpochStore>,
1179 ) {
1180 const INITIAL_BACKOFF: Duration = Duration::from_millis(100);
1181 const MAX_BACKOFF: Duration = Duration::from_secs(10);
1182
1183 info!(
1184 epoch = ?epoch_store.epoch(),
1185 authority = ?self.authority,
1186 "Sending EndOfPublish message to consensus",
1187 );
1188
1189 let mut attempt: u32 = 0;
1190 loop {
1191 match self.submit(
1192 ConsensusTransaction::new_end_of_publish(self.authority),
1193 None,
1194 epoch_store,
1195 ) {
1196 Ok(_) => return,
1197 Err(IotaError::EpochEnded(_)) => {
1198 warn!(
1199 epoch = ?epoch_store.epoch(),
1200 authority = ?self.authority,
1201 "EndOfPublish submission stopped: epoch has ended",
1202 );
1203 return;
1204 }
1205 Err(err) => {
1206 let backoff = (INITIAL_BACKOFF * 2u32.pow(attempt.min(10))).min(MAX_BACKOFF);
1207 warn!(
1208 epoch = ?epoch_store.epoch(),
1209 authority = ?self.authority,
1210 attempt,
1211 "Failed to submit EndOfPublish, retrying in {:?}: {:?}",
1212 backoff,
1213 err,
1214 );
1215 tokio::time::sleep(backoff).await;
1216 attempt = attempt.saturating_add(1);
1217 }
1218 }
1219 }
1220 }
1221}
1222
1223impl CheckConnection for ConnectionMonitorStatus {
1224 fn check_connection(
1225 &self,
1226 ourself: &AuthorityName,
1227 authority: &AuthorityName,
1228 ) -> Option<ConnectionStatus> {
1229 if ourself == authority {
1230 return Some(ConnectionStatus::Connected);
1231 }
1232
1233 let mapping = self.authority_names_to_peer_ids.load_full();
1234 let peer_id = match mapping.get(authority) {
1235 Some(p) => p,
1236 None => {
1237 warn!(
1238 "failed to find peer {:?} in connection monitor listener",
1239 authority
1240 );
1241 return None;
1242 }
1243 };
1244
1245 let res = match self.connection_statuses.try_get(peer_id) {
1246 TryResult::Present(c) => Some(c.value().clone()),
1247 TryResult::Absent => None,
1248 TryResult::Locked => {
1249 Some(ConnectionStatus::Disconnected)
1251 }
1252 };
1253 res
1254 }
1255 fn update_mapping_for_epoch(
1256 &self,
1257 authority_names_to_peer_ids: HashMap<AuthorityName, PeerId>,
1258 ) {
1259 self.authority_names_to_peer_ids
1260 .swap(Arc::new(authority_names_to_peer_ids));
1261 }
1262}
1263
1264impl CheckConnection for ConnectionMonitorStatusForTests {
1265 fn check_connection(
1266 &self,
1267 _ourself: &AuthorityName,
1268 _authority: &AuthorityName,
1269 ) -> Option<ConnectionStatus> {
1270 Some(ConnectionStatus::Connected)
1271 }
1272 fn update_mapping_for_epoch(
1273 &self,
1274 _authority_names_to_peer_ids: HashMap<AuthorityName, PeerId>,
1275 ) {
1276 }
1277}
1278
1279pub fn get_position_in_list(
1280 search_authority: AuthorityName,
1281 positions: Vec<AuthorityName>,
1282) -> usize {
1283 positions
1284 .into_iter()
1285 .find_position(|authority| *authority == search_authority)
1286 .expect("Couldn't find ourselves in shuffled committee")
1287 .0
1288}
1289
1290impl ReconfigurationInitiator for Arc<ConsensusAdapter> {
1291 fn close_epoch(&self, epoch_store: &Arc<AuthorityPerEpochStore>) {
1298 let send_end_of_publish = {
1299 let reconfig_guard = epoch_store.get_reconfig_state_write_lock_guard();
1300 if !reconfig_guard.should_accept_user_certs() {
1301 return;
1303 }
1304
1305 let send_end_of_publish = if epoch_store.protocol_config().enable_pcool_flow() {
1306 debug!(epoch=?epoch_store.epoch(), "Closing epoch in P-COOL mode");
1309
1310 true
1311 } else {
1312 let pending_count = epoch_store.pending_consensus_certificates_count();
1315 debug!(epoch=?epoch_store.epoch(), ?pending_count, "Trying to close epoch");
1316
1317 pending_count == 0 };
1319
1320 epoch_store.close_user_certs(reconfig_guard);
1321
1322 send_end_of_publish
1323 };
1325
1326 if send_end_of_publish {
1327 let adapter = self.clone();
1334 let epoch_store = epoch_store.clone();
1335 spawn_monitored_task!(async move {
1336 if epoch_store
1337 .within_alive_epoch(adapter.submit_end_of_publish_with_retry(&epoch_store))
1338 .await
1339 .is_err()
1340 {
1341 warn!(
1342 epoch = ?epoch_store.epoch(),
1343 "EndOfPublish submission cancelled: epoch has ended",
1344 );
1345 }
1346 });
1347 }
1348 }
1349}
1350
1351struct CancelOnDrop<T>(JoinHandle<T>);
1352
1353impl<T> Deref for CancelOnDrop<T> {
1354 type Target = JoinHandle<T>;
1355
1356 fn deref(&self) -> &Self::Target {
1357 &self.0
1358 }
1359}
1360
1361impl<T> Drop for CancelOnDrop<T> {
1362 fn drop(&mut self) {
1363 self.0.abort();
1364 }
1365}
1366
1367struct InflightDropGuard<'a> {
1369 adapter: &'a ConsensusAdapter,
1370 start: Instant,
1371 position: Option<usize>,
1372 positions_moved: Option<usize>,
1373 preceding_disconnected: Option<usize>,
1374 tx_type: &'static str,
1375 processed_method: ProcessedMethod,
1376}
1377
1378#[derive(PartialEq, Eq, Copy, Clone)]
1379enum ProcessedMethod {
1380 Consensus,
1381 Checkpoint,
1382 Dropped,
1383}
1384
1385impl<'a> InflightDropGuard<'a> {
1386 pub fn acquire(adapter: &'a ConsensusAdapter, tx_type: &'static str) -> Self {
1387 adapter
1388 .num_inflight_transactions
1389 .fetch_add(1, Ordering::SeqCst);
1390 adapter
1391 .metrics
1392 .sequencing_certificate_inflight
1393 .with_label_values(&[tx_type])
1394 .inc();
1395 adapter
1396 .metrics
1397 .sequencing_certificate_attempt
1398 .with_label_values(&[tx_type])
1399 .inc();
1400 Self {
1401 adapter,
1402 start: Instant::now(),
1403 position: None,
1404 positions_moved: None,
1405 preceding_disconnected: None,
1406 tx_type,
1407 processed_method: ProcessedMethod::Consensus,
1408 }
1409 }
1410}
1411
1412impl Drop for InflightDropGuard<'_> {
1413 fn drop(&mut self) {
1414 self.adapter
1415 .num_inflight_transactions
1416 .fetch_sub(1, Ordering::SeqCst);
1417 self.adapter
1418 .metrics
1419 .sequencing_certificate_inflight
1420 .with_label_values(&[self.tx_type])
1421 .dec();
1422
1423 let position = if let Some(position) = self.position {
1424 self.adapter
1425 .metrics
1426 .sequencing_certificate_authority_position
1427 .observe(position as f64);
1428 position.to_string()
1429 } else {
1430 "not_submitted".to_string()
1431 };
1432
1433 if let Some(positions_moved) = self.positions_moved {
1434 self.adapter
1435 .metrics
1436 .sequencing_certificate_positions_moved
1437 .observe(positions_moved as f64);
1438 };
1439
1440 if let Some(preceding_disconnected) = self.preceding_disconnected {
1441 self.adapter
1442 .metrics
1443 .sequencing_certificate_preceding_disconnected
1444 .observe(preceding_disconnected as f64);
1445 };
1446
1447 let latency = self.start.elapsed();
1448 let processed_method = match self.processed_method {
1449 ProcessedMethod::Consensus => "processed_via_consensus",
1450 ProcessedMethod::Checkpoint => "processed_via_checkpoint",
1451 ProcessedMethod::Dropped => "dropped",
1452 };
1453 self.adapter
1454 .metrics
1455 .sequencing_certificate_latency
1456 .with_label_values(&[position.as_str(), self.tx_type, processed_method])
1457 .observe(latency.as_secs_f64());
1458
1459 if self.position == Some(0) {
1465 let sampled = matches!(
1468 self.tx_type,
1469 "shared_certificate" | "owned_certificate" | "checkpoint_signature" | "soft_bundle"
1470 );
1471 if sampled && self.processed_method == ProcessedMethod::Consensus {
1475 self.adapter.latency_observer.report(latency);
1476 }
1477 }
1478 }
1479}
1480
1481impl SubmitToConsensus for Arc<ConsensusAdapter> {
1482 fn submit_to_consensus(
1483 &self,
1484 transactions: &[ConsensusTransaction],
1485 epoch_store: &Arc<AuthorityPerEpochStore>,
1486 ) -> IotaResult {
1487 self.submit_batch(transactions, None, epoch_store)
1488 .map(|_| ())
1489 }
1490}
1491
1492pub fn position_submit_certificate(
1493 committee: &Committee,
1494 ourselves: &AuthorityName,
1495 tx_digest: &TransactionDigest,
1496) -> usize {
1497 let validators = committee.shuffle_by_stake_from_tx_digest(tx_digest);
1498 get_position_in_list(*ourselves, validators)
1499}
1500
1501#[cfg(test)]
1502mod adapter_tests {
1503 use std::{sync::Arc, time::Duration};
1504
1505 use fastcrypto::{
1506 bls12381::min_sig::BLS12381PrivateKey,
1507 traits::{KeyPair, ToFromBytes},
1508 };
1509 use iota_sdk_types::TransactionDigest;
1510 use iota_types::{
1511 committee::Committee,
1512 crypto::{AuthorityKeyPair, AuthorityPublicKeyBytes},
1513 };
1514
1515 use super::position_submit_certificate;
1516 use crate::{
1517 checkpoints::CheckpointStore,
1518 consensus_adapter::{
1519 ConnectionMonitorStatusForTests, ConsensusAdapter, ConsensusAdapterMetrics,
1520 },
1521 starfish_adapter::LazyStarfishClient,
1522 };
1523
1524 fn test_committee() -> Committee {
1528 const STAKES: [u64; 10] = [3, 7, 1, 9, 4, 4, 2, 8, 5, 6];
1529 let authorities = STAKES
1530 .iter()
1531 .enumerate()
1532 .map(|(i, stake)| {
1533 let key_pair = AuthorityKeyPair::from(
1534 BLS12381PrivateKey::from_bytes(&[i as u8 + 1; 32]).unwrap(),
1535 );
1536 (AuthorityPublicKeyBytes::from(key_pair.public()), *stake)
1537 })
1538 .collect();
1539 Committee::new_for_testing_with_normalized_voting_power(0, authorities)
1540 }
1541
1542 #[tokio::test]
1543 async fn test_await_submit_delay_user_transaction() {
1544 let committee = test_committee();
1545
1546 let consensus_adapter = ConsensusAdapter::new(
1548 Arc::new(LazyStarfishClient::new()),
1549 CheckpointStore::new_for_tests(),
1550 *committee.authority_by_index(0).unwrap(),
1551 Arc::new(ConnectionMonitorStatusForTests {}),
1552 100_000,
1553 100_000,
1554 Some(1),
1555 Some(Duration::from_secs(2)),
1556 ConsensusAdapterMetrics::new_test(),
1557 50,
1558 );
1559
1560 let tx_digest = TransactionDigest::new([7; 32]);
1562
1563 let (position, positions_moved, _) =
1565 consensus_adapter.submission_position(&committee, &tx_digest);
1566 assert_eq!(position, 8);
1567 assert_eq!(positions_moved, 0);
1568
1569 let (delay_step, position, positions_moved, _) =
1571 consensus_adapter.await_submit_delay_user_transaction(&committee, &tx_digest);
1572
1573 assert_eq!(position, 1);
1574 assert_eq!(delay_step, Duration::from_secs(2));
1575 assert_eq!(positions_moved, 0);
1576
1577 let consensus_adapter = ConsensusAdapter::new(
1579 Arc::new(LazyStarfishClient::new()),
1580 CheckpointStore::new_for_tests(),
1581 *committee.authority_by_index(0).unwrap(),
1582 Arc::new(ConnectionMonitorStatusForTests {}),
1583 100_000,
1584 100_000,
1585 None,
1586 None,
1587 ConsensusAdapterMetrics::new_test(),
1588 50,
1589 );
1590
1591 let (delay_step, position, positions_moved, _) =
1592 consensus_adapter.await_submit_delay_user_transaction(&committee, &tx_digest);
1593
1594 assert_eq!(position, 8);
1595
1596 assert_eq!(delay_step, Duration::from_secs(16));
1598 assert_eq!(positions_moved, 0);
1599 }
1600
1601 #[test]
1602 fn test_position_submit_certificate() {
1603 let committee = test_committee();
1604
1605 const NUM_TEST_TRANSACTIONS: u32 = 1000;
1607
1608 for tx_idx in 0..NUM_TEST_TRANSACTIONS {
1609 let mut digest_bytes = [0u8; 32];
1610 digest_bytes[..4].copy_from_slice(&tx_idx.to_le_bytes());
1611 let tx_digest = TransactionDigest::new(digest_bytes);
1612
1613 let mut zero_found = false;
1614 for (name, _) in committee.members() {
1615 let f = position_submit_certificate(&committee, name, &tx_digest);
1616 assert!(f < committee.num_members());
1617 if f == 0 {
1618 assert!(!zero_found);
1620 zero_found = true;
1621 }
1622 }
1623 assert!(zero_found);
1624 }
1625 }
1626}