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