Skip to main content

iota_core/
consensus_adapter.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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    // Certificate sequencing metrics
70    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
206/// Block status for internal use in the consensus adapter.
207pub 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
244/// Submit IOTA certificates to the consensus.
245pub struct ConsensusAdapter {
246    /// The network client connecting to the consensus node of this authority.
247    consensus_client: Arc<dyn ConsensusClient>,
248
249    /// The checkpoint store for the validator
250    checkpoint_store: Arc<CheckpointStore>,
251
252    /// Authority pubkey.
253    authority: AuthorityName,
254
255    /// The hard limit on the number of inflight transactions at this node.
256    /// Used as the upper bound for graduated pre-consensus load shedding
257    /// (`graduated_load_shedding_soft_limit_pct`) in the certificate-less
258    /// (P-COOL) mode, and as the threshold for the binary
259    /// cutoff in `ConsensusAdapter::check_consensus_overload()` in both
260    /// certificate-less and certificate-based flows.
261    max_pending_transactions: usize,
262
263    /// Number of submitted transactions still inflight at this node.
264    num_inflight_transactions: AtomicU64,
265
266    /// Dictates the maximum position  from which will submit to consensus. Even
267    /// if the is elected to submit from a higher position than this, it
268    /// will "reset" to the max_submit_position.
269    max_submit_position: Option<usize>,
270
271    /// When provided it will override the current back off logic and will use
272    /// this value instead as delay step.
273    submit_delay_step_override: Option<Duration>,
274
275    /// A structure to check the connection statuses populated by the Connection
276    /// Monitor Listener
277    connection_monitor_status: Arc<dyn CheckConnection>,
278
279    /// A structure to check the reputation scores populated by Consensus
280    low_scoring_authorities: ArcSwap<Arc<ArcSwap<HashMap<AuthorityName, u64>>>>,
281
282    /// A structure to register metrics
283    metrics: ConsensusAdapterMetrics,
284
285    /// Semaphore limiting parallel submissions to consensus
286    submit_semaphore: Semaphore,
287
288    /// Tracks consensus submission latency for adaptive submit-delay backoff.
289    latency_observer: LatencyObserver,
290
291    /// Percentage of `max_pending_transactions` (hard limit) defining the soft
292    /// limit at which graduated pre-consensus load shedding begins. When
293    /// in-flight transactions are at or below the soft limit, no shedding
294    /// occurs; above it, the shedding rate scales linearly from 0% to 100% at
295    /// `max_pending_transactions`. Used in the certificate-less (P-COOL) mode.
296    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    /// Current connection statuses forwarded from the connection monitor
310    pub connection_statuses: Arc<DashMap<PeerId, ConnectionStatus>>,
311    /// A map from authority name to peer id
312    pub authority_names_to_peer_ids: ArcSwap<HashMap<AuthorityName, PeerId>>,
313}
314
315pub struct ConnectionMonitorStatusForTests {}
316
317impl ConsensusAdapter {
318    /// Make a new Consensus adapter instance.
319    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    /// Test-only: creates a consensus adapter with a `MockConsensusClient`,
352    /// default values for all parameters, and the given authority name.
353    #[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        // Currently consensus worker might lose transactions on restart, so we need to
378        // resend them.
379        // TODO: get_all_pending_consensus_transactions is called
380        // twice when initializing AuthorityPerEpochStore and here, should not
381        // be a big deal but can be optimized
382        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                // In the P-COOL flow, the list of pending consensus
387                // certificates is always empty.
388                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` does not contain `EndOfPublish` yet, we need to insert it.
399            if !recovered
400                .iter()
401                .any(ConsensusTransaction::is_end_of_publish)
402            {
403                // There are two cases when this is needed:
404                // (1) We send `EndOfPublish` message after removing pending certificates in
405                // `submit_and_wait_inner`. It is possible that node will crash
406                // between those two steps, in which case we might need to
407                // re-introduce `EndOfPublish` message on restart.
408                // (2) If node crashed inside `ConsensusAdapter::close_epoch`,
409                // after reconfig lock state was written to DB and before we persisted
410                // `EndOfPublish` message.
411                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        // Use the minimum digest to compute submit delay.
432        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                    // P-COOL: no submit delay needed (number of submitting validators
440                    // controlled through another mechanism)
441                    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); // > p50 consensus latency with global deployment
468        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    /// Overrides the latency and the position if there are defined settings for
495    /// `max_submit_position` and `submit_delay_step_override`. If the
496    /// `max_submit_position` has defined, then that will always be used
497    /// irrespective of any so far decision. Same for the
498    /// `submit_delay_step_override`.
499    fn override_by_max_submit_position_settings(
500        &self,
501        latency: Duration,
502        mut position: usize,
503    ) -> (Duration, usize) {
504        // Respect any manual override for position and latency from the settings
505        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    /// Check when this authority should submit the certificate to consensus.
514    /// This sorts all authorities based on pseudo-random distribution derived
515    /// from transaction hash.
516    ///
517    /// The function targets having 1 consensus transaction submitted per user
518    /// transaction when system operates normally.
519    ///
520    /// The function returns the position of this authority when it is their
521    /// turn to submit the transaction to consensus.
522    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    /// This function runs the following algorithm to decide whether or not to
533    /// submit a transaction to consensus.
534    ///
535    /// It takes in a deterministic list that represents positions of all the
536    /// authorities. The authority in the first position will be responsible
537    /// for submitting to consensus, and so we check if we are this
538    /// validator, and if so, return true.
539    ///
540    /// If we are not in that position, we check our connectivity to the
541    /// authority in that position. If we are connected to them, we can
542    /// assume that they are operational and will submit the transaction. If
543    /// we are not connected to them, we assume that they are not operational
544    /// and we will not rely on that authority to submit the transaction. So
545    /// we shift them out of the first position, and run this algorithm
546    /// again on the new set of positions.
547    ///
548    /// This can possibly result in a transaction being submitted twice if an
549    /// authority sees a false negative in connectivity to another, such as
550    /// in the case of a network partition.
551    ///
552    /// Recursively, if the authority further ahead of us in the positions is a
553    /// low performing authority, we will move our positions up one, and
554    /// submit the transaction. This allows maintaining performance overall.
555    /// We will only do this part for authorities that are not low performers
556    /// themselves to prevent extra amplification in the case that the
557    /// positions look like [low_scoring_a1, low_scoring_a2, a3]
558    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; // don't filter ourself out
574                if keep {
575                    before_our_position = false;
576                }
577
578                // filter out any nodes that appear disconnected
579                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; // used for metrics
586                }
587
588                // Filter out low scoring nodes
589                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    /// This method blocks until transaction is persisted in local database
605    /// It then returns handle to async task, user can join this handle to await
606    /// while transaction is processed by consensus
607    ///
608    /// This method guarantees that once submit(but not returned async handle)
609    /// returns, transaction is persisted and will eventually be sent to
610    /// consensus even after restart
611    ///
612    /// When submitting a certificate caller **must** provide a ReconfigState
613    /// lock guard
614    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            // Soft-bundle batches must be homogeneous: either all
631            // CertifiedTransaction (certificate flow) or all
632            // UserTransactionV1 (P-COOL flow). submit_and_wait_inner
633            // assumes a single transaction kind across the batch.
634            for transaction in transactions {
635                // Routed through the shared user-transaction helpers rather than an
636                // inline match, so a new user-transaction kind is classified in one
637                // place (`ConsensusTransactionKind::is_user_transaction`) and becomes
638                // soft-bundle eligible automatically.
639                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    /// Returns the limit on the number of inflight transactions at this node.
652    pub(super) fn max_pending_transactions(&self) -> usize {
653        self.max_pending_transactions
654    }
655
656    /// Returns the percentage of `max_pending_transactions` (hard limit)
657    /// defining the soft limit at which graduated pre-consensus load
658    /// shedding begins. Defaults to 50%. Used in the certificate-less
659    /// (P-COOL) mode.
660    pub(super) fn graduated_load_shedding_soft_limit_pct(&self) -> u32 {
661        self.graduated_load_shedding_soft_limit_pct
662    }
663
664    /// Returns the number of transactions currently in-flight in consensus.
665    pub(super) fn num_inflight_transactions(&self) -> u64 {
666        self.num_inflight_transactions.load(Ordering::Relaxed)
667    }
668
669    /// Test-only: sets the number of in-flight transactions.
670    #[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    /// Returns `true` if both hard limits allow another transaction:
677    /// (i) in-flight count is at or below `max_pending_transactions`, and
678    /// (ii) `submit_semaphore` has at least one available permit.
679    ///
680    /// Uses relaxed atomic reads: the two limits are not observed atomically.
681    fn check_consensus_hard_limits(&self) -> bool {
682        // First check total in-flight transactions (waiting and in submission).
683        // TODO: this check is redundant in the P-COOL flow - graduated
684        // shedding already rejects at 100% once `num_inflight_transactions`
685        // reaches `max_pending_transactions`. Remove when the certificate-based
686        // flow is removed from the codebase. The semaphore check below stays in
687        // either case, since it is a separate concurrency limit not covered
688        // by graduated shedding.
689        if self.num_inflight_transactions.load(Ordering::Relaxed) as usize
690            > self.max_pending_transactions
691        {
692            return false;
693        }
694
695        // Then check if `submit_semaphore` has permits
696        self.submit_semaphore.available_permits() > 0
697    }
698
699    /// `IotaResult` wrapper for `check_consensus_hard_limits`. Returns
700    /// `TooManyTransactionsPendingConsensus` when the hard limits are exceeded.
701    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        // Reconfiguration lock is dropped when pending_consensus_transactions is
716        // persisted, before it is handled by consensus
717        let async_stage = self
718            .clone()
719            .submit_and_wait(transactions.to_vec(), epoch_store.clone());
720        // Number of these tasks is weakly limited based on `num_inflight_transactions`.
721        // (Limit is not applied atomically, and only to user transactions.)
722        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        // When epoch_terminated signal is received all pending submit_and_wait_inner
732        // are dropped.
733        //
734        // This is needed because submit_and_wait_inner waits on read_notify for
735        // consensus message to be processed, which may never happen on epoch
736        // boundary.
737        //
738        // In addition to that, within_alive_epoch ensures that all pending consensus
739        // adapter tasks are stopped before reconfiguration can proceed.
740        //
741        // This is essential because consensus workers reuse same ports when consensus
742        // restarts, this means we might be sending transactions from previous
743        // epochs to consensus of new epoch if we have not had this barrier.
744        epoch_store
745            .within_alive_epoch(self.submit_and_wait_inner(transactions, &epoch_store))
746            .await
747            .ok(); // result here indicates if epoch ended earlier, we don't
748        // care about it
749    }
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        // submit_batch enforces that multi-tx batches (soft bundles) are
761        // homogeneous: either all CertifiedTransaction or all UserTransactionV1.
762        // Single-tx submits can be any kind.
763        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        // Create the waiter until the node's turn comes to submit to consensus
785        let (await_submit, position, positions_moved, preceding_disconnected) =
786            self.await_submit_delay(epoch_store.committee(), &transactions[..]);
787
788        // Create the waiter until the transaction is processed by consensus or via
789        // checkpoint
790        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            // We need to wait for some delay until we submit transaction to the consensus
796            _ = await_submit => Some(processed_via_consensus_or_checkpoint),
797
798            // If epoch ends, don't wait for submit delay
799            _ = 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            // If transaction is received by consensus or checkpoint while we wait, we are done.
805            // Capture the resolved `ProcessedMethod` so the latency metric and
806            // `latency_observer` accurately reflect whether the early-fire was a
807            // consensus processing, a checkpoint sync, or a `dropped_tx_status_cache`
808            // hit. Without this, the guard would stay at its default `Consensus`,
809            // mislabeling cache-hit retries.
810            method = &mut processed_via_consensus_or_checkpoint => {
811                guard.processed_method = method;
812                None
813            }
814        };
815
816        // Log warnings for administrative transactions that fail to get sequenced
817        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            // populate the position only when this authority submits the transaction
849            // to consensus
850            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            // We enter this branch when in select above await_submit completed and
864            // processed_waiter is pending This means it is time for us to
865            // submit transaction to consensus
866            let submit_inner = async {
867                const RETRY_DELAY_STEP: Duration = Duration::from_secs(1);
868
869                loop {
870                    // Submit the transaction to consensus and return the submit result with a
871                    // status waiter
872                    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                            // Block has been sequenced. Nothing more to do, we do have guarantees
889                            // that the transaction will appear in consensus output.
890                            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                            // Block has been garbage collected and we have no guarantees that the
901                            // transaction will appear in consensus output. We'll
902                            // resubmit the transaction to consensus. If the transaction has been
903                            // already "processed", then probably someone else has submitted
904                            // the transaction and managed to get sequenced. Then this future will
905                            // have been cancelled anyways so no need to check here on the processed
906                            // output.
907                            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                // In the P-COOL flow, `UserTransactionV1` kind corresponds
943                // to user transactions.  Routed through the shared predicate
944                // so a new user-transaction kind is classified in one place.
945                transactions[0].kind.is_user_transaction()
946            } else {
947                // In the certificate mode, `CertifiedTransaction` kind corresponds
948                // to user transactions.
949                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                // In the P-COOL flow, `EndOfPublish` is sent solely from
954                // `close_epoch`. There is no pending certificate drain to
955                // monitor here, and sending from this per-transaction callback
956                // would produce N duplicate EndOfPublish messages (one per
957                // in-flight user transaction completing after `RejectUserCerts`
958                // is set).
959                false
960            } else {
961                // In certificate mode, `EndOfPublish` is sent once the list of
962                // pending consensus certificates is drained. Multiple tasks can
963                // enter here concurrently with `pending_count == 0`, producing
964                // duplicate messages — this is rare and does not affect
965                // correctness.
966                //
967                // Note: there could be a race condition here where we enter
968                // this check in `RejectAllCerts` state. In that case we don't
969                // need to send `EndOfPublish` because the condition to enter
970                // `RejectAllCerts` is when 2f+1 other validators already
971                // sequenced their `EndOfPublish` message.
972                //
973                // TODO: This entire certificate-mode drain logic can be removed
974                // once the certificate flow is fully cleaned up.
975                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 // send end of epoch if no pending certificates
983                } else {
984                    false
985                }
986            }
987        } else {
988            false
989        };
990        if send_end_of_publish {
991            // Spawn a separate task for EndOfPublish so that
992            // submit_and_wait_inner returns promptly after the original
993            // transaction is processed. Awaiting the retry loop inline
994            // would hold the InflightDropGuard and inflate in-flight
995            // metrics for the duration of retries.
996            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                    // This can happen during reconfig, or when consensus has full internal buffers
1036                    // and needs to back pressure, so retry a few times before logging warnings.
1037                    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                        // Shorter delay for DKG messages, which are time-sensitive and happen at
1052                        // start-of-epoch when submit errors due to active reconfig are likely.
1053                        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        // we want to record the num of retries when reporting latency but to avoid
1065        // label cardinality we do some simple bucketing to give us a good
1066        // enough idea of how many retries happened associated with the latency.
1067        let bucket = match retries {
1068            0..=10 => retries.to_string(), // just report the retry count as is
1069            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    /// Waits for transactions to appear either to consensus output or been
1084    /// executed via a checkpoint (state sync).
1085    /// Returns the processed method, whether the transactions have been
1086    /// processed via consensus, or have been synced via checkpoint.
1087    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                // If the transaction is a checkpoint signature, we can also wait to get
1109                // notified when a checkpoint with equal or higher sequence
1110                // number has been already synced. This way we don't try to unnecessarily
1111                // sequence the signature for an already verified checkpoint.
1112                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            // We wait for each transaction individually to be processed by consensus,
1121            // executed in a checkpoint or dropped. We could equally just get
1122            // notified in aggregate when all transactions are processed, but
1123            // with this approach can get notified in a more fine-grained way as
1124            // transactions can be marked as processed in different ways. This
1125            // is mostly a concern for the soft-bundle transactions.
1126            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    /// Submits an `EndOfPublish` message to consensus with exponential
1167    /// backoff (capped at `MAX_BACKOFF`). Retries indefinitely on any
1168    /// error — both transient failures (e.g. DB write errors in
1169    /// `insert_pending_consensus_transactions`) and permanent ones (e.g.
1170    /// `EpochEnded` from `tables()`). A missing `EndOfPublish` would
1171    /// stall the epoch, so the loop never gives up on its own.
1172    ///
1173    /// Callers **must** wrap this with `epoch_store.within_alive_epoch()`
1174    /// to cancel retries when the epoch terminates — this is the
1175    /// mechanism that stops the loop on permanent `EpochEnded` errors.
1176    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                // update is in progress, assume the status is still or becoming disconnected
1250                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    /// This method is called externally to begin reconfiguration.
1292    /// It transitions the reconfig state to reject new user transactions.
1293    /// `ConsensusAdapter` will send `EndOfPublish` once all pending
1294    /// transactions are drained (in the certificate mode) or right away
1295    /// (in the P-COOL flow). Submission is asynchronous —
1296    /// a background task handles retries so this method returns promptly.
1297    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                // Allow caller to call this method multiple times
1302                return;
1303            }
1304
1305            let send_end_of_publish = if epoch_store.protocol_config().enable_pcool_flow() {
1306                // In the P-COOL flow, there are no pending consensus
1307                // certificates, so `EndOfPublish` is always sent immediately.
1308                debug!(epoch=?epoch_store.epoch(), "Closing epoch in P-COOL mode");
1309
1310                true
1311            } else {
1312                // In certificate mode, `EndOfPublish` is sent only once the list
1313                // of pending consensus certificates is drained.
1314                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 // send end of epoch if no pending certificates
1318            };
1319
1320            epoch_store.close_user_certs(reconfig_guard);
1321
1322            send_end_of_publish
1323            // reconfig_guard lock is dropped here.
1324        };
1325
1326        if send_end_of_publish {
1327            // Spawned because ReconfigurationInitiator::close_epoch is
1328            // sync — it cannot await. This is safe: by this point
1329            // close_user_certs() has already set the reconfig state to
1330            // reject new transactions, so no further user work depends
1331            // on this method returning. The background task retries
1332            // until the message is delivered or the epoch terminates.
1333            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
1367/// Tracks number of inflight consensus requests and relevant metrics
1368struct 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        // Only sample latency after consensus quorum is up. Otherwise, the wait for
1460        // consensus quorum at the beginning of an epoch can distort the sampled
1461        // latencies. Technically there are more system transaction types that
1462        // can be included in samples after the first consensus commit, but this
1463        // set of types should be enough.
1464        if self.position == Some(0) {
1465            // Transaction types below require quorum existed in the current epoch.
1466            // TODO: refactor tx_type to enum.
1467            let sampled = matches!(
1468                self.tx_type,
1469                "shared_certificate" | "owned_certificate" | "checkpoint_signature" | "soft_bundle"
1470            );
1471            // Exclude checkpoint-synced and dropped txs from the latency observer:
1472            // their latency reflects state-sync timing or rejection, not consensus
1473            // throughput, and would skew the observed consensus latency.
1474            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    /// A committee built from fixed keys and stakes, so the submission order
1525    /// these tests check depends only on the code under test, not on `rand`
1526    /// or on how fastcrypto generates keys.
1527    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        // When we define max submit position and delay step
1547        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        // transaction to submit
1561        let tx_digest = TransactionDigest::new([7; 32]);
1562
1563        // Ensure that the original position is higher
1564        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        // Make sure that position is set to max value 1
1570        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        // Without submit position and delay step
1578        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        // delay_step * position * 2 = 1 * 8 * 2 = 16
1597        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        // many transaction digests, and account for validator selection
1606        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                    // One and only one validator gets position 0
1619                    assert!(!zero_found);
1620                    zero_found = true;
1621                }
1622            }
1623            assert!(zero_found);
1624        }
1625    }
1626}