Skip to main content

sui_core/
consensus_transaction_pool.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pull-based submission of transactions to consensus, enabled by
5//! `NodeConfig::consensus_transaction_pool`. Instead of the admission-queue drain
6//! task pushing transactions through `ConsensusAdapter` into consensus, the
7//! consensus proposer pulls them from a [`ConsensusTransactionPool`] at block
8//! proposal time via the `consensus_core::TransactionPool` trait.
9//!
10//! Producers feed the pool from three directions: the validator gRPC service
11//! inserts user transactions ([`TransactionPoolContext::try_insert`]), the
12//! `ConsensusAdapter` submits system transactions and pings through
13//! [`TransactionPoolClient`], and each submission is resolved when the proposer
14//! includes it in a block (or with an error on eviction or epoch end).
15//!
16//! A pool instance is bound to a single epoch: `ConsensusManager` creates one at
17//! consensus start, publishes it through the process-lifetime
18//! [`TransactionPoolContext`], and closes it before stopping consensus.
19
20use crate::admission_queue::{
21    AdmissionQueueEntry, AdmissionQueueMetrics, PopAction, PriorityAdmissionQueue,
22};
23use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
24use crate::consensus_adapter::{
25    BlockStatusReceiver, ConsensusAdapterMetrics, ConsensusClient, ProcessedMethod,
26    processing_error,
27};
28use crate::consensus_handler::{SequencedConsensusTransactionKey, tx_type_label};
29use async_trait::async_trait;
30use consensus_core::{BlockStatus, ClientError, LimitReached, Transaction, TransactionPool};
31use consensus_types::block::{
32    BlockRef, NUM_RESERVED_TRANSACTION_INDICES, PING_TRANSACTION_INDEX, Round, TransactionIndex,
33};
34use itertools::Itertools;
35use mysten_common::debug_fatal;
36use mysten_common::sync::notify_read::OwnedRegistration;
37use parking_lot::Mutex;
38use prometheus::IntGauge;
39use std::collections::{BTreeMap, VecDeque};
40use std::sync::Arc;
41use std::time::{Duration, Instant};
42use sui_macros::fail_point_if;
43use sui_types::base_types::EpochId;
44use sui_types::digests::TransactionDigest;
45use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
46use sui_types::messages_checkpoint::CheckpointSequenceNumber;
47use sui_types::messages_consensus::{
48    ConsensusPosition, ConsensusTransaction, ConsensusTransactionKey,
49};
50use tokio::sync::{oneshot, watch};
51use tracing::warn;
52
53// Pings originate from external RPC clients, so we give them a hard bound.
54const MAX_PENDING_PINGS: usize = 1_024;
55
56// System traffic is correctness-critical and cannot be rejected. A depth this
57// large signals that proposal progress is stuck and needs operator attention.
58const SYSTEM_LANE_WARN_THRESHOLD: usize = 10_000;
59
60/// Resolves with the submission's final consensus positions once the proposer
61/// includes it in a block, or with an error on eviction or epoch end.
62pub type PositionReceiver = oneshot::Receiver<SuiResult<Vec<ConsensusPosition>>>;
63
64/// Payload delivered when the proposer includes a `ConsensusAdapter`-path
65/// submission in a block: the block, the submission's transaction indices in it,
66/// and a subscription for the block's eventual status.
67type BlockInclusion = (
68    BlockRef,
69    Vec<TransactionIndex>,
70    oneshot::Receiver<BlockStatus>,
71);
72
73/// How a queued submission is resolved once its fate is known.
74enum EntryAck {
75    /// RPC user path: positions on inclusion, error on eviction / epoch end.
76    User(oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>),
77    /// `ConsensusAdapter` path (system transactions and pings): block inclusion
78    /// ack plus a `BlockStatus` subscription.
79    SystemOrPing(oneshot::Sender<BlockInclusion>),
80}
81
82/// Must be explicitly resolved; dropping it unresolved is a bug.
83struct PendingAck {
84    ack: Option<EntryAck>,
85    keys: Vec<ConsensusTransactionKey>,
86    created_at: &'static std::panic::Location<'static>,
87    created: Instant,
88}
89
90impl PendingAck {
91    #[track_caller]
92    fn new(ack: EntryAck, keys: Vec<ConsensusTransactionKey>) -> Self {
93        Self {
94            ack: Some(ack),
95            keys,
96            created_at: std::panic::Location::caller(),
97            created: Instant::now(),
98        }
99    }
100
101    fn resolve_included(
102        mut self,
103        epoch: EpochId,
104        block_ref: BlockRef,
105        indices: Vec<TransactionIndex>,
106        block: &mut ProposedBlock,
107    ) {
108        match self.ack.take().expect("ack must be pending") {
109            EntryAck::User(sender) => {
110                let positions = indices
111                    .into_iter()
112                    .map(|index| ConsensusPosition {
113                        epoch,
114                        block: block_ref,
115                        index,
116                    })
117                    .collect();
118                let _ = sender.send(Ok(positions));
119            }
120            EntryAck::SystemOrPing(sender) => {
121                let (status_sender, status_receiver) = oneshot::channel();
122                block.subscribers.push(status_sender);
123                let _ = sender.send((block_ref, indices, status_receiver));
124            }
125        }
126    }
127
128    fn resolve_error(mut self, error: SuiError) {
129        match self.ack.take().expect("ack must be pending") {
130            EntryAck::User(sender) => {
131                let _ = sender.send(Err(error));
132            }
133            EntryAck::SystemOrPing(sender) => {
134                drop(sender);
135            }
136        }
137    }
138
139    fn drop_deliberately(mut self, _reason: &'static str) {
140        drop(self.ack.take().expect("ack must be pending"));
141    }
142
143    fn is_abandoned_system(&self) -> bool {
144        matches!(&self.ack, Some(EntryAck::SystemOrPing(sender)) if sender.is_closed())
145    }
146}
147
148impl Drop for PendingAck {
149    fn drop(&mut self) {
150        let flavor = match self.ack {
151            Some(EntryAck::User(_)) => "User",
152            Some(EntryAck::SystemOrPing(_)) => "SystemOrPing",
153            None => return,
154        };
155        if !std::thread::panicking() {
156            debug_fatal!(
157                "{} ack created at {} dropped without resolution after {:?}; keys={:?}",
158                flavor,
159                self.created_at,
160                self.created.elapsed(),
161                self.keys
162            );
163        }
164    }
165}
166
167/// Watches one queued transaction key for the two ways it can become processed
168/// without this validator proposing it: consensus output from a block another
169/// validator proposed, and execution through a state-synced checkpoint.
170struct ProcessedWatch {
171    consensus: OwnedRegistration<SequencedConsensusTransactionKey, ()>,
172    checkpoint: Option<OwnedRegistration<TransactionDigest, CheckpointSequenceNumber>>,
173    /// Saves results of `try_recv` on the fields above.
174    observed: Option<ProcessedMethod>,
175}
176
177impl ProcessedWatch {
178    fn register(epoch_store: &Arc<AuthorityPerEpochStore>, key: ConsensusTransactionKey) -> Self {
179        let key = SequencedConsensusTransactionKey::External(key);
180        Self {
181            checkpoint: key
182                .user_transaction_digest()
183                .map(|digest| epoch_store.register_executed_in_checkpoint_notify(&digest)),
184            consensus: epoch_store.register_consensus_message_processed_notify(&key),
185            observed: None,
186        }
187    }
188
189    /// Return the path through which the key was observed as processed, if any.
190    fn check_processed(&mut self) -> Option<ProcessedMethod> {
191        if self.observed.is_none() {
192            if self.consensus.try_recv().is_ok() {
193                self.observed = Some(ProcessedMethod::ConsensusMessageProcessed);
194            } else if let Some(checkpoint) = &mut self.checkpoint
195                && checkpoint.try_recv().is_ok()
196            {
197                self.observed = Some(ProcessedMethod::CheckpointExecuted);
198            }
199        }
200        self.observed
201    }
202}
203
204/// One queued submission. Multiple transactions form a soft bundle, which is
205/// included in a block atomically or not at all.
206struct PoolEntry {
207    transactions: Vec<Transaction>,
208    total_bytes: usize,
209    gas_price: u64,
210    tx_type: &'static str, // tx label for metrics
211    ack: PendingAck,
212    metrics: Arc<AdmissionQueueMetrics>,
213    /// Empty for system and ping submissions: the `ConsensusAdapter` already
214    /// checks those before they reach the pool.
215    processed: Vec<ProcessedWatch>,
216}
217
218/// `Some` once every watch has observed processing. Bundles are proposed
219/// atomically, so a partially processed one is still proposed in full.
220fn all_processed(watches: &mut [ProcessedWatch]) -> Option<ProcessedMethod> {
221    let mut result: Option<ProcessedMethod> = None;
222    for watch in watches {
223        let observed = watch.check_processed()?;
224        result = result.max(Some(observed));
225    }
226    result
227}
228
229impl AdmissionQueueEntry for PoolEntry {
230    fn gas_price(&self) -> u64 {
231        self.gas_price
232    }
233
234    fn transaction_keys(&self) -> impl Iterator<Item = ConsensusTransactionKey> {
235        self.ack.keys.iter().cloned()
236    }
237
238    fn notify_evicted(self, min_gas_price: u64) {
239        self.metrics.pool_depth.with_label_values(&["user"]).dec();
240        self.metrics
241            .pool_bytes
242            .with_label_values(&["user"])
243            .sub(self.total_bytes as i64);
244        self.ack.resolve_error(
245            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price }
246                .into(),
247        );
248    }
249
250    fn notify_rejected(self, min_gas_price: u64) {
251        self.ack.resolve_error(
252            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price }
253                .into(),
254        );
255    }
256}
257
258/// Tracks the user lane's state, which is closed when user certs are no longer accepted.
259enum UserLane {
260    Open(PriorityAdmissionQueue<PoolEntry>),
261    Closed,
262}
263
264impl UserLane {
265    #[must_use]
266    fn close(&mut self) -> Vec<PoolEntry> {
267        match std::mem::replace(self, Self::Closed) {
268            Self::Open(queue) => queue.into_entries(),
269            Self::Closed => Vec::new(),
270        }
271    }
272}
273
274struct Pool {
275    user: UserLane,
276    // Unbounded: system transactions are correctness-critical, produced by trusted
277    // internal components at bounded rates, and must never be evicted or outbid.
278    system: VecDeque<PoolEntry>,
279    pings: VecDeque<PendingAck>,
280    // This validator's proposed blocks, resolved by `notify_committed` once each
281    // commits or is garbage collected.
282    blocks: BTreeMap<BlockRef, ProposedBlock>,
283}
284
285#[derive(Default)]
286struct ProposedBlock {
287    /// `ConsensusAdapter`-path acks awaiting the block's status.
288    subscribers: Vec<oneshot::Sender<BlockStatus>>,
289    /// Every entry in the block, for reporting metrics.
290    entries: Vec<ProposedEntry>,
291}
292
293struct ProposedEntry {
294    lane: TakenLane,
295    tx_type: &'static str,
296    created: Instant,
297}
298
299enum Inner {
300    Open(Pool),
301    Closed,
302}
303
304/// A single epoch's transaction pool, drained by the consensus proposer through the
305/// `consensus_core::TransactionPool` impl below. `take()` drains pings (zero block
306/// space), then the system lane FIFO, then the user lane in gas-price order —
307/// user transactions never delay system ones.
308///
309/// Backpressure for user RPCs is provided solely by the capacity limit of the
310/// `user` priority queue.
311pub struct ConsensusTransactionPool {
312    epoch_store: Arc<AuthorityPerEpochStore>,
313    metrics: Arc<AdmissionQueueMetrics>,
314    adapter_metrics: ConsensusAdapterMetrics,
315    inner: Arc<Mutex<Inner>>,
316}
317
318impl ConsensusTransactionPool {
319    pub fn new(
320        epoch_store: Arc<AuthorityPerEpochStore>,
321        max_pending_transactions: usize,
322        metrics: Arc<AdmissionQueueMetrics>,
323        adapter_metrics: ConsensusAdapterMetrics,
324    ) -> Self {
325        assert!(
326            max_pending_transactions > 0,
327            "consensus transaction pool max_pending_transactions must be > 0"
328        );
329        assert!(
330            epoch_store
331                .protocol_config()
332                .max_num_transactions_in_block()
333                .saturating_sub(1)
334                < u64::from(TransactionIndex::MAX.saturating_sub(NUM_RESERVED_TRANSACTION_INDICES)),
335            "Unsupported max_num_transactions_in_block: {}",
336            epoch_store
337                .protocol_config()
338                .max_num_transactions_in_block()
339        );
340
341        Self {
342            epoch_store,
343            metrics: metrics.clone(),
344            adapter_metrics,
345            inner: Arc::new(Mutex::new(Inner::Open(Pool {
346                user: UserLane::Open(PriorityAdmissionQueue::new(
347                    max_pending_transactions,
348                    metrics,
349                )),
350                system: VecDeque::new(),
351                pings: VecDeque::new(),
352                blocks: BTreeMap::new(),
353            }))),
354        }
355    }
356
357    #[cfg(test)]
358    pub(crate) fn new_for_tests(
359        epoch_store: Arc<AuthorityPerEpochStore>,
360        max_pending_transactions: usize,
361        metrics: Arc<AdmissionQueueMetrics>,
362    ) -> Self {
363        Self::new(
364            epoch_store,
365            max_pending_transactions,
366            metrics,
367            ConsensusAdapterMetrics::new_test(),
368        )
369    }
370
371    pub fn epoch(&self) -> EpochId {
372        self.epoch_store.epoch()
373    }
374
375    /// Inserts user transactions to the pool. Returns the receiver for their
376    /// eventual positions, and bool indicating whether all transactions in the
377    /// group were newly inserted.
378    /// Fails fast on epoch mismatch, a closed pool/user lane, block-limit
379    /// violations, or when outbid by the eviction policy.
380    /// `gas_price` is the entry's priority in the user lane (proposed first, and
381    /// outbids lower prices when the lane is full).
382    pub fn try_insert(
383        &self,
384        caller_epoch: EpochId,
385        gas_price: u64,
386        transactions: Vec<ConsensusTransaction>,
387    ) -> SuiResult<(PositionReceiver, bool)> {
388        // Check whether the transactions were already processed by consensus or
389        // checkpoint execution.
390        let keys = transactions
391            .iter()
392            .map(ConsensusTransaction::key)
393            .collect::<Vec<_>>();
394        let mut processed = keys
395            .iter()
396            .map(|key| ProcessedWatch::register(&self.epoch_store, key.clone()))
397            .collect::<Vec<_>>();
398        if let Some(method) = self.check_already_processed(&mut processed)? {
399            self.metrics
400                .pool_already_processed
401                .with_label_values(&["insert", method.metric_label()])
402                .inc();
403            return Err(processing_error(
404                processed.iter().map(|watch| watch.consensus.key()),
405                method,
406            ));
407        }
408
409        let tx_type = tx_type_label(&transactions);
410        let (transactions, total_bytes) = self.serialize_and_validate(&transactions)?;
411        let (sender, receiver) = oneshot::channel();
412        let entry = PoolEntry {
413            transactions,
414            total_bytes,
415            gas_price,
416            tx_type,
417            ack: PendingAck::new(EntryAck::User(sender), keys),
418            metrics: self.metrics.clone(),
419            processed,
420        };
421
422        let mut inner = self.inner.lock();
423        let user = match self.check_user_lane_open_locked(caller_epoch, &mut inner) {
424            Ok(user) => user,
425            Err(error) => {
426                drop(inner);
427                entry.ack.resolve_error(error.clone());
428                return Err(error);
429            }
430        };
431        let outcome = user.try_insert(entry);
432        drop(inner);
433
434        let newly_inserted = outcome.notify()?;
435        self.metrics.pool_depth.with_label_values(&["user"]).inc();
436        self.metrics
437            .pool_bytes
438            .with_label_values(&["user"])
439            .add(total_bytes as i64);
440        Ok((receiver, newly_inserted))
441    }
442
443    /// Queues a submission from the `ConsensusAdapter` path. An empty slice is a
444    /// ping: it consumes no block space and is acked with `PING_TRANSACTION_INDEX`
445    /// on the next proposal.
446    fn submit(
447        &self,
448        caller_epoch: EpochId,
449        transactions: &[ConsensusTransaction],
450    ) -> SuiResult<oneshot::Receiver<BlockInclusion>> {
451        let (sender, receiver) = oneshot::channel();
452        if transactions.is_empty() {
453            let mut inner = self.inner.lock();
454            let pool = self.check_epoch_and_open_locked(caller_epoch, &mut inner)?;
455            if pool.pings.len() >= MAX_PENDING_PINGS {
456                return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
457            }
458            pool.pings
459                .push_back(PendingAck::new(EntryAck::SystemOrPing(sender), Vec::new()));
460            self.metrics.pool_depth.with_label_values(&["ping"]).inc();
461            return Ok(receiver);
462        }
463
464        // User transactions must enter the pool through `try_insert`.
465        if transactions
466            .iter()
467            .any(ConsensusTransaction::is_user_transaction)
468        {
469            debug_fatal!(
470                "user transaction submitted through the consensus transaction pool client"
471            );
472            return Err(SuiErrorKind::GenericAuthorityError {
473                error: "user transactions cannot be submitted through the transaction pool client"
474                    .to_string(),
475            }
476            .into());
477        }
478        // Size limits cannot be relaxed for system transactions: peers enforce the
479        // same limits on every transaction in a received block, and an over-budget
480        // entry at the head of the FIFO system lane would wedge the lane.
481        let (serialized, total_bytes) = match self.serialize_and_validate(transactions) {
482            Ok(validated) => validated,
483            Err(error) => {
484                debug_fatal!("system transaction failed validation: {error}");
485                return Err(error);
486            }
487        };
488        let entry = PoolEntry {
489            transactions: serialized,
490            total_bytes,
491            gas_price: 0,
492            tx_type: tx_type_label(transactions),
493            ack: PendingAck::new(
494                EntryAck::SystemOrPing(sender),
495                transactions.iter().map(ConsensusTransaction::key).collect(),
496            ),
497            metrics: self.metrics.clone(),
498            processed: Vec::new(),
499        };
500
501        {
502            let mut inner = self.inner.lock();
503            let pool = match self.check_epoch_and_open_locked(caller_epoch, &mut inner) {
504                Ok(pool) => pool,
505                Err(error) => {
506                    drop(inner);
507                    entry.ack.resolve_error(error.clone());
508                    return Err(error);
509                }
510            };
511            pool.system.push_back(entry);
512            let depth = pool.system.len();
513            self.metrics
514                .pool_depth
515                .with_label_values(&["system"])
516                .set(depth as i64);
517            self.metrics
518                .pool_bytes
519                .with_label_values(&["system"])
520                .add(total_bytes as i64);
521            if depth == SYSTEM_LANE_WARN_THRESHOLD {
522                warn!(
523                    depth,
524                    "Consensus transaction pool system lane exceeded warning threshold"
525                );
526            }
527        }
528        Ok(receiver)
529    }
530
531    fn check_already_processed(
532        &self,
533        watches: &mut [ProcessedWatch],
534    ) -> SuiResult<Option<ProcessedMethod>> {
535        // Check for tx already processed by consensus.
536        let consensus_processed = self.epoch_store.check_consensus_messages_processed(
537            watches.iter().map(|watch| watch.consensus.key().clone()),
538        )?;
539        for (watch, consensus_processed) in watches.iter_mut().zip_eq(consensus_processed) {
540            if consensus_processed {
541                watch.observed = Some(ProcessedMethod::ConsensusMessageProcessed);
542            }
543        }
544
545        // Check for tx processed by checkpoint execution.
546        let (unobserved, digests): (Vec<_>, Vec<_>) = watches
547            .iter_mut()
548            .filter_map(|watch| {
549                if watch.observed.is_some() {
550                    return None;
551                }
552                let digest = *watch.checkpoint.as_ref()?.key();
553                Some((watch, digest))
554            })
555            .unzip();
556        if !digests.is_empty() {
557            let executed = self
558                .epoch_store
559                .multi_get_transaction_checkpoint(&digests)?;
560            for (watch, executed) in unobserved.into_iter().zip_eq(executed) {
561                if executed.is_some() {
562                    watch.observed = Some(ProcessedMethod::CheckpointExecuted);
563                }
564            }
565        }
566        Ok(all_processed(watches))
567    }
568
569    fn check_epoch_and_open_locked<'a>(
570        &self,
571        caller_epoch: EpochId,
572        inner: &'a mut Inner,
573    ) -> SuiResult<&'a mut Pool> {
574        if caller_epoch != self.epoch() {
575            return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
576        }
577        match inner {
578            Inner::Open(pool) => Ok(pool),
579            Inner::Closed => Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into()),
580        }
581    }
582
583    fn check_user_lane_open_locked<'a>(
584        &self,
585        caller_epoch: EpochId,
586        inner: &'a mut Inner,
587    ) -> SuiResult<&'a mut PriorityAdmissionQueue<PoolEntry>> {
588        let pool = self.check_epoch_and_open_locked(caller_epoch, inner)?;
589        match &mut pool.user {
590            UserLane::Open(user) => Ok(user),
591            UserLane::Closed => Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into()),
592        }
593    }
594
595    /// Enforces per-transaction and per-block size limits at insert time, which
596    /// guarantees every admitted entry fits an empty proposal — `take()` can
597    /// therefore never stall on an unfittable head-of-queue entry.
598    ///
599    /// Mirrors the checks in `consensus_core::TransactionClient::submit`, which
600    /// pool submissions bypass (the proposer drains the pool directly).
601    fn serialize_and_validate(
602        &self,
603        transactions: &[ConsensusTransaction],
604    ) -> SuiResult<(Vec<Transaction>, usize)> {
605        let protocol_config = self.epoch_store.protocol_config();
606        let bundle_count = u64::try_from(transactions.len()).expect("bundle count fits into u64");
607        if bundle_count > protocol_config.max_num_transactions_in_block() {
608            return Err(consensus_client_error(
609                ClientError::OversizedTransactionBundleCount(
610                    bundle_count,
611                    protocol_config.max_num_transactions_in_block(),
612                ),
613            ));
614        }
615
616        let mut total_bytes = 0usize;
617        let mut serialized = Vec::with_capacity(transactions.len());
618        for transaction in transactions {
619            let bytes =
620                bcs::to_bytes(transaction).expect("Serializing consensus transaction cannot fail");
621            let transaction_bytes =
622                u64::try_from(bytes.len()).expect("transaction size fits into u64");
623            if transaction_bytes > protocol_config.max_transaction_size_bytes() {
624                return Err(consensus_client_error(ClientError::OversizedTransaction(
625                    transaction_bytes,
626                    protocol_config.max_transaction_size_bytes(),
627                )));
628            }
629            total_bytes += bytes.len();
630            let bundle_bytes = u64::try_from(total_bytes).expect("bundle size fits into u64");
631            if bundle_bytes > protocol_config.max_transactions_in_block_bytes() {
632                return Err(consensus_client_error(
633                    ClientError::OversizedTransactionBundleBytes(
634                        bundle_bytes,
635                        protocol_config.max_transactions_in_block_bytes(),
636                    ),
637                ));
638            }
639            serialized.push(Transaction::new(bytes));
640        }
641        Ok((serialized, total_bytes))
642    }
643
644    /// Permanently shuts the pool down, called before stopping the consensus
645    /// authority. Pending user waiters are resolved with `ValidatorHaltedAtEpochEnd`
646    /// so the RPC retry loop resubmits them in the next epoch; system and ping acks
647    /// are dropped, which their submitters treat as consensus shutdown. Idempotent.
648    pub fn close(&self) {
649        let mut pool = {
650            let mut inner = self.inner.lock();
651            match std::mem::replace(&mut *inner, Inner::Closed) {
652                Inner::Open(pool) => pool,
653                Inner::Closed => return,
654            }
655        };
656
657        // The teardown below runs on the extracted pool with the mutex released.
658        self.resolve_flushed_user_entries(pool.user.close());
659        for entry in pool.system {
660            entry.ack.drop_deliberately("pool closed");
661        }
662        for ping in pool.pings {
663            ping.drop_deliberately("pool closed");
664        }
665        drop(pool.blocks);
666        self.metrics
667            .pool_depth
668            .with_label_values(&["system"])
669            .set(0);
670        self.metrics
671            .pool_bytes
672            .with_label_values(&["system"])
673            .set(0);
674        self.metrics.pool_depth.with_label_values(&["ping"]).set(0);
675    }
676
677    /// Resolves user entries flushed by `UserLane::close` with the retriable halted
678    /// error, so they retry in the next epoch. Dropping the entries deregisters
679    /// their processed watches — call with the pool mutex released.
680    fn resolve_flushed_user_entries(&self, entries: Vec<PoolEntry>) {
681        for entry in entries {
682            self.decrement_lane_metrics("user", &entry);
683            entry
684                .ack
685                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
686        }
687    }
688
689    fn decrement_lane_metrics(&self, lane: &str, entry: &PoolEntry) {
690        self.metrics.pool_depth.with_label_values(&[lane]).dec();
691        self.metrics
692            .pool_bytes
693            .with_label_values(&[lane])
694            .sub(entry.total_bytes as i64);
695    }
696
697    #[cfg(test)]
698    pub fn queue_depth(&self, lane: &str) -> i64 {
699        self.metrics.pool_depth.with_label_values(&[lane]).get()
700    }
701
702    #[cfg(test)]
703    pub fn already_processed_count(&self, stage: &str, method: &str) -> u64 {
704        self.metrics
705            .pool_already_processed
706            .with_label_values(&[stage, method])
707            .get()
708    }
709
710    #[cfg(test)]
711    pub fn abandoned_count(&self, lane: &str) -> u64 {
712        self.metrics.pool_abandoned.with_label_values(&[lane]).get()
713    }
714}
715
716#[derive(Clone, Copy)]
717enum TakenLane {
718    System,
719    User,
720}
721
722impl TakenLane {
723    fn label(self) -> &'static str {
724        match self {
725            TakenLane::System => "system",
726            TakenLane::User => "user",
727        }
728    }
729}
730
731struct TakenEntry {
732    lane: TakenLane,
733    entry: PoolEntry,
734}
735
736/// Holds the entries handed out by one `take()` until their fate is known:
737/// `acknowledge` (the proposer created a block) resolves every waiter with its
738/// position/inclusion ack, while dropping the guard uninvoked (failed proposal or
739/// shutdown) returns the entries to the front of their lanes so nothing is lost or
740/// reordered. All paths serialize on the pool mutex, so they compose correctly
741/// with `close()` running in between.
742struct TakenTransactionsGuard {
743    epoch: EpochId,
744    inner: Arc<Mutex<Inner>>,
745    metrics: Arc<AdmissionQueueMetrics>,
746    entries: Option<Vec<TakenEntry>>,
747    pings: Option<Vec<PendingAck>>,
748}
749
750impl TakenTransactionsGuard {
751    fn acknowledge(mut self, block_ref: BlockRef) {
752        let entries = self.entries.take().expect("acknowledgement called once");
753        let pings = self.pings.take().expect("acknowledgement called once");
754        let mut inner = self.inner.lock();
755        let Inner::Open(pool) = &mut *inner else {
756            drop(inner);
757            Self::resolve_after_close(entries, pings, "pool closed before acknowledgement");
758            return;
759        };
760
761        // Transactions occupy the block in exactly the order take() returned them,
762        // so indices are assigned contiguously across entries in that order.
763        let mut next_index = 0usize;
764        let mut watches_to_drop = Vec::with_capacity(entries.len());
765        let mut block = ProposedBlock::default();
766        for taken in entries {
767            let lane = taken.lane;
768            let mut entry = taken.entry;
769            block.entries.push(ProposedEntry {
770                lane,
771                tx_type: entry.tx_type,
772                created: entry.ack.created,
773            });
774            watches_to_drop.push(std::mem::take(&mut entry.processed));
775            let start = next_index;
776            next_index += entry.transactions.len();
777            let indices = (start..next_index)
778                .map(|index| {
779                    TransactionIndex::try_from(index)
780                        .expect("validated transaction index must fit TransactionIndex")
781                })
782                .collect::<Vec<_>>();
783            self.metrics
784                .queue_wait_latency
785                .with_label_values(&[lane.label()])
786                .observe(entry.ack.created.elapsed().as_secs_f64());
787            entry
788                .ack
789                .resolve_included(self.epoch, block_ref, indices, &mut block);
790        }
791        for ping in pings {
792            self.metrics
793                .queue_wait_latency
794                .with_label_values(&["ping"])
795                .observe(ping.created.elapsed().as_secs_f64());
796            ping.resolve_included(
797                self.epoch,
798                block_ref,
799                vec![PING_TRANSACTION_INDEX],
800                &mut block,
801            );
802        }
803        if !block.subscribers.is_empty() || !block.entries.is_empty() {
804            pool.blocks.insert(block_ref, block);
805        }
806        drop(inner);
807        drop(watches_to_drop); // drop after mutex release
808    }
809
810    // The pool closed while these entries were out with the proposer: resolve user
811    // waiters with the retriable halted error and drop system/ping acks, exactly as
812    // close() did for the entries still queued.
813    fn resolve_after_close(entries: Vec<TakenEntry>, pings: Vec<PendingAck>, reason: &'static str) {
814        for taken in entries {
815            taken
816                .entry
817                .ack
818                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
819        }
820        for ping in pings {
821            ping.drop_deliberately(reason);
822        }
823    }
824}
825
826impl Drop for TakenTransactionsGuard {
827    fn drop(&mut self) {
828        let Some(entries) = self.entries.take() else {
829            return;
830        };
831        let pings = self.pings.take().unwrap_or_default();
832        let mut inner = self.inner.lock();
833        let Inner::Open(pool) = &mut *inner else {
834            drop(inner);
835            Self::resolve_after_close(entries, pings, "pool closed before dropped acknowledgement");
836            return;
837        };
838
839        let mut requeued = 0;
840        let mut halted = Vec::new();
841        // Capacity is bypassed because these entries were already admitted. Concurrent
842        // inserts can put us over capacity, which we accept transiently.
843        for taken in entries.into_iter().rev() {
844            match taken.lane {
845                TakenLane::System => {
846                    requeued += 1;
847                    self.metrics.pool_depth.with_label_values(&["system"]).inc();
848                    self.metrics
849                        .pool_bytes
850                        .with_label_values(&["system"])
851                        .add(taken.entry.total_bytes as i64);
852                    pool.system.push_front(taken.entry);
853                    let depth = pool.system.len();
854                    if depth == SYSTEM_LANE_WARN_THRESHOLD {
855                        warn!(
856                            depth,
857                            "Consensus transaction pool system lane exceeded warning threshold"
858                        );
859                    }
860                }
861                TakenLane::User => match &mut pool.user {
862                    UserLane::Open(user) => {
863                        requeued += 1;
864                        self.metrics.pool_depth.with_label_values(&["user"]).inc();
865                        self.metrics
866                            .pool_bytes
867                            .with_label_values(&["user"])
868                            .add(taken.entry.total_bytes as i64);
869                        user.reinsert_front(taken.entry);
870                    }
871                    UserLane::Closed => halted.push(taken.entry),
872                },
873            }
874        }
875        for ping in pings.into_iter().rev() {
876            pool.pings.push_front(ping);
877            self.metrics.pool_depth.with_label_values(&["ping"]).inc();
878        }
879        self.metrics
880            .pool_requeued_on_dropped_ack
881            .inc_by(requeued as u64);
882        drop(inner);
883
884        for entry in halted {
885            entry
886                .ack
887                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
888        }
889    }
890}
891
892impl TransactionPool for ConsensusTransactionPool {
893    fn take(
894        &self,
895        max_count: usize,
896        max_bytes: usize,
897    ) -> (
898        Vec<Transaction>,
899        Box<dyn FnOnce(BlockRef) + Send>,
900        LimitReached,
901    ) {
902        // This acquires (and immediately releases) the epoch store's reconfiguration
903        // read lock. It must happen before the pool mutex is taken — never read the
904        // reconfig state while holding the pool mutex.
905        let should_accept_user_certs = self
906            .epoch_store
907            .get_reconfig_state_read_lock_guard()
908            .should_accept_user_certs();
909        #[allow(unused_mut)]
910        let mut disabled = false;
911        fail_point_if!("consensus_transaction_pool_disable_take", || {
912            disabled = true;
913        });
914        #[allow(unused_mut)]
915        let mut user_take_disabled = false;
916        fail_point_if!("consensus_transaction_pool_disable_user_take", || {
917            user_take_disabled = true;
918        });
919
920        let mut inner = self.inner.lock();
921        let pool = match &mut *inner {
922            Inner::Open(pool) => pool,
923            Inner::Closed => {
924                return (
925                    Vec::new(),
926                    Box::new(|_| {}),
927                    LimitReached::AllTransactionsIncluded,
928                );
929            }
930        };
931        let flushed = if !should_accept_user_certs {
932            pool.user.close()
933        } else {
934            Vec::new()
935        };
936        if disabled {
937            drop(inner);
938            self.resolve_flushed_user_entries(flushed); // resolve after mutex release
939            return (
940                Vec::new(),
941                Box::new(|_| {}),
942                LimitReached::AllTransactionsIncluded,
943            );
944        }
945
946        let (abandoned_pings, pings): (Vec<_>, Vec<_>) = pool
947            .pings
948            .drain(..)
949            .partition(PendingAck::is_abandoned_system);
950        self.metrics
951            .pool_depth
952            .with_label_values(&["ping"])
953            .sub((pings.len() + abandoned_pings.len()) as i64);
954
955        let mut transactions = Vec::new();
956        let mut entries = Vec::new();
957        let mut total_bytes = 0usize;
958        let mut limit_reached = LimitReached::AllTransactionsIncluded;
959
960        let mut abandoned = Vec::new();
961        while let Some(entry) = pool.system.front() {
962            // Skip entries whose submitter stopped waiting (e.g. a checkpoint
963            // signature whose checkpoint was already synced).
964            if entry.ack.is_abandoned_system() {
965                let entry = pool.system.pop_front().expect("front entry must exist");
966                self.decrement_lane_metrics("system", &entry);
967                abandoned.push(entry);
968                continue;
969            }
970            if let Some(limit) =
971                entry_limit(entry, transactions.len(), total_bytes, max_count, max_bytes)
972            {
973                limit_reached = limit;
974                break;
975            }
976            let entry = pool.system.pop_front().expect("front entry must exist");
977            total_bytes += entry.total_bytes;
978            transactions.extend(entry.transactions.iter().cloned());
979            self.decrement_lane_metrics("system", &entry);
980            entries.push(TakenEntry {
981                lane: TakenLane::System,
982                entry,
983            });
984        }
985
986        let mut already_processed = Vec::new();
987        if !user_take_disabled
988            && matches!(limit_reached, LimitReached::AllTransactionsIncluded)
989            && let UserLane::Open(user) = &mut pool.user
990        {
991            let mut pending_count = transactions.len();
992            let mut pending_bytes = total_bytes;
993            let popped;
994            (popped, already_processed) = user.pop_batch_while(|entry| {
995                // An already-processed entry is excluded without consuming block budget.
996                if all_processed(&mut entry.processed).is_some() {
997                    return PopAction::Exclude;
998                }
999                match entry_limit(entry, pending_count, pending_bytes, max_count, max_bytes) {
1000                    Some(limit) => {
1001                        limit_reached = limit;
1002                        PopAction::Stop
1003                    }
1004                    None => {
1005                        pending_count += entry.transactions.len();
1006                        pending_bytes += entry.total_bytes;
1007                        PopAction::Include
1008                    }
1009                }
1010            });
1011            for entry in popped {
1012                self.decrement_lane_metrics("user", &entry);
1013                transactions.extend(entry.transactions.iter().cloned());
1014                entries.push(TakenEntry {
1015                    lane: TakenLane::User,
1016                    entry,
1017                });
1018            }
1019        }
1020        drop(inner);
1021
1022        // Resolve flushed/processed items outside the mutex.
1023        self.resolve_flushed_user_entries(flushed);
1024        for entry in abandoned {
1025            self.metrics
1026                .pool_abandoned
1027                .with_label_values(&["system"])
1028                .inc();
1029            entry
1030                .ack
1031                .drop_deliberately("submitter stopped waiting before proposal");
1032        }
1033        for ping in abandoned_pings {
1034            self.metrics
1035                .pool_abandoned
1036                .with_label_values(&["ping"])
1037                .inc();
1038            ping.drop_deliberately("submitter stopped waiting before proposal");
1039        }
1040        for mut entry in already_processed {
1041            self.decrement_lane_metrics("user", &entry);
1042            let method = all_processed(&mut entry.processed)
1043                .expect("excluded entries are already processed");
1044            self.metrics
1045                .pool_already_processed
1046                .with_label_values(&["proposal", method.metric_label()])
1047                .inc();
1048            let error = processing_error(
1049                entry.processed.iter().map(|watch| watch.consensus.key()),
1050                method,
1051            );
1052            entry.ack.resolve_error(error);
1053        }
1054
1055        self.metrics
1056            .pool_taken_per_proposal
1057            .observe(transactions.len() as f64);
1058        let guard = TakenTransactionsGuard {
1059            epoch: self.epoch(),
1060            inner: self.inner.clone(),
1061            metrics: self.metrics.clone(),
1062            entries: Some(entries),
1063            pings: Some(pings),
1064        };
1065        (
1066            transactions,
1067            Box::new(move |block_ref| guard.acknowledge(block_ref)),
1068            limit_reached,
1069        )
1070    }
1071
1072    // Same semantics as consensus-core's TransactionConsumer::notify_own_blocks_status:
1073    // committed blocks resolve Sequenced first, then every remaining subscription at
1074    // round <= gc_round resolves GarbageCollected (those blocks can never commit).
1075    fn notify_committed(&self, own_committed_blocks: Vec<BlockRef>, gc_round: Round) {
1076        let mut sequenced = Vec::new();
1077        let mut garbage_collected = Vec::new();
1078        {
1079            let mut inner = self.inner.lock();
1080            let Inner::Open(pool) = &mut *inner else {
1081                return;
1082            };
1083            for block_ref in own_committed_blocks {
1084                if let Some(block) = pool.blocks.remove(&block_ref) {
1085                    for subscriber in block.subscribers {
1086                        let _ = subscriber.send(BlockStatus::Sequenced(block_ref));
1087                    }
1088                    sequenced.extend(block.entries);
1089                }
1090            }
1091            while let Some((block_ref, block)) = pool.blocks.pop_first() {
1092                if block_ref.round > gc_round {
1093                    pool.blocks.insert(block_ref, block);
1094                    break;
1095                }
1096                self.metrics
1097                    .pool_gc_notified
1098                    .inc_by(block.subscribers.len() as u64);
1099                for subscriber in block.subscribers {
1100                    let _ = subscriber.send(BlockStatus::GarbageCollected(block_ref));
1101                }
1102                garbage_collected.extend(block.entries);
1103            }
1104        }
1105
1106        // Metrics are updated with the mutex released.
1107        self.report_proposed_status(&sequenced, "sequenced");
1108        self.report_proposed_status(&garbage_collected, "garbage_collected");
1109        self.report_commit_latency(&sequenced);
1110    }
1111}
1112
1113impl ConsensusTransactionPool {
1114    /// Reports user entries to `sequencing_certificate_status*` metrics.
1115    /// System entries are reported by the adapter itself, which waits on their block
1116    /// status.
1117    fn report_proposed_status(&self, entries: &[ProposedEntry], status: &'static str) {
1118        let mut counts: Vec<(&'static str, u64)> = Vec::new();
1119        for entry in entries
1120            .iter()
1121            .filter(|entry| matches!(entry.lane, TakenLane::User))
1122        {
1123            match counts
1124                .iter_mut()
1125                .find(|(tx_type, _)| *tx_type == entry.tx_type)
1126            {
1127                Some((_, count)) => *count += 1,
1128                None => counts.push((entry.tx_type, 1)),
1129            }
1130        }
1131        for (tx_type, count) in counts {
1132            self.adapter_metrics
1133                .sequencing_certificate_status
1134                .with_label_values(&[tx_type, status])
1135                .inc_by(count);
1136        }
1137    }
1138
1139    fn report_commit_latency(&self, entries: &[ProposedEntry]) {
1140        let now = Instant::now();
1141        let user = self
1142            .metrics
1143            .pool_commit_latency
1144            .with_label_values(&["user"]);
1145        let system = self
1146            .metrics
1147            .pool_commit_latency
1148            .with_label_values(&["system"]);
1149        for entry in entries {
1150            let histogram = match entry.lane {
1151                TakenLane::User => &user,
1152                TakenLane::System => &system,
1153            };
1154            histogram.observe(now.saturating_duration_since(entry.created).as_secs_f64());
1155        }
1156    }
1157}
1158
1159/// Which block limit the entry would exceed, if any. Entries are all-or-nothing:
1160/// a bundle that does not fit stays queued in full for the next proposal.
1161fn entry_limit(
1162    entry: &PoolEntry,
1163    current_count: usize,
1164    current_bytes: usize,
1165    max_count: usize,
1166    max_bytes: usize,
1167) -> Option<LimitReached> {
1168    if current_bytes.saturating_add(entry.total_bytes) > max_bytes {
1169        return Some(LimitReached::MaxBytes);
1170    }
1171    if current_count.saturating_add(entry.transactions.len()) > max_count {
1172        return Some(LimitReached::MaxNumOfTransactions);
1173    }
1174    None
1175}
1176
1177fn consensus_client_error(error: ClientError) -> SuiError {
1178    SuiErrorKind::FailedToSubmitToConsensus(error.to_string()).into()
1179}
1180
1181#[derive(Clone)]
1182enum PoolState {
1183    /// Before the first consensus start.
1184    Absent,
1185    Active(EpochId, Arc<ConsensusTransactionPool>),
1186    /// No pool will be installed for this epoch (the node is not a validator, or is
1187    /// shutting down); waiters must fail rather than wait for it.
1188    Unavailable(EpochId),
1189}
1190
1191/// Process-lifetime handle connecting submitters to the current epoch's pool.
1192/// The watch channel is the single authoritative state, always updated with
1193/// `send_replace` so an installation is never lost while no receiver is subscribed.
1194///
1195/// This closes the reconfiguration window in which the new epoch store is already
1196/// published but the new epoch's consensus (and pool) has not started yet: callers
1197/// wait on the watch instead of failing or, worse, inserting into the previous
1198/// epoch's still-installed pool.
1199pub struct TransactionPoolContext {
1200    state: watch::Sender<PoolState>,
1201    metrics: Arc<AdmissionQueueMetrics>,
1202    adapter_metrics: ConsensusAdapterMetrics,
1203}
1204
1205impl TransactionPoolContext {
1206    pub fn new(
1207        metrics: Arc<AdmissionQueueMetrics>,
1208        adapter_metrics: ConsensusAdapterMetrics,
1209    ) -> Self {
1210        let (state, _) = watch::channel(PoolState::Absent);
1211        Self {
1212            state,
1213            metrics,
1214            adapter_metrics,
1215        }
1216    }
1217
1218    pub fn set_active(&self, epoch: EpochId, pool: Arc<ConsensusTransactionPool>) {
1219        assert_eq!(epoch, pool.epoch());
1220        drop(self.state.send_replace(PoolState::Active(epoch, pool)));
1221    }
1222
1223    /// Marks `epoch` as one that will never get a pool, promptly failing current and
1224    /// future waiters that would otherwise hang until their transport deadline.
1225    pub fn set_unavailable(&self, epoch: EpochId) {
1226        drop(self.state.send_replace(PoolState::Unavailable(epoch)));
1227    }
1228
1229    pub async fn try_insert(
1230        &self,
1231        epoch: EpochId,
1232        gas_price: u64,
1233        transactions: Vec<ConsensusTransaction>,
1234    ) -> SuiResult<(PositionReceiver, bool)> {
1235        self.wait_for_pool(epoch)
1236            .await?
1237            .try_insert(epoch, gas_price, transactions)
1238    }
1239
1240    /// Resolves the pool for `caller_epoch`. Waits while the state is absent or older
1241    /// than the caller — e.g. during the gap between epoch-store publication and consensus
1242    /// start. Fails immediately with `ValidatorHaltedAtEpochEnd` when the state is *newer*
1243    /// (the caller raced with a stale epoch store and should re-validate), and with a
1244    /// retriable overload error when this epoch is `Unavailable` (client retries
1245    /// against another validator). Bounded only by the caller's own deadline or
1246    /// cancellation.
1247    pub async fn wait_for_pool(
1248        &self,
1249        caller_epoch: EpochId,
1250    ) -> SuiResult<Arc<ConsensusTransactionPool>> {
1251        self.wait_for_pool_after_subscribe(caller_epoch, || {})
1252            .await
1253    }
1254
1255    async fn wait_for_pool_after_subscribe(
1256        &self,
1257        caller_epoch: EpochId,
1258        after_subscribe: impl FnOnce(),
1259    ) -> SuiResult<Arc<ConsensusTransactionPool>> {
1260        let mut receiver = self.state.subscribe();
1261        after_subscribe();
1262        let start = Instant::now();
1263        loop {
1264            let state = receiver.borrow_and_update().clone();
1265            match state {
1266                PoolState::Active(epoch, pool) if epoch == caller_epoch => {
1267                    return Ok(pool);
1268                }
1269                PoolState::Active(epoch, _) if epoch > caller_epoch => {
1270                    return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1271                }
1272                PoolState::Unavailable(epoch) if epoch > caller_epoch => {
1273                    return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1274                }
1275                PoolState::Unavailable(epoch) if epoch == caller_epoch => {
1276                    return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
1277                }
1278                PoolState::Absent | PoolState::Active(_, _) | PoolState::Unavailable(_) => {}
1279            }
1280
1281            let _waiting = WaitingInsertGuard::new(self.metrics.pool_waiting_inserts.clone());
1282            match tokio::time::timeout(Duration::from_secs(10), receiver.changed()).await {
1283                Ok(Ok(())) => {}
1284                Ok(Err(_)) => {
1285                    // Unreachable: `changed()` errors only when the watch sender is dropped,
1286                    // but the sender is a field of this context and `&self` is borrowed
1287                    // across this await. All real teardown paths publish a state change
1288                    // (`set_active` / `set_unavailable`) instead of dropping the sender.
1289                    debug_fatal!("transaction pool watch sender dropped while context is alive");
1290                    return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
1291                }
1292                Err(_) => {
1293                    warn!(
1294                        caller_epoch,
1295                        elapsed = ?start.elapsed(),
1296                        "Waiting for consensus transaction pool to initialize"
1297                    );
1298                }
1299            }
1300        }
1301    }
1302
1303    pub fn metrics(&self) -> &Arc<AdmissionQueueMetrics> {
1304        &self.metrics
1305    }
1306
1307    pub fn adapter_metrics(&self) -> &ConsensusAdapterMetrics {
1308        &self.adapter_metrics
1309    }
1310
1311    #[cfg(test)]
1312    pub(crate) fn new_for_tests(metrics: Arc<AdmissionQueueMetrics>) -> Self {
1313        Self::new(metrics, ConsensusAdapterMetrics::new_test())
1314    }
1315}
1316
1317struct WaitingInsertGuard {
1318    gauge: IntGauge,
1319}
1320
1321impl WaitingInsertGuard {
1322    fn new(gauge: IntGauge) -> Self {
1323        gauge.inc();
1324        Self { gauge }
1325    }
1326}
1327
1328impl Drop for WaitingInsertGuard {
1329    fn drop(&mut self) {
1330        self.gauge.dec();
1331    }
1332}
1333
1334/// The `ConsensusClient` installed in pull mode in place of `LazyMysticetiClient`,
1335/// so the unchanged `ConsensusAdapter` (system transactions, pings, GC retries)
1336/// submits into the pool. Wraps the context rather than one pool, resolving the
1337/// right epoch's pool per submission.
1338pub struct TransactionPoolClient {
1339    context: Arc<TransactionPoolContext>,
1340}
1341
1342impl TransactionPoolClient {
1343    pub fn new(context: Arc<TransactionPoolContext>) -> Self {
1344        Self { context }
1345    }
1346}
1347
1348#[async_trait]
1349impl ConsensusClient for TransactionPoolClient {
1350    async fn submit(
1351        &self,
1352        transactions: &[ConsensusTransaction],
1353        epoch_store: &Arc<AuthorityPerEpochStore>,
1354    ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)> {
1355        let epoch = epoch_store.epoch();
1356        let pool = self.context.wait_for_pool(epoch).await?;
1357        let receiver = pool.submit(epoch, transactions)?;
1358        let (block_ref, indices, status_receiver) = receiver.await.map_err(|error| {
1359            consensus_client_error(ClientError::ConsensusShuttingDown(error.to_string()))
1360        })?;
1361        let positions = indices
1362            .into_iter()
1363            .map(|index| ConsensusPosition {
1364                epoch,
1365                block: block_ref,
1366                index,
1367            })
1368            .collect();
1369        Ok((positions, status_receiver))
1370    }
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375    use super::*;
1376    use crate::authority::AuthorityState;
1377    use crate::authority::test_authority_builder::TestAuthorityBuilder;
1378    use consensus_config::AuthorityIndex;
1379    use consensus_types::block::BlockDigest;
1380    use sui_protocol_config::ProtocolConfig;
1381    use sui_types::base_types::{AuthorityName, random_object_ref};
1382    use sui_types::crypto::{AccountKeyPair, get_key_pair};
1383    use sui_types::transaction::PlainTransactionWithClaims;
1384
1385    async fn test_state_and_pool(
1386        capacity: usize,
1387    ) -> (Arc<AuthorityState>, Arc<ConsensusTransactionPool>) {
1388        let state = TestAuthorityBuilder::new().build().await;
1389        let pool = pool_for_current_epoch(&state, capacity);
1390        (state, pool)
1391    }
1392
1393    async fn test_state_and_pool_with_protocol_config(
1394        capacity: usize,
1395        protocol_config: ProtocolConfig,
1396    ) -> (Arc<AuthorityState>, Arc<ConsensusTransactionPool>) {
1397        let state = TestAuthorityBuilder::new()
1398            .with_protocol_config(protocol_config)
1399            .build()
1400            .await;
1401        let pool = pool_for_current_epoch(&state, capacity);
1402        (state, pool)
1403    }
1404
1405    fn pool_for_current_epoch(
1406        state: &Arc<AuthorityState>,
1407        capacity: usize,
1408    ) -> Arc<ConsensusTransactionPool> {
1409        Arc::new(ConsensusTransactionPool::new_for_tests(
1410            state.epoch_store_for_testing().clone(),
1411            capacity,
1412            Arc::new(AdmissionQueueMetrics::new_for_tests()),
1413        ))
1414    }
1415
1416    fn transaction() -> ConsensusTransaction {
1417        ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO)
1418    }
1419
1420    fn user_transaction() -> ConsensusTransaction {
1421        let (sender, keypair) = get_key_pair::<AccountKeyPair>();
1422        let transaction = crate::test_utils::make_transfer_sui_transaction(
1423            random_object_ref(),
1424            sender,
1425            Some(1),
1426            sender,
1427            &keypair,
1428            1,
1429        );
1430        ConsensusTransaction::new_user_transaction_v2_message(
1431            &AuthorityName::ZERO,
1432            PlainTransactionWithClaims::no_aliases(transaction),
1433        )
1434    }
1435
1436    fn block(round: Round) -> BlockRef {
1437        BlockRef::new(round, AuthorityIndex::new_for_test(0), BlockDigest::MIN)
1438    }
1439
1440    fn consensus_key(transaction: &ConsensusTransaction) -> SequencedConsensusTransactionKey {
1441        SequencedConsensusTransactionKey::External(transaction.key())
1442    }
1443
1444    fn digest_of(transaction: &ConsensusTransaction) -> TransactionDigest {
1445        consensus_key(transaction)
1446            .user_transaction_digest()
1447            .expect("expected a user transaction key")
1448    }
1449
1450    #[cfg(debug_assertions)]
1451    #[tokio::test]
1452    #[should_panic(expected = "dropped without resolution")]
1453    async fn unresolved_pending_ack_panics() {
1454        let (sender, _receiver) = oneshot::channel();
1455        drop(PendingAck::new(
1456            EntryAck::User(sender),
1457            vec![transaction().key()],
1458        ));
1459    }
1460
1461    #[tokio::test]
1462    async fn user_lane_close_returns_pending_entries_and_closes_lane() {
1463        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1464        let mut lane = UserLane::Open(PriorityAdmissionQueue::new(1, metrics.clone()));
1465        let consensus_transaction = transaction();
1466        let serialized = bcs::to_bytes(&consensus_transaction).unwrap();
1467        let (sender, receiver) = oneshot::channel();
1468        let entry = PoolEntry {
1469            transactions: vec![Transaction::new(serialized.clone())],
1470            total_bytes: serialized.len(),
1471            gas_price: 1,
1472            tx_type: tx_type_label(std::slice::from_ref(&consensus_transaction)),
1473            ack: PendingAck::new(EntryAck::User(sender), vec![consensus_transaction.key()]),
1474            metrics,
1475            processed: Vec::new(),
1476        };
1477        let UserLane::Open(user) = &mut lane else {
1478            unreachable!("new user lane must be open");
1479        };
1480        user.insert(entry).unwrap();
1481
1482        let mut entries = lane.close();
1483        assert!(matches!(lane, UserLane::Closed));
1484        assert_eq!(entries.len(), 1);
1485        entries
1486            .pop()
1487            .unwrap()
1488            .ack
1489            .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1490        assert!(matches!(
1491            receiver.await.unwrap().unwrap_err().as_inner(),
1492            SuiErrorKind::ValidatorHaltedAtEpochEnd
1493        ));
1494    }
1495
1496    #[tokio::test]
1497    async fn locked_user_recheck_defuses_ack_after_pool_close() {
1498        let (_state, pool) = test_state_and_pool(1).await;
1499        pool.close();
1500        // The armed ack constructed inside try_insert must be defused when the locked
1501        // check fails — a regression detonates the drop bomb and panics this test.
1502        assert!(matches!(
1503            pool.try_insert(pool.epoch(), 1, vec![transaction()])
1504                .unwrap_err()
1505                .as_inner(),
1506            SuiErrorKind::ValidatorHaltedAtEpochEnd
1507        ));
1508    }
1509
1510    #[tokio::test]
1511    async fn take_respects_priority_budgets_and_acknowledges_positions() {
1512        let (_state, pool) = test_state_and_pool(10).await;
1513        let epoch = pool.epoch();
1514        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
1515        assert_eq!(pool.queue_depth("system"), 1);
1516        let (low_receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1517        let (high_receiver, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
1518
1519        let (transactions, ack, limit) = pool.take(2, usize::MAX);
1520        assert_eq!(transactions.len(), 2);
1521        assert_eq!(limit, LimitReached::MaxNumOfTransactions);
1522        assert_eq!(pool.queue_depth("system"), 0);
1523        let block_ref = block(5);
1524        ack(block_ref);
1525
1526        let (system_block, system_indices, system_status) = system_receiver.await.unwrap();
1527        assert_eq!(system_block, block_ref);
1528        assert_eq!(system_indices, vec![0]);
1529        let positions = high_receiver.await.unwrap().unwrap();
1530        assert_eq!(
1531            positions,
1532            vec![ConsensusPosition {
1533                epoch,
1534                block: block_ref,
1535                index: 1,
1536            }]
1537        );
1538        pool.notify_committed(vec![block_ref], 0);
1539        assert_eq!(
1540            system_status.await.unwrap(),
1541            BlockStatus::Sequenced(block_ref)
1542        );
1543
1544        let serialized_len = bcs::to_bytes(&transaction()).unwrap().len();
1545        let (transactions, ack, limit) = pool.take(1, serialized_len - 1);
1546        assert!(transactions.is_empty());
1547        assert_eq!(limit, LimitReached::MaxBytes);
1548        drop(ack);
1549
1550        let (transactions, ack, limit) = pool.take(1, serialized_len);
1551        assert_eq!(transactions.len(), 1);
1552        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
1553        drop(ack);
1554        assert_eq!(pool.queue_depth("user"), 1);
1555
1556        let (transactions, ack, _) = pool.take(1, serialized_len);
1557        assert_eq!(transactions.len(), 1);
1558        ack(block(6));
1559        assert_eq!(low_receiver.await.unwrap().unwrap()[0].index, 0);
1560    }
1561
1562    #[tokio::test]
1563    async fn bundles_are_atomic() {
1564        let (_state, pool) = test_state_and_pool(10).await;
1565        let (_receiver, _) = pool
1566            .try_insert(pool.epoch(), 10, vec![transaction(), transaction()])
1567            .unwrap();
1568        let (transactions, ack, limit) = pool.take(1, usize::MAX);
1569        assert!(transactions.is_empty());
1570        assert_eq!(limit, LimitReached::MaxNumOfTransactions);
1571        drop(ack);
1572
1573        let bundle_bytes = 2 * bcs::to_bytes(&transaction()).unwrap().len();
1574        let (transactions, ack, limit) = pool.take(2, bundle_bytes - 1);
1575        assert!(transactions.is_empty());
1576        assert_eq!(limit, LimitReached::MaxBytes);
1577        drop(ack);
1578
1579        let (transactions, ack, _) = pool.take(2, bundle_bytes);
1580        assert_eq!(transactions.len(), 2);
1581        drop(ack);
1582        pool.close();
1583    }
1584
1585    #[tokio::test]
1586    async fn dropped_ack_preserves_front_order() {
1587        let (_state, pool) = test_state_and_pool(10).await;
1588        let epoch = pool.epoch();
1589        let (first, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1590        let (mut second, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1591        let (_, ack, _) = pool.take(2, usize::MAX);
1592        drop(ack);
1593
1594        let (_, ack, _) = pool.take(1, usize::MAX);
1595        ack(block(1));
1596        assert_eq!(first.await.unwrap().unwrap()[0].index, 0);
1597        assert!(second.try_recv().is_err());
1598        pool.close();
1599    }
1600
1601    #[tokio::test]
1602    async fn ping_capacity_and_reserved_index() {
1603        let (_state, pool) = test_state_and_pool(1).await;
1604        let epoch = pool.epoch();
1605        let mut receivers = Vec::with_capacity(MAX_PENDING_PINGS);
1606        for _ in 0..MAX_PENDING_PINGS {
1607            receivers.push(pool.submit(epoch, &[]).unwrap());
1608        }
1609        assert!(matches!(
1610            pool.submit(epoch, &[]).err().unwrap().as_inner(),
1611            SuiErrorKind::TooManyTransactionsPendingConsensus
1612        ));
1613
1614        let (transactions, ack, _) = pool.take(0, 0);
1615        assert!(transactions.is_empty());
1616        let block_ref = block(1);
1617        ack(block_ref);
1618        let (ping_block, indices, status) = receivers.remove(0).await.unwrap();
1619        assert_eq!(ping_block, block_ref);
1620        assert_eq!(indices, vec![PING_TRANSACTION_INDEX]);
1621        pool.notify_committed(vec![block_ref], 0);
1622        assert_eq!(status.await.unwrap(), BlockStatus::Sequenced(block_ref));
1623    }
1624
1625    #[tokio::test]
1626    async fn system_lane_is_not_bounded_by_user_capacity() {
1627        let (_state, pool) = test_state_and_pool(1).await;
1628        let receivers = (0..3)
1629            .map(|_| pool.submit(pool.epoch(), &[transaction()]).unwrap())
1630            .collect::<Vec<_>>();
1631        assert_eq!(pool.queue_depth("system"), 3);
1632        let (transactions, ack, _) = pool.take(3, usize::MAX);
1633        assert_eq!(transactions.len(), 3);
1634        ack(block(1));
1635        for receiver in receivers {
1636            assert_eq!(receiver.await.unwrap().1.len(), 1);
1637        }
1638    }
1639
1640    #[tokio::test]
1641    async fn take_skips_abandoned_system_entries_and_pings() {
1642        let (_state, pool) = test_state_and_pool(1).await;
1643        let epoch = pool.epoch();
1644
1645        // Abandoned entry first, so skipping it must not consume the count budget
1646        // of take(1, ..) below.
1647        let stale = pool.submit(epoch, &[transaction()]).unwrap();
1648        drop(stale);
1649        let live = pool.submit(epoch, &[transaction()]).unwrap();
1650        let stale_ping = pool.submit(epoch, &[]).unwrap();
1651        drop(stale_ping);
1652        let live_ping = pool.submit(epoch, &[]).unwrap();
1653
1654        let (transactions, ack, _) = pool.take(1, usize::MAX);
1655        assert_eq!(transactions.len(), 1);
1656        assert_eq!(pool.queue_depth("system"), 0);
1657        assert_eq!(pool.queue_depth("ping"), 0);
1658        assert_eq!(pool.abandoned_count("system"), 1);
1659        assert_eq!(pool.abandoned_count("ping"), 1);
1660
1661        ack(block(1));
1662        assert_eq!(live.await.unwrap().1.len(), 1);
1663        assert_eq!(live_ping.await.unwrap().1, vec![PING_TRANSACTION_INDEX]);
1664    }
1665
1666    #[tokio::test]
1667    async fn eviction_rejection_and_duplicate_detection_match_admission_queue() {
1668        let (_state, pool) = test_state_and_pool(2).await;
1669        let epoch = pool.epoch();
1670        let (first, newly_inserted) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1671        assert!(newly_inserted);
1672        let (_second, newly_inserted) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1673        assert!(!newly_inserted);
1674
1675        let (_high, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
1676        assert!(matches!(
1677            first.await.unwrap().unwrap_err().as_inner(),
1678            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 20 }
1679        ));
1680        assert!(matches!(
1681            pool.try_insert(epoch, 10, vec![transaction()])
1682                .err()
1683                .unwrap()
1684                .as_inner(),
1685            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 10 }
1686        ));
1687        pool.close();
1688    }
1689
1690    #[tokio::test]
1691    async fn close_resolves_pending_and_taken_user_entries() {
1692        let (_state, pool) = test_state_and_pool(10).await;
1693        let epoch = pool.epoch();
1694        for wrong_epoch in [epoch + 1, epoch + 2] {
1695            assert!(matches!(
1696                pool.try_insert(wrong_epoch, 1, vec![transaction()])
1697                    .err()
1698                    .unwrap()
1699                    .as_inner(),
1700                SuiErrorKind::ValidatorHaltedAtEpochEnd
1701            ));
1702        }
1703        let (pending, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1704        let (taken, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
1705        let (_, ack, _) = pool.take(1, usize::MAX);
1706        pool.close();
1707
1708        assert!(matches!(
1709            pending.await.unwrap().unwrap_err().as_inner(),
1710            SuiErrorKind::ValidatorHaltedAtEpochEnd
1711        ));
1712        ack(block(1));
1713        assert!(matches!(
1714            taken.await.unwrap().unwrap_err().as_inner(),
1715            SuiErrorKind::ValidatorHaltedAtEpochEnd
1716        ));
1717        assert!(matches!(
1718            pool.try_insert(epoch, 30, vec![transaction()])
1719                .err()
1720                .unwrap()
1721                .as_inner(),
1722            SuiErrorKind::ValidatorHaltedAtEpochEnd
1723        ));
1724    }
1725
1726    #[tokio::test]
1727    async fn dropped_ack_after_close_does_not_requeue() {
1728        let (_state, pool) = test_state_and_pool(10).await;
1729        let epoch = pool.epoch();
1730        let (receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1731        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
1732        let (_, ack, _) = pool.take(2, usize::MAX);
1733        pool.close();
1734        drop(ack);
1735        assert!(matches!(
1736            receiver.await.unwrap().unwrap_err().as_inner(),
1737            SuiErrorKind::ValidatorHaltedAtEpochEnd
1738        ));
1739        assert!(system_receiver.await.is_err());
1740        assert_eq!(pool.queue_depth("user"), 0);
1741    }
1742
1743    #[tokio::test]
1744    async fn take_closes_user_lane_when_epoch_stops_accepting_user_certs() {
1745        let (state, pool) = test_state_and_pool(10).await;
1746        let epoch = pool.epoch();
1747        let (user_receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1748        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
1749
1750        let epoch_store = state.epoch_store_for_testing();
1751        epoch_store.close_user_certs_for_manual_epoch_close(
1752            epoch_store.get_reconfig_state_write_lock_guard(),
1753        );
1754        let (transactions, ack, limit) = pool.take(10, usize::MAX);
1755        assert_eq!(transactions.len(), 1);
1756        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
1757        assert!(matches!(
1758            user_receiver.await.unwrap().unwrap_err().as_inner(),
1759            SuiErrorKind::ValidatorHaltedAtEpochEnd
1760        ));
1761        assert!(matches!(
1762            pool.try_insert(epoch, 20, vec![transaction()])
1763                .err()
1764                .unwrap()
1765                .as_inner(),
1766            SuiErrorKind::ValidatorHaltedAtEpochEnd
1767        ));
1768
1769        let block_ref = block(1);
1770        ack(block_ref);
1771        assert_eq!(system_receiver.await.unwrap().0, block_ref);
1772    }
1773
1774    #[tokio::test]
1775    async fn dropped_ack_after_user_lane_close_resolves_taken_entries() {
1776        let (state, pool) = test_state_and_pool(10).await;
1777        let epoch = pool.epoch();
1778        let (taken, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
1779        let (transactions, ack, _) = pool.take(1, usize::MAX);
1780        assert_eq!(transactions.len(), 1);
1781
1782        // Close the user lane while the taken entry is still out with the proposer.
1783        let epoch_store = state.epoch_store_for_testing();
1784        epoch_store.close_user_certs_for_manual_epoch_close(
1785            epoch_store.get_reconfig_state_write_lock_guard(),
1786        );
1787        let (flushed, flush_ack, _) = pool.take(10, usize::MAX);
1788        assert!(flushed.is_empty());
1789        drop(flush_ack);
1790
1791        // The dropped ack must resolve the taken entry with the halted error instead
1792        // of re-queueing it into the closed lane.
1793        drop(ack);
1794        assert!(matches!(
1795            taken.await.unwrap().unwrap_err().as_inner(),
1796            SuiErrorKind::ValidatorHaltedAtEpochEnd
1797        ));
1798        assert_eq!(pool.queue_depth("user"), 0);
1799    }
1800
1801    #[tokio::test]
1802    async fn notify_committed_sequences_commits_before_garbage_collection() {
1803        let (_state, pool) = test_state_and_pool(10).await;
1804        let epoch = pool.epoch();
1805        let first = pool.submit(epoch, &[transaction()]).unwrap();
1806        let (_, ack, _) = pool.take(1, usize::MAX);
1807        let first_block = block(1);
1808        ack(first_block);
1809        let (_, _, first_status) = first.await.unwrap();
1810
1811        let second = pool.submit(epoch, &[transaction()]).unwrap();
1812        let (_, ack, _) = pool.take(1, usize::MAX);
1813        let second_block = block(2);
1814        ack(second_block);
1815        let (_, _, second_status) = second.await.unwrap();
1816
1817        pool.notify_committed(vec![second_block], 1);
1818        assert_eq!(
1819            second_status.await.unwrap(),
1820            BlockStatus::Sequenced(second_block)
1821        );
1822        assert_eq!(
1823            first_status.await.unwrap(),
1824            BlockStatus::GarbageCollected(first_block)
1825        );
1826    }
1827
1828    #[tokio::test]
1829    async fn notify_committed_reports_proposed_user_outcomes() {
1830        let (_state, pool) = test_state_and_pool(10).await;
1831        let epoch = pool.epoch();
1832        let status = |status: &str| {
1833            pool.adapter_metrics
1834                .sequencing_certificate_status
1835                .with_label_values(&["owned_user_transaction_v2", status])
1836                .get()
1837        };
1838        let commit_latency_count = |lane: &str| {
1839            pool.metrics
1840                .pool_commit_latency
1841                .with_label_values(&[lane])
1842                .get_sample_count()
1843        };
1844
1845        // Block 1: one user transaction, later garbage collected.
1846        let (_first, _) = pool.try_insert(epoch, 1, vec![user_transaction()]).unwrap();
1847        let (_, ack, _) = pool.take(1, usize::MAX);
1848        ack(block(1));
1849        // Block 2: one user transaction and one system transaction, committed.
1850        let (_second, _) = pool.try_insert(epoch, 1, vec![user_transaction()]).unwrap();
1851        let _system = pool.submit(epoch, &[transaction()]).unwrap();
1852        let (_, ack, _) = pool.take(2, usize::MAX);
1853        ack(block(2));
1854        assert_eq!(status("sequenced"), 0);
1855
1856        pool.notify_committed(vec![block(2)], 1);
1857        assert_eq!(status("sequenced"), 1);
1858        assert_eq!(status("garbage_collected"), 1);
1859        // System entries report through the adapter; only their latency is recorded here.
1860        assert_eq!(commit_latency_count("user"), 1);
1861        assert_eq!(commit_latency_count("system"), 1);
1862    }
1863
1864    #[tokio::test]
1865    async fn insert_validation_rejects_oversized_transactions_and_bundles() {
1866        let serialized_len = u64::try_from(bcs::to_bytes(&transaction()).unwrap().len()).unwrap();
1867
1868        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
1869        config.set_consensus_max_transaction_size_bytes_for_testing(serialized_len - 1);
1870        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
1871        assert!(
1872            pool.try_insert(pool.epoch(), 1, vec![transaction()])
1873                .err()
1874                .unwrap()
1875                .to_string()
1876                .contains("Transaction size")
1877        );
1878
1879        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
1880        config.set_consensus_max_num_transactions_in_block_for_testing(1);
1881        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
1882        assert!(
1883            pool.try_insert(pool.epoch(), 1, vec![transaction(), transaction()])
1884                .err()
1885                .unwrap()
1886                .to_string()
1887                .contains("bundle count")
1888        );
1889
1890        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
1891        config.set_consensus_max_transactions_in_block_bytes_for_testing(serialized_len);
1892        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
1893        assert!(
1894            pool.try_insert(pool.epoch(), 1, vec![transaction(), transaction()])
1895                .err()
1896                .unwrap()
1897                .to_string()
1898                .contains("bundle size")
1899        );
1900    }
1901
1902    #[cfg(debug_assertions)]
1903    #[tokio::test]
1904    #[should_panic(expected = "system transaction failed validation")]
1905    async fn oversized_system_transaction_is_an_invariant_violation() {
1906        let serialized_len = u64::try_from(bcs::to_bytes(&transaction()).unwrap().len()).unwrap();
1907        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
1908        config.set_consensus_max_transaction_size_bytes_for_testing(serialized_len - 1);
1909        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
1910        let _ = pool.submit(pool.epoch(), &[transaction()]);
1911    }
1912
1913    #[tokio::test]
1914    async fn take_skips_already_processed_entries_without_shrinking_the_block() {
1915        let (state, pool) = test_state_and_pool(10).await;
1916        let epoch = pool.epoch();
1917        let epoch_store = state.epoch_store_for_testing();
1918
1919        // Both bid highest, so they head the pop order and would consume the whole
1920        // budget below if they were not skipped — one observed through consensus
1921        // output, one through checkpoint execution.
1922        let processed = user_transaction();
1923        let (processed_consensus, _) = pool
1924            .try_insert(epoch, 100, vec![processed.clone()])
1925            .unwrap();
1926        let executed = user_transaction();
1927        let (processed_checkpoint, _) = pool.try_insert(epoch, 90, vec![executed.clone()]).unwrap();
1928        let live = (0..3)
1929            .map(|_| {
1930                pool.try_insert(epoch, 10, vec![user_transaction()])
1931                    .unwrap()
1932                    .0
1933            })
1934            .collect::<Vec<_>>();
1935
1936        epoch_store.process_notifications(std::iter::once(&consensus_key(&processed)));
1937        epoch_store
1938            .insert_finalized_transactions(&[digest_of(&executed)], 1)
1939            .unwrap();
1940
1941        let (transactions, ack, limit) = pool.take(3, usize::MAX);
1942        assert_eq!(transactions.len(), 3);
1943        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
1944        assert_eq!(pool.queue_depth("user"), 0);
1945        assert_eq!(
1946            pool.already_processed_count("proposal", "consensus_message"),
1947            1
1948        );
1949        assert_eq!(
1950            pool.already_processed_count("proposal", "checkpoint_execution"),
1951            1
1952        );
1953        for receiver in [processed_consensus, processed_checkpoint] {
1954            assert!(matches!(
1955                receiver.await.unwrap().unwrap_err().as_inner(),
1956                SuiErrorKind::TransactionProcessing { .. }
1957            ));
1958        }
1959
1960        ack(block(1));
1961        for receiver in live {
1962            assert_eq!(receiver.await.unwrap().unwrap().len(), 1);
1963        }
1964    }
1965
1966    #[tokio::test]
1967    async fn bundles_are_skipped_only_when_every_transaction_is_already_processed() {
1968        let (state, pool) = test_state_and_pool(10).await;
1969        let epoch = pool.epoch();
1970        let epoch_store = state.epoch_store_for_testing();
1971
1972        let first = user_transaction();
1973        let second = user_transaction();
1974        let (receiver, _) = pool
1975            .try_insert(epoch, 10, vec![first.clone(), second.clone()])
1976            .unwrap();
1977
1978        epoch_store.process_notifications(std::iter::once(&consensus_key(&first)));
1979        let (transactions, ack, _) = pool.take(10, usize::MAX);
1980        assert_eq!(transactions.len(), 2);
1981        assert_eq!(
1982            pool.already_processed_count("proposal", "consensus_message"),
1983            0
1984        );
1985        // Requeue instead of acknowledging: the next proposal must still remember that
1986        // `first` was observed as processed, since its notification is delivered once.
1987        drop(ack);
1988
1989        epoch_store.process_notifications(std::iter::once(&consensus_key(&second)));
1990        let (transactions, ack, _) = pool.take(10, usize::MAX);
1991        assert!(transactions.is_empty());
1992        assert_eq!(
1993            pool.already_processed_count("proposal", "consensus_message"),
1994            1
1995        );
1996        assert!(matches!(
1997            receiver.await.unwrap().unwrap_err().as_inner(),
1998            SuiErrorKind::TransactionProcessing { .. }
1999        ));
2000        drop(ack);
2001    }
2002
2003    #[tokio::test]
2004    async fn insert_rejects_already_processed_transactions() {
2005        let (state, pool) = test_state_and_pool(10).await;
2006        let epoch = pool.epoch();
2007        let epoch_store = state.epoch_store_for_testing();
2008
2009        // An already-processed insert must fail before the ack is armed — a regression detonates
2010        // the drop bomb and panics this test.
2011        let via_consensus = user_transaction();
2012        epoch_store.test_insert_user_signature(digest_of(&via_consensus), vec![]);
2013        assert!(matches!(
2014            pool.try_insert(epoch, 10, vec![via_consensus])
2015                .unwrap_err()
2016                .as_inner(),
2017            SuiErrorKind::TransactionProcessing { .. }
2018        ));
2019        assert_eq!(
2020            pool.already_processed_count("insert", "consensus_message"),
2021            1
2022        );
2023
2024        let via_checkpoint = user_transaction();
2025        epoch_store
2026            .insert_finalized_transactions(&[digest_of(&via_checkpoint)], 1)
2027            .unwrap();
2028        assert!(matches!(
2029            pool.try_insert(epoch, 10, vec![via_checkpoint])
2030                .unwrap_err()
2031                .as_inner(),
2032            SuiErrorKind::TransactionProcessing { .. }
2033        ));
2034        assert_eq!(
2035            pool.already_processed_count("insert", "checkpoint_execution"),
2036            1
2037        );
2038
2039        assert_eq!(pool.queue_depth("user"), 0);
2040        let (transactions, ack, _) = pool.take(10, usize::MAX);
2041        assert!(transactions.is_empty());
2042        drop(ack);
2043    }
2044
2045    #[tokio::test]
2046    async fn departing_entries_deregister_their_processed_watches() {
2047        let (state, pool) = test_state_and_pool(1).await;
2048        let epoch = pool.epoch();
2049        let epoch_store = state.epoch_store_for_testing();
2050        // Each user key registers with both the consensus and the checkpoint registry.
2051        let baseline = epoch_store.num_pending_processed_notifications();
2052
2053        let evicted_tx = user_transaction();
2054        let (evicted, _) = pool
2055            .try_insert(epoch, 10, vec![evicted_tx.clone()])
2056            .unwrap();
2057        assert_eq!(
2058            epoch_store.num_pending_processed_notifications(),
2059            baseline + 2
2060        );
2061
2062        let taken_tx = user_transaction();
2063        let (taken, _) = pool.try_insert(epoch, 20, vec![taken_tx.clone()]).unwrap();
2064        assert!(matches!(
2065            evicted.await.unwrap().unwrap_err().as_inner(),
2066            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { .. }
2067        ));
2068        assert_eq!(
2069            epoch_store.num_pending_processed_notifications(),
2070            baseline + 2
2071        );
2072
2073        let (transactions, ack, _) = pool.take(10, usize::MAX);
2074        assert_eq!(transactions.len(), 1);
2075        ack(block(1));
2076        taken.await.unwrap().unwrap();
2077        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2078
2079        let closed_tx = user_transaction();
2080        let (closed, _) = pool.try_insert(epoch, 10, vec![closed_tx.clone()]).unwrap();
2081        assert_eq!(
2082            epoch_store.num_pending_processed_notifications(),
2083            baseline + 2
2084        );
2085        pool.close();
2086        assert!(matches!(
2087            closed.await.unwrap().unwrap_err().as_inner(),
2088            SuiErrorKind::ValidatorHaltedAtEpochEnd
2089        ));
2090        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2091
2092        for transaction in [&evicted_tx, &taken_tx, &closed_tx] {
2093            epoch_store.process_notifications(std::iter::once(&consensus_key(transaction)));
2094            epoch_store
2095                .insert_finalized_transactions(&[digest_of(transaction)], 1)
2096                .unwrap();
2097        }
2098        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2099    }
2100
2101    #[tokio::test]
2102    async fn zero_user_capacity_is_rejected_at_startup() {
2103        let state = TestAuthorityBuilder::new().build().await;
2104        let epoch_store = state.epoch_store_for_testing().clone();
2105        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2106            ConsensusTransactionPool::new_for_tests(
2107                epoch_store,
2108                0,
2109                Arc::new(AdmissionQueueMetrics::new_for_tests()),
2110            )
2111        }));
2112        assert!(result.is_err());
2113    }
2114
2115    #[tokio::test]
2116    async fn context_observes_installed_pool_and_waits_for_matching_epoch() {
2117        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
2118        let context = Arc::new(TransactionPoolContext::new_for_tests(metrics));
2119        let state = TestAuthorityBuilder::new().build().await;
2120        let pool = pool_for_current_epoch(&state, 10);
2121        let epoch = pool.epoch();
2122        let absent_waiter = tokio::spawn({
2123            let context = context.clone();
2124            async move { context.wait_for_pool(epoch).await }
2125        });
2126        while context.metrics().pool_waiting_inserts.get() == 0 {
2127            tokio::task::yield_now().await;
2128        }
2129        assert_eq!(context.metrics().pool_waiting_inserts.get(), 1);
2130        context.set_active(epoch, pool.clone());
2131        assert_eq!(absent_waiter.await.unwrap().unwrap().epoch(), epoch);
2132
2133        // send_replace retains the active state after the only receiver is gone.
2134        assert_eq!(context.wait_for_pool(epoch).await.unwrap().epoch(), epoch);
2135
2136        let race_context = Arc::new(TransactionPoolContext::new_for_tests(Arc::new(
2137            AdmissionQueueMetrics::new_for_tests(),
2138        )));
2139        let setter = race_context.clone();
2140        let race_pool = pool.clone();
2141        assert_eq!(
2142            race_context
2143                .wait_for_pool_after_subscribe(epoch, move || {
2144                    setter.set_active(epoch, race_pool);
2145                })
2146                .await
2147                .unwrap()
2148                .epoch(),
2149            epoch
2150        );
2151
2152        let next_epoch = epoch + 1;
2153        let context_waiter = context.clone();
2154        let waiter = tokio::spawn(async move { context_waiter.wait_for_pool(next_epoch).await });
2155        while context.metrics().pool_waiting_inserts.get() == 0 {
2156            tokio::task::yield_now().await;
2157        }
2158        assert_eq!(context.metrics().pool_waiting_inserts.get(), 1);
2159        state.reconfigure_for_testing().await;
2160        let next_pool = pool_for_current_epoch(&state, 10);
2161        assert_eq!(next_pool.epoch(), next_epoch);
2162        context.set_active(next_epoch, next_pool);
2163        assert_eq!(waiter.await.unwrap().unwrap().epoch(), next_epoch);
2164    }
2165
2166    #[tokio::test]
2167    async fn context_fails_stale_and_unavailable_epochs_promptly() {
2168        let context =
2169            TransactionPoolContext::new_for_tests(Arc::new(AdmissionQueueMetrics::new_for_tests()));
2170        // Advance to epoch 1 so a stale (older-epoch) caller can exist.
2171        let state = TestAuthorityBuilder::new().build().await;
2172        state.reconfigure_for_testing().await;
2173        let pool = pool_for_current_epoch(&state, 10);
2174        let active_epoch = pool.epoch();
2175        context.set_active(active_epoch, pool);
2176        assert!(matches!(
2177            context
2178                .wait_for_pool(active_epoch - 1)
2179                .await
2180                .err()
2181                .unwrap()
2182                .as_inner(),
2183            SuiErrorKind::ValidatorHaltedAtEpochEnd
2184        ));
2185        context.set_unavailable(active_epoch + 1);
2186        assert!(matches!(
2187            context
2188                .wait_for_pool(active_epoch + 1)
2189                .await
2190                .err()
2191                .unwrap()
2192                .as_inner(),
2193            SuiErrorKind::TooManyTransactionsPendingConsensus
2194        ));
2195        assert!(matches!(
2196            context
2197                .wait_for_pool(active_epoch)
2198                .await
2199                .err()
2200                .unwrap()
2201                .as_inner(),
2202            SuiErrorKind::ValidatorHaltedAtEpochEnd
2203        ));
2204    }
2205
2206    #[tokio::test]
2207    async fn transaction_pool_client_maps_ping_ack_and_status() {
2208        let state = TestAuthorityBuilder::new().build().await;
2209        let epoch_store = state.epoch_store_for_testing().clone();
2210        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
2211        let context = Arc::new(TransactionPoolContext::new_for_tests(metrics.clone()));
2212        let pool = Arc::new(ConsensusTransactionPool::new_for_tests(
2213            epoch_store.clone(),
2214            10,
2215            metrics,
2216        ));
2217        context.set_active(epoch_store.epoch(), pool.clone());
2218        let client = Arc::new(TransactionPoolClient::new(context));
2219
2220        let task = tokio::spawn({
2221            let client = client.clone();
2222            let epoch_store = epoch_store.clone();
2223            async move { client.submit(&[], &epoch_store).await }
2224        });
2225        while pool.queue_depth("ping") == 0 {
2226            tokio::task::yield_now().await;
2227        }
2228        let (_, ack, _) = pool.take(0, 0);
2229        let block_ref = block(3);
2230        ack(block_ref);
2231        let (positions, status) = task.await.unwrap().unwrap();
2232        assert_eq!(positions[0].block, block_ref);
2233        assert_eq!(positions[0].index, PING_TRANSACTION_INDEX);
2234        pool.notify_committed(vec![block_ref], 0);
2235        assert_eq!(status.await.unwrap(), BlockStatus::Sequenced(block_ref));
2236
2237        let user_transaction = user_transaction();
2238        let task = tokio::spawn({
2239            let client = client.clone();
2240            let epoch_store = epoch_store.clone();
2241            async move { client.submit(&[user_transaction], &epoch_store).await }
2242        });
2243        match task.await {
2244            // debug_fatal panics in debug/sim builds.
2245            Err(join_error) => assert!(join_error.is_panic()),
2246            // Release builds log and return the rejection instead.
2247            Ok(result) => assert!(result.is_err()),
2248        }
2249        assert_eq!(pool.queue_depth("user"), 0);
2250        assert_eq!(pool.queue_depth("system"), 0);
2251    }
2252}