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 crate::execution_cache::ExecutionCacheWrite;
30use crate::staggered_submission::{StaggerQuota, StaggeredSlot, proposers_metric_label};
31use async_trait::async_trait;
32use consensus_core::{BlockStatus, ClientError, LimitReached, Transaction, TransactionPool};
33use consensus_types::block::{
34    BlockRef, NUM_RESERVED_TRANSACTION_INDICES, PING_TRANSACTION_INDEX, Round, TransactionIndex,
35};
36use itertools::Itertools;
37use mysten_common::debug_fatal;
38use mysten_common::sync::notify_read::OwnedRegistration;
39use mysten_metrics::monitored_scope;
40use parking_lot::Mutex;
41use prometheus::IntGauge;
42use std::collections::{BTreeMap, VecDeque};
43use std::sync::Arc;
44use std::sync::Weak;
45use std::time::{Duration, Instant};
46use sui_macros::fail_point_if;
47use sui_types::base_types::{EpochId, ObjectRef};
48use sui_types::digests::TransactionDigest;
49use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
50use sui_types::messages_checkpoint::CheckpointSequenceNumber;
51use sui_types::messages_consensus::{
52    ConsensusPosition, ConsensusTransaction, ConsensusTransactionKey, ConsensusTransactionKind,
53};
54use sui_types::transaction::{InputObjectKind, TransactionDataAPI};
55use tokio::sync::{oneshot, watch};
56use tracing::warn;
57
58// Pings originate from external RPC clients, so we give them a hard bound.
59const MAX_PENDING_PINGS: usize = 1_024;
60
61// System traffic is correctness-critical and cannot be rejected. A depth this
62// large signals that proposal progress is stuck and needs operator attention.
63const SYSTEM_LANE_WARN_THRESHOLD: usize = 10_000;
64
65// Bounds the expired entries one proposal resolves, because resolution runs on the
66// proposer before the block is built. After a long proposal pause, the remaining
67// expired entries are resolved by later proposals.
68const MAX_EXPIRED_PER_PROPOSAL: usize = 1_000;
69
70/// Resolves with the submission's final consensus positions once the proposer
71/// includes it in a block, or with an error on eviction or epoch end.
72pub type PositionReceiver = oneshot::Receiver<SuiResult<Vec<ConsensusPosition>>>;
73
74/// Payload delivered when the proposer includes a `ConsensusAdapter`-path
75/// submission in a block: the block, the submission's transaction indices in it,
76/// and a subscription for the block's eventual status.
77type BlockInclusion = (
78    BlockRef,
79    Vec<TransactionIndex>,
80    oneshot::Receiver<BlockStatus>,
81);
82
83/// How a queued submission is resolved once its fate is known.
84enum EntryAck {
85    /// RPC user path: positions on inclusion, error on eviction / epoch end.
86    User(oneshot::Sender<SuiResult<Vec<ConsensusPosition>>>),
87    /// `ConsensusAdapter` path (system transactions and pings): block inclusion
88    /// ack plus a `BlockStatus` subscription.
89    SystemOrPing(oneshot::Sender<BlockInclusion>),
90}
91
92/// Must be explicitly resolved; dropping it unresolved is a bug.
93struct PendingAck {
94    ack: Option<EntryAck>,
95    keys: Vec<ConsensusTransactionKey>,
96    created_at: &'static std::panic::Location<'static>,
97    created: Instant,
98}
99
100impl PendingAck {
101    #[track_caller]
102    fn new(ack: EntryAck, keys: Vec<ConsensusTransactionKey>) -> Self {
103        Self {
104            ack: Some(ack),
105            keys,
106            created_at: std::panic::Location::caller(),
107            created: Instant::now(),
108        }
109    }
110
111    fn resolve_included(
112        mut self,
113        epoch: EpochId,
114        block_ref: BlockRef,
115        indices: Vec<TransactionIndex>,
116        block: &mut ProposedBlock,
117    ) {
118        match self.ack.take().expect("ack must be pending") {
119            EntryAck::User(sender) => {
120                let positions = indices
121                    .into_iter()
122                    .map(|index| ConsensusPosition {
123                        epoch,
124                        block: block_ref,
125                        index,
126                    })
127                    .collect();
128                let _ = sender.send(Ok(positions));
129            }
130            EntryAck::SystemOrPing(sender) => {
131                let (status_sender, status_receiver) = oneshot::channel();
132                block.subscribers.push(status_sender);
133                let _ = sender.send((block_ref, indices, status_receiver));
134            }
135        }
136    }
137
138    fn resolve_error(mut self, error: SuiError) {
139        match self.ack.take().expect("ack must be pending") {
140            EntryAck::User(sender) => {
141                let _ = sender.send(Err(error));
142            }
143            EntryAck::SystemOrPing(sender) => {
144                drop(sender);
145            }
146        }
147    }
148
149    fn drop_deliberately(mut self, _reason: &'static str) {
150        drop(self.ack.take().expect("ack must be pending"));
151    }
152
153    fn is_abandoned_system(&self) -> bool {
154        matches!(&self.ack, Some(EntryAck::SystemOrPing(sender)) if sender.is_closed())
155    }
156}
157
158impl Drop for PendingAck {
159    fn drop(&mut self) {
160        let flavor = match self.ack {
161            Some(EntryAck::User(_)) => "User",
162            Some(EntryAck::SystemOrPing(_)) => "SystemOrPing",
163            None => return,
164        };
165        if !std::thread::panicking() {
166            debug_fatal!(
167                "{} ack created at {} dropped without resolution after {:?}; keys={:?}",
168                flavor,
169                self.created_at,
170                self.created.elapsed(),
171                self.keys
172            );
173        }
174    }
175}
176
177/// Watches one queued transaction key for the two ways it can become processed
178/// without this validator proposing it: consensus output from a block another
179/// validator proposed, and execution through a state-synced checkpoint.
180struct ProcessedWatch {
181    consensus: OwnedRegistration<SequencedConsensusTransactionKey, ()>,
182    checkpoint: Option<OwnedRegistration<TransactionDigest, CheckpointSequenceNumber>>,
183    /// Saves results of `try_recv` on the fields above.
184    observed: Option<ProcessedMethod>,
185}
186
187impl ProcessedWatch {
188    fn register(epoch_store: &Arc<AuthorityPerEpochStore>, key: ConsensusTransactionKey) -> Self {
189        let key = SequencedConsensusTransactionKey::External(key);
190        Self {
191            checkpoint: key
192                .user_transaction_digest()
193                .map(|digest| epoch_store.register_executed_in_checkpoint_notify(&digest)),
194            consensus: epoch_store.register_consensus_message_processed_notify(&key),
195            observed: None,
196        }
197    }
198
199    /// Return the path through which the key was observed as processed, if any.
200    fn check_processed(&mut self) -> Option<ProcessedMethod> {
201        if self.observed.is_none() {
202            if self.consensus.try_recv().is_ok() {
203                self.observed = Some(ProcessedMethod::ConsensusMessageProcessed);
204            } else if let Some(checkpoint) = &mut self.checkpoint
205                && checkpoint.try_recv().is_ok()
206            {
207                self.observed = Some(ProcessedMethod::CheckpointExecuted);
208            }
209        }
210        self.observed
211    }
212}
213
214/// One queued submission. Multiple transactions form a soft bundle, which is
215/// included in a block atomically or not at all.
216struct PoolEntry {
217    transactions: Vec<Transaction>,
218    total_bytes: usize,
219    gas_price: u64,
220    tx_type: &'static str, // tx label for metrics
221    /// See `proposers_metric_label`.
222    proposers: &'static str,
223    ack: PendingAck,
224    metrics: Arc<AdmissionQueueMetrics>,
225    /// Empty for system and ping submissions: the `ConsensusAdapter` already
226    /// checks those before they reach the pool.
227    processed: Vec<ProcessedWatch>,
228    /// Owned input objects of every transaction in the entry, minus the objects the
229    /// submitter claimed immutable. Validated against live object versions each time the
230    /// entry is considered for a proposal, so an entry whose inputs were consumed while it
231    /// waited in the pool is dropped before it occupies block space.
232    owned_object_refs: Vec<ObjectRef>,
233    /// Set by `take()` when the owned-object validation fails. The entry is excluded from
234    /// the proposal and resolved with this error outside the pool mutex.
235    rejection: Option<SuiError>,
236    /// Staggered submission of transactions without allowed proposers: `Some` for held
237    /// entries, carrying the eligibility time — `take()` leaves the entry queued until
238    /// then, so a copy committed by an earlier-slotted validator during the hold
239    /// resolves it through the `processed` watches without it ever occupying block
240    /// space — and the quota registration, released when the entry is consumed into a
241    /// proposal and rolled back on drop otherwise. `None` submits on the next
242    /// proposal.
243    staggered_slot: Option<StaggeredSlot>,
244}
245
246/// `Some` once every watch has observed processing. Bundles are proposed
247/// atomically, so a partially processed one is still proposed in full.
248fn all_processed(watches: &mut [ProcessedWatch]) -> Option<ProcessedMethod> {
249    let mut result: Option<ProcessedMethod> = None;
250    for watch in watches {
251        let observed = watch.check_processed()?;
252        result = result.max(Some(observed));
253    }
254    result
255}
256
257impl AdmissionQueueEntry for PoolEntry {
258    fn gas_price(&self) -> u64 {
259        self.gas_price
260    }
261
262    fn transaction_keys(&self) -> impl Iterator<Item = ConsensusTransactionKey> {
263        self.ack.keys.iter().cloned()
264    }
265
266    fn notify_evicted(self, min_gas_price: u64) {
267        self.metrics.pool_depth.with_label_values(&["user"]).dec();
268        self.metrics
269            .pool_bytes
270            .with_label_values(&["user"])
271            .sub(self.total_bytes as i64);
272        self.ack.resolve_error(
273            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price }
274                .into(),
275        );
276    }
277
278    fn notify_rejected(self, min_gas_price: u64) {
279        self.ack.resolve_error(
280            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price }
281                .into(),
282        );
283    }
284}
285
286/// Tracks the user lane's state, which is closed when user certs are no longer accepted.
287enum UserLane {
288    Open(PriorityAdmissionQueue<PoolEntry>),
289    Closed,
290}
291
292impl UserLane {
293    #[must_use]
294    fn close(&mut self) -> Vec<PoolEntry> {
295        match std::mem::replace(self, Self::Closed) {
296            Self::Open(queue) => queue.into_entries(),
297            Self::Closed => Vec::new(),
298        }
299    }
300}
301
302/// Token bucket over the BCS-encoded, uncompressed bytes of user-lane entries handed to the
303/// proposer: the same measure as `max_transactions_in_block_bytes`, not wire bytes.
304struct UserByteBudget {
305    bytes_per_second: u64,
306    capacity: u64,
307    tokens: f64,
308    last_refill: Instant,
309}
310
311impl UserByteBudget {
312    fn new(bytes_per_second: u64, max_block_bytes: u64, now: Instant) -> Self {
313        let capacity = bytes_per_second.max(max_block_bytes);
314        Self {
315            bytes_per_second,
316            capacity,
317            tokens: capacity as f64,
318            last_refill: now,
319        }
320    }
321
322    fn available(&mut self, now: Instant) -> usize {
323        let elapsed = now
324            .saturating_duration_since(self.last_refill)
325            .as_secs_f64();
326        self.tokens =
327            (self.tokens + elapsed * self.bytes_per_second as f64).min(self.capacity as f64);
328        self.last_refill = now;
329        self.tokens as usize
330    }
331
332    fn consume(&mut self, bytes: usize) {
333        let bytes = bytes as f64;
334        if bytes > self.tokens {
335            debug_fatal!(
336                "user byte budget overdrawn: consuming {bytes} with {} tokens available",
337                self.tokens
338            );
339        }
340        self.tokens = (self.tokens - bytes).max(0.0);
341    }
342
343    fn refund(&mut self, bytes: usize) {
344        self.tokens = (self.tokens + bytes as f64).min(self.capacity as f64);
345    }
346}
347
348struct Pool {
349    user: UserLane,
350    user_byte_budget: Option<Box<UserByteBudget>>,
351    // Unbounded: system transactions are correctness-critical, produced by trusted
352    // internal components at bounded rates, and must never be evicted or outbid.
353    system: VecDeque<PoolEntry>,
354    pings: VecDeque<PendingAck>,
355    // This validator's proposed blocks, resolved by `notify_committed` once each
356    // commits or is garbage collected.
357    blocks: BTreeMap<BlockRef, ProposedBlock>,
358}
359
360#[derive(Default)]
361struct ProposedBlock {
362    /// `ConsensusAdapter`-path acks awaiting the block's status.
363    subscribers: Vec<oneshot::Sender<BlockStatus>>,
364    /// Every entry in the block, for reporting metrics.
365    entries: Vec<ProposedEntry>,
366}
367
368struct ProposedEntry {
369    lane: TakenLane,
370    tx_type: &'static str,
371    /// See `proposers_metric_label`.
372    proposers: &'static str,
373    created: Instant,
374}
375
376enum Inner {
377    Open(Pool),
378    Closed,
379}
380
381/// A single epoch's transaction pool, drained by the consensus proposer through the
382/// `consensus_core::TransactionPool` impl below. `take()` drains pings (zero block
383/// space), then the system lane FIFO, then the user lane in gas-price order —
384/// user transactions never delay system ones.
385///
386/// Backpressure for user RPCs is provided solely by the capacity limit of the
387/// `user` priority queue; staggered (held) entries are additionally bounded by the
388/// quota in [`StaggerQuota::submission_slot`].
389pub struct ConsensusTransactionPool {
390    epoch_store: Arc<AuthorityPerEpochStore>,
391    cache_writer: Arc<dyn ExecutionCacheWrite>,
392    metrics: Arc<AdmissionQueueMetrics>,
393    adapter_metrics: ConsensusAdapterMetrics,
394    stagger_quota: StaggerQuota,
395    /// When set, user-lane entries queued for at least this long are resolved with a
396    /// retriable error at proposal time.
397    max_queue_age: Option<Duration>,
398    inner: Arc<Mutex<Inner>>,
399}
400
401impl ConsensusTransactionPool {
402    pub fn new(
403        epoch_store: Arc<AuthorityPerEpochStore>,
404        cache_writer: Arc<dyn ExecutionCacheWrite>,
405        max_pending_transactions: usize,
406        max_user_bytes_per_second: Option<u64>,
407        max_queue_age: Option<Duration>,
408        metrics: Arc<AdmissionQueueMetrics>,
409        adapter_metrics: ConsensusAdapterMetrics,
410    ) -> Self {
411        assert!(
412            max_pending_transactions > 0,
413            "consensus transaction pool max_pending_transactions must be > 0"
414        );
415        assert!(
416            max_queue_age.is_none_or(|ttl| !ttl.is_zero()),
417            "consensus transaction pool max_queue_age must be > 0"
418        );
419        assert!(
420            epoch_store
421                .protocol_config()
422                .max_num_transactions_in_block()
423                .saturating_sub(1)
424                < u64::from(TransactionIndex::MAX.saturating_sub(NUM_RESERVED_TRANSACTION_INDICES)),
425            "Unsupported max_num_transactions_in_block: {}",
426            epoch_store
427                .protocol_config()
428                .max_num_transactions_in_block()
429        );
430
431        let user_byte_budget = max_user_bytes_per_second.map(|bytes_per_second| {
432            assert!(
433                bytes_per_second > 0,
434                "consensus transaction pool max_user_bytes_per_second must be > 0"
435            );
436            Box::new(UserByteBudget::new(
437                bytes_per_second,
438                epoch_store
439                    .protocol_config()
440                    .max_transactions_in_block_bytes(),
441                Instant::now(),
442            ))
443        });
444        Self {
445            epoch_store,
446            cache_writer,
447            metrics: metrics.clone(),
448            adapter_metrics,
449            max_queue_age,
450            stagger_quota: StaggerQuota::new(
451                max_pending_transactions,
452                metrics.pool_staggered_held.clone(),
453                metrics.pool_staggered_rejected.clone(),
454                metrics.pool_stagger_shadow_decisions.clone(),
455                metrics.pool_stagger_shadow_delay.clone(),
456            ),
457            inner: Arc::new(Mutex::new(Inner::Open(Pool {
458                user: UserLane::Open(PriorityAdmissionQueue::new(
459                    max_pending_transactions,
460                    metrics,
461                )),
462                user_byte_budget,
463                system: VecDeque::new(),
464                pings: VecDeque::new(),
465                blocks: BTreeMap::new(),
466            }))),
467        }
468    }
469
470    #[cfg(test)]
471    pub(crate) fn new_for_tests(
472        epoch_store: Arc<AuthorityPerEpochStore>,
473        cache_writer: Arc<dyn ExecutionCacheWrite>,
474        max_pending_transactions: usize,
475        metrics: Arc<AdmissionQueueMetrics>,
476    ) -> Self {
477        Self::new(
478            epoch_store,
479            cache_writer,
480            max_pending_transactions,
481            None,
482            None,
483            metrics,
484            ConsensusAdapterMetrics::new_test(),
485        )
486    }
487
488    pub fn epoch(&self) -> EpochId {
489        self.epoch_store.epoch()
490    }
491
492    /// Inserts user transactions to the pool. Returns the receiver for their
493    /// eventual positions, and bool indicating whether all transactions in the
494    /// group were newly inserted.
495    /// Fails fast on epoch mismatch, a closed pool/user lane, block-limit
496    /// violations, or when outbid by the eviction policy.
497    /// `gas_price` is the entry's priority in the user lane (proposed first, and
498    /// outbids lower prices when the lane is full).
499    pub fn try_insert(
500        &self,
501        caller_epoch: EpochId,
502        gas_price: u64,
503        transactions: Vec<ConsensusTransaction>,
504    ) -> SuiResult<(PositionReceiver, bool)> {
505        // Check whether the transactions were already processed by consensus or
506        // checkpoint execution.
507        let keys = transactions
508            .iter()
509            .map(ConsensusTransaction::key)
510            .collect::<Vec<_>>();
511        let mut processed = keys
512            .iter()
513            .map(|key| ProcessedWatch::register(&self.epoch_store, key.clone()))
514            .collect::<Vec<_>>();
515        if let Some(method) = self.check_already_processed(&mut processed)? {
516            self.metrics
517                .pool_already_processed
518                .with_label_values(&["insert", method.metric_label()])
519                .inc();
520            return Err(processing_error(
521                processed.iter().map(|watch| watch.consensus.key()),
522                method,
523            ));
524        }
525
526        let tx_type = tx_type_label(&transactions);
527        let (serialized, total_bytes) = self.serialize_and_validate(&transactions)?;
528        let owned_object_refs = owned_object_refs(&transactions);
529
530        let user_transactions: Vec<_> = transactions
531            .iter()
532            .filter_map(|transaction| transaction.kind.as_user_transaction())
533            .collect();
534        let staggered_slot = self
535            .stagger_quota
536            .submission_slot(&user_transactions, &self.epoch_store)?;
537        let stagger_delay = staggered_slot.as_ref().map(|slot| slot.delay());
538        let (sender, receiver) = oneshot::channel();
539        let entry = PoolEntry {
540            transactions: serialized,
541            total_bytes,
542            gas_price,
543            tx_type,
544            proposers: proposers_metric_label(&transactions, &self.epoch_store),
545            ack: PendingAck::new(EntryAck::User(sender), keys),
546            metrics: self.metrics.clone(),
547            processed,
548            owned_object_refs,
549            rejection: None,
550            staggered_slot,
551        };
552
553        let mut inner = self.inner.lock();
554        let user = match self.check_user_lane_open_locked(caller_epoch, &mut inner) {
555            Ok(user) => user,
556            Err(error) => {
557                drop(inner);
558                entry.ack.resolve_error(error.clone());
559                return Err(error);
560            }
561        };
562        let outcome = user.try_insert(entry);
563        drop(inner);
564
565        let newly_inserted = outcome.notify()?;
566        // Observed only for entries actually admitted, so the histogram records holds
567        // that are really applied. The entry (and with it the slot) is in the pool, but
568        // the slot's delay was captured at creation.
569        if let Some(delay) = stagger_delay {
570            self.metrics
571                .pool_staggered_delay
572                .observe(delay.as_secs_f64());
573        }
574        self.metrics.pool_depth.with_label_values(&["user"]).inc();
575        self.metrics
576            .pool_bytes
577            .with_label_values(&["user"])
578            .add(total_bytes as i64);
579        Ok((receiver, newly_inserted))
580    }
581
582    /// Queues a submission from the `ConsensusAdapter` path. An empty slice is a
583    /// ping: it consumes no block space and is acked with `PING_TRANSACTION_INDEX`
584    /// on the next proposal.
585    fn submit(
586        &self,
587        caller_epoch: EpochId,
588        transactions: &[ConsensusTransaction],
589    ) -> SuiResult<oneshot::Receiver<BlockInclusion>> {
590        let (sender, receiver) = oneshot::channel();
591        if transactions.is_empty() {
592            let mut inner = self.inner.lock();
593            let pool = self.check_epoch_and_open_locked(caller_epoch, &mut inner)?;
594            if pool.pings.len() >= MAX_PENDING_PINGS {
595                return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
596            }
597            pool.pings
598                .push_back(PendingAck::new(EntryAck::SystemOrPing(sender), Vec::new()));
599            self.metrics.pool_depth.with_label_values(&["ping"]).inc();
600            return Ok(receiver);
601        }
602
603        // User transactions must enter the pool through `try_insert`.
604        if transactions
605            .iter()
606            .any(ConsensusTransaction::is_user_transaction)
607        {
608            debug_fatal!(
609                "user transaction submitted through the consensus transaction pool client"
610            );
611            return Err(SuiErrorKind::GenericAuthorityError {
612                error: "user transactions cannot be submitted through the transaction pool client"
613                    .to_string(),
614            }
615            .into());
616        }
617        // Size limits cannot be relaxed for system transactions: peers enforce the
618        // same limits on every transaction in a received block, and an over-budget
619        // entry at the head of the FIFO system lane would wedge the lane.
620        let (serialized, total_bytes) = match self.serialize_and_validate(transactions) {
621            Ok(validated) => validated,
622            Err(error) => {
623                debug_fatal!("system transaction failed validation: {error}");
624                return Err(error);
625            }
626        };
627        let entry = PoolEntry {
628            transactions: serialized,
629            total_bytes,
630            gas_price: 0,
631            tx_type: tx_type_label(transactions),
632            proposers: "na",
633            ack: PendingAck::new(
634                EntryAck::SystemOrPing(sender),
635                transactions.iter().map(ConsensusTransaction::key).collect(),
636            ),
637            metrics: self.metrics.clone(),
638            processed: Vec::new(),
639            owned_object_refs: Vec::new(),
640            rejection: None,
641            staggered_slot: None,
642        };
643
644        {
645            let mut inner = self.inner.lock();
646            let pool = match self.check_epoch_and_open_locked(caller_epoch, &mut inner) {
647                Ok(pool) => pool,
648                Err(error) => {
649                    drop(inner);
650                    entry.ack.resolve_error(error.clone());
651                    return Err(error);
652                }
653            };
654            pool.system.push_back(entry);
655            let depth = pool.system.len();
656            self.metrics
657                .pool_depth
658                .with_label_values(&["system"])
659                .set(depth as i64);
660            self.metrics
661                .pool_bytes
662                .with_label_values(&["system"])
663                .add(total_bytes as i64);
664            if depth == SYSTEM_LANE_WARN_THRESHOLD {
665                warn!(
666                    depth,
667                    "Consensus transaction pool system lane exceeded warning threshold"
668                );
669            }
670        }
671        Ok(receiver)
672    }
673
674    fn check_already_processed(
675        &self,
676        watches: &mut [ProcessedWatch],
677    ) -> SuiResult<Option<ProcessedMethod>> {
678        // Check for tx already processed by consensus.
679        let consensus_processed = self.epoch_store.check_consensus_messages_processed(
680            watches.iter().map(|watch| watch.consensus.key().clone()),
681        );
682        for (watch, consensus_processed) in watches.iter_mut().zip_eq(consensus_processed) {
683            if consensus_processed {
684                watch.observed = Some(ProcessedMethod::ConsensusMessageProcessed);
685            }
686        }
687
688        // Check for tx processed by checkpoint execution.
689        let (unobserved, digests): (Vec<_>, Vec<_>) = watches
690            .iter_mut()
691            .filter_map(|watch| {
692                if watch.observed.is_some() {
693                    return None;
694                }
695                let digest = *watch.checkpoint.as_ref()?.key();
696                Some((watch, digest))
697            })
698            .unzip();
699        if !digests.is_empty() {
700            let executed = self
701                .epoch_store
702                .multi_get_transaction_checkpoint(&digests)?;
703            for (watch, executed) in unobserved.into_iter().zip_eq(executed) {
704                if executed.is_some() {
705                    watch.observed = Some(ProcessedMethod::CheckpointExecuted);
706                }
707            }
708        }
709        Ok(all_processed(watches))
710    }
711
712    fn check_epoch_and_open_locked<'a>(
713        &self,
714        caller_epoch: EpochId,
715        inner: &'a mut Inner,
716    ) -> SuiResult<&'a mut Pool> {
717        if caller_epoch != self.epoch() {
718            return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
719        }
720        match inner {
721            Inner::Open(pool) => Ok(pool),
722            Inner::Closed => Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into()),
723        }
724    }
725
726    fn check_user_lane_open_locked<'a>(
727        &self,
728        caller_epoch: EpochId,
729        inner: &'a mut Inner,
730    ) -> SuiResult<&'a mut PriorityAdmissionQueue<PoolEntry>> {
731        let pool = self.check_epoch_and_open_locked(caller_epoch, inner)?;
732        match &mut pool.user {
733            UserLane::Open(user) => Ok(user),
734            UserLane::Closed => Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into()),
735        }
736    }
737
738    /// Enforces per-transaction and per-block size limits at insert time, which
739    /// guarantees every admitted entry fits an empty proposal — `take()` can
740    /// therefore never stall on an unfittable head-of-queue entry.
741    ///
742    /// Mirrors the checks in `consensus_core::TransactionClient::submit`, which
743    /// pool submissions bypass (the proposer drains the pool directly).
744    fn serialize_and_validate(
745        &self,
746        transactions: &[ConsensusTransaction],
747    ) -> SuiResult<(Vec<Transaction>, usize)> {
748        let protocol_config = self.epoch_store.protocol_config();
749        let bundle_count = u64::try_from(transactions.len()).expect("bundle count fits into u64");
750        if bundle_count > protocol_config.max_num_transactions_in_block() {
751            return Err(consensus_client_error(
752                ClientError::OversizedTransactionBundleCount(
753                    bundle_count,
754                    protocol_config.max_num_transactions_in_block(),
755                ),
756            ));
757        }
758
759        let mut total_bytes = 0usize;
760        let mut serialized = Vec::with_capacity(transactions.len());
761        for transaction in transactions {
762            let bytes =
763                bcs::to_bytes(transaction).expect("Serializing consensus transaction cannot fail");
764            let transaction_bytes =
765                u64::try_from(bytes.len()).expect("transaction size fits into u64");
766            if transaction_bytes > protocol_config.max_transaction_size_bytes() {
767                return Err(consensus_client_error(ClientError::OversizedTransaction(
768                    transaction_bytes,
769                    protocol_config.max_transaction_size_bytes(),
770                )));
771            }
772            total_bytes += bytes.len();
773            let bundle_bytes = u64::try_from(total_bytes).expect("bundle size fits into u64");
774            if bundle_bytes > protocol_config.max_transactions_in_block_bytes() {
775                return Err(consensus_client_error(
776                    ClientError::OversizedTransactionBundleBytes(
777                        bundle_bytes,
778                        protocol_config.max_transactions_in_block_bytes(),
779                    ),
780                ));
781            }
782            serialized.push(Transaction::new(bytes));
783        }
784        Ok((serialized, total_bytes))
785    }
786
787    /// Permanently shuts the pool down, called before stopping the consensus
788    /// authority. Pending user waiters are resolved with `ValidatorHaltedAtEpochEnd`
789    /// so the RPC retry loop resubmits them in the next epoch; system and ping acks
790    /// are dropped, which their submitters treat as consensus shutdown. Idempotent.
791    pub fn close(&self) {
792        let mut pool = {
793            let mut inner = self.inner.lock();
794            match std::mem::replace(&mut *inner, Inner::Closed) {
795                Inner::Open(pool) => pool,
796                Inner::Closed => return,
797            }
798        };
799
800        // The teardown below runs on the extracted pool with the mutex released.
801        self.resolve_flushed_user_entries(pool.user.close());
802        for entry in pool.system {
803            entry.ack.drop_deliberately("pool closed");
804        }
805        for ping in pool.pings {
806            ping.drop_deliberately("pool closed");
807        }
808        drop(pool.blocks);
809        self.metrics
810            .pool_depth
811            .with_label_values(&["system"])
812            .set(0);
813        self.metrics
814            .pool_bytes
815            .with_label_values(&["system"])
816            .set(0);
817        self.metrics.pool_depth.with_label_values(&["ping"]).set(0);
818    }
819
820    /// Resolves user entries flushed by `UserLane::close` with the retriable halted
821    /// error, so they retry in the next epoch. Dropping the entries deregisters
822    /// their processed watches — call with the pool mutex released.
823    fn resolve_flushed_user_entries(&self, entries: Vec<PoolEntry>) {
824        for entry in entries {
825            self.decrement_lane_metrics("user", &entry);
826            entry
827                .ack
828                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
829        }
830    }
831
832    /// Resolves a user entry the sweep removed. An entry already committed elsewhere, or
833    /// one whose owned inputs were consumed, gets its final outcome rather than a retriable
834    /// overload error, so the submitter does not resubmit a transaction that cannot succeed.
835    fn resolve_swept_user_entry(&self, mut entry: PoolEntry) {
836        let _scope = monitored_scope("ConsensusTransactionPool::resolve_swept_user_entry");
837        self.decrement_lane_metrics("user", &entry);
838        if let Some(method) = all_processed(&mut entry.processed) {
839            self.resolve_already_processed(entry, "sweep", method);
840        } else if let Err(error) = self
841            .cache_writer
842            .validate_owned_object_versions(&entry.owned_object_refs)
843        {
844            self.resolve_rejected(entry, error);
845        } else {
846            self.metrics.expired.inc();
847            // The hint exceeds the default 1 s recent-submission dedup window
848            // (`recent_submission_dedup_window_ms`), so a retry that honours it is not
849            // rejected as already submitted. A larger configured window can still reject it.
850            entry.ack.resolve_error(
851                SuiErrorKind::ValidatorOverloadedRetryAfter {
852                    retry_after_secs: 2,
853                }
854                .into(),
855            );
856        }
857    }
858
859    fn resolve_rejected(&self, entry: PoolEntry, error: SuiError) {
860        self.metrics
861            .pool_rejected_at_proposal
862            .with_label_values(&[error.as_inner().to_variant_name()])
863            .inc();
864        entry.ack.resolve_error(error);
865    }
866
867    fn resolve_already_processed(&self, entry: PoolEntry, stage: &str, method: ProcessedMethod) {
868        self.metrics
869            .pool_already_processed
870            .with_label_values(&[stage, method.metric_label()])
871            .inc();
872        let error = processing_error(
873            entry.processed.iter().map(|watch| watch.consensus.key()),
874            method,
875        );
876        entry.ack.resolve_error(error);
877    }
878
879    fn decrement_lane_metrics(&self, lane: &str, entry: &PoolEntry) {
880        self.metrics.pool_depth.with_label_values(&[lane]).dec();
881        self.metrics
882            .pool_bytes
883            .with_label_values(&[lane])
884            .sub(entry.total_bytes as i64);
885    }
886
887    #[cfg(test)]
888    pub fn queue_depth(&self, lane: &str) -> i64 {
889        self.metrics.pool_depth.with_label_values(&[lane]).get()
890    }
891
892    #[cfg(test)]
893    pub fn already_processed_count(&self, stage: &str, method: &str) -> u64 {
894        self.metrics
895            .pool_already_processed
896            .with_label_values(&[stage, method])
897            .get()
898    }
899
900    #[cfg(test)]
901    pub fn abandoned_count(&self, lane: &str) -> u64 {
902        self.metrics.pool_abandoned.with_label_values(&[lane]).get()
903    }
904
905    #[cfg(test)]
906    pub fn rejected_at_proposal_count(&self, error: &str) -> u64 {
907        self.metrics
908            .pool_rejected_at_proposal
909            .with_label_values(&[error])
910            .get()
911    }
912}
913
914#[derive(Clone, Copy)]
915enum TakenLane {
916    System,
917    User,
918}
919
920impl TakenLane {
921    fn label(self) -> &'static str {
922        match self {
923            TakenLane::System => "system",
924            TakenLane::User => "user",
925        }
926    }
927}
928
929struct TakenEntry {
930    lane: TakenLane,
931    entry: PoolEntry,
932}
933
934impl TakenEntry {
935    /// The entry has been consumed into a proposal: its staggered quota registration,
936    /// if any, is released here. Requeue after a dropped acknowledgement rearms it.
937    fn new(lane: TakenLane, mut entry: PoolEntry) -> Self {
938        if let Some(slot) = &mut entry.staggered_slot {
939            slot.release();
940        }
941        Self { lane, entry }
942    }
943}
944
945/// Holds the entries handed out by one `take()` until their fate is known:
946/// `acknowledge` (the proposer created a block) resolves every waiter with its
947/// position/inclusion ack, while dropping the guard uninvoked (failed proposal or
948/// shutdown) returns the entries to the front of their lanes so nothing is lost or
949/// reordered. All paths serialize on the pool mutex, so they compose correctly
950/// with `close()` running in between.
951struct TakenTransactionsGuard {
952    epoch: EpochId,
953    inner: Arc<Mutex<Inner>>,
954    metrics: Arc<AdmissionQueueMetrics>,
955    entries: Option<Vec<TakenEntry>>,
956    pings: Option<Vec<PendingAck>>,
957}
958
959impl TakenTransactionsGuard {
960    fn acknowledge(mut self, block_ref: BlockRef) {
961        let entries = self.entries.take().expect("acknowledgement called once");
962        let pings = self.pings.take().expect("acknowledgement called once");
963        let mut inner = self.inner.lock();
964        let Inner::Open(pool) = &mut *inner else {
965            drop(inner);
966            Self::resolve_after_close(entries, pings, "pool closed before acknowledgement");
967            return;
968        };
969
970        // Transactions occupy the block in exactly the order take() returned them,
971        // so indices are assigned contiguously across entries in that order.
972        let mut next_index = 0usize;
973        let mut watches_to_drop = Vec::with_capacity(entries.len());
974        let mut block = ProposedBlock::default();
975        for taken in entries {
976            let lane = taken.lane;
977            let mut entry = taken.entry;
978            block.entries.push(ProposedEntry {
979                lane,
980                tx_type: entry.tx_type,
981                proposers: entry.proposers,
982                created: entry.ack.created,
983            });
984            watches_to_drop.push(std::mem::take(&mut entry.processed));
985            let start = next_index;
986            next_index += entry.transactions.len();
987            let indices = (start..next_index)
988                .map(|index| {
989                    TransactionIndex::try_from(index)
990                        .expect("validated transaction index must fit TransactionIndex")
991                })
992                .collect::<Vec<_>>();
993            self.metrics
994                .queue_wait_latency
995                .with_label_values(&[lane.label(), entry.proposers])
996                .observe(entry.ack.created.elapsed().as_secs_f64());
997            entry
998                .ack
999                .resolve_included(self.epoch, block_ref, indices, &mut block);
1000        }
1001        for ping in pings {
1002            self.metrics
1003                .queue_wait_latency
1004                .with_label_values(&["ping", "na"])
1005                .observe(ping.created.elapsed().as_secs_f64());
1006            ping.resolve_included(
1007                self.epoch,
1008                block_ref,
1009                vec![PING_TRANSACTION_INDEX],
1010                &mut block,
1011            );
1012        }
1013        if !block.subscribers.is_empty() || !block.entries.is_empty() {
1014            pool.blocks.insert(block_ref, block);
1015        }
1016        drop(inner);
1017        drop(watches_to_drop); // drop after mutex release
1018    }
1019
1020    // The pool closed while these entries were out with the proposer: resolve user
1021    // waiters with the retriable halted error and drop system/ping acks, exactly as
1022    // close() did for the entries still queued.
1023    fn resolve_after_close(entries: Vec<TakenEntry>, pings: Vec<PendingAck>, reason: &'static str) {
1024        for taken in entries {
1025            taken
1026                .entry
1027                .ack
1028                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1029        }
1030        for ping in pings {
1031            ping.drop_deliberately(reason);
1032        }
1033    }
1034}
1035
1036impl Drop for TakenTransactionsGuard {
1037    fn drop(&mut self) {
1038        let Some(entries) = self.entries.take() else {
1039            return;
1040        };
1041        let pings = self.pings.take().unwrap_or_default();
1042        let mut inner = self.inner.lock();
1043        let Inner::Open(pool) = &mut *inner else {
1044            drop(inner);
1045            Self::resolve_after_close(entries, pings, "pool closed before dropped acknowledgement");
1046            return;
1047        };
1048
1049        let mut requeued = 0;
1050        let mut halted = Vec::new();
1051        // Capacity is bypassed because these entries were already admitted. Concurrent
1052        // inserts can put us over capacity, which we accept transiently.
1053        for mut taken in entries.into_iter().rev() {
1054            match taken.lane {
1055                TakenLane::System => {
1056                    requeued += 1;
1057                    self.metrics.pool_depth.with_label_values(&["system"]).inc();
1058                    self.metrics
1059                        .pool_bytes
1060                        .with_label_values(&["system"])
1061                        .add(taken.entry.total_bytes as i64);
1062                    pool.system.push_front(taken.entry);
1063                    let depth = pool.system.len();
1064                    if depth == SYSTEM_LANE_WARN_THRESHOLD {
1065                        warn!(
1066                            depth,
1067                            "Consensus transaction pool system lane exceeded warning threshold"
1068                        );
1069                    }
1070                }
1071                TakenLane::User => match &mut pool.user {
1072                    UserLane::Open(user) => {
1073                        requeued += 1;
1074                        self.metrics.pool_depth.with_label_values(&["user"]).inc();
1075                        self.metrics
1076                            .pool_bytes
1077                            .with_label_values(&["user"])
1078                            .add(taken.entry.total_bytes as i64);
1079                        if let Some(slot) = &mut taken.entry.staggered_slot {
1080                            slot.rearm();
1081                        }
1082                        if let Some(budget) = &mut pool.user_byte_budget {
1083                            budget.refund(taken.entry.total_bytes);
1084                        }
1085                        user.reinsert_front(taken.entry);
1086                    }
1087                    UserLane::Closed => halted.push(taken.entry),
1088                },
1089            }
1090        }
1091        for ping in pings.into_iter().rev() {
1092            pool.pings.push_front(ping);
1093            self.metrics.pool_depth.with_label_values(&["ping"]).inc();
1094        }
1095        self.metrics
1096            .pool_requeued_on_dropped_ack
1097            .inc_by(requeued as u64);
1098        drop(inner);
1099
1100        for entry in halted {
1101            entry
1102                .ack
1103                .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1104        }
1105    }
1106}
1107
1108impl TransactionPool for ConsensusTransactionPool {
1109    fn take(
1110        &self,
1111        max_count: usize,
1112        max_bytes: usize,
1113    ) -> (
1114        Vec<Transaction>,
1115        Box<dyn FnOnce(BlockRef) + Send>,
1116        LimitReached,
1117    ) {
1118        let _scope = monitored_scope("ConsensusTransactionPool::take");
1119        // This acquires (and immediately releases) the epoch store's reconfiguration
1120        // read lock. It must happen before the pool mutex is taken — never read the
1121        // reconfig state while holding the pool mutex.
1122        let should_accept_user_certs = self
1123            .epoch_store
1124            .get_reconfig_state_read_lock_guard()
1125            .should_accept_user_certs();
1126        #[allow(unused_mut)]
1127        let mut disabled = false;
1128        fail_point_if!("consensus_transaction_pool_disable_take", || {
1129            disabled = true;
1130        });
1131        #[allow(unused_mut)]
1132        let mut user_take_disabled = false;
1133        fail_point_if!("consensus_transaction_pool_disable_user_take", || {
1134            user_take_disabled = true;
1135        });
1136
1137        let mut inner = self.inner.lock();
1138        let pool = match &mut *inner {
1139            Inner::Open(pool) => pool,
1140            Inner::Closed => {
1141                return (
1142                    Vec::new(),
1143                    Box::new(|_| {}),
1144                    LimitReached::AllTransactionsIncluded,
1145                );
1146            }
1147        };
1148        let flushed = if !should_accept_user_certs {
1149            pool.user.close()
1150        } else {
1151            Vec::new()
1152        };
1153        if disabled {
1154            drop(inner);
1155            self.resolve_flushed_user_entries(flushed); // resolve after mutex release
1156            return (
1157                Vec::new(),
1158                Box::new(|_| {}),
1159                LimitReached::AllTransactionsIncluded,
1160            );
1161        }
1162
1163        let (abandoned_pings, pings): (Vec<_>, Vec<_>) = pool
1164            .pings
1165            .drain(..)
1166            .partition(PendingAck::is_abandoned_system);
1167        self.metrics
1168            .pool_depth
1169            .with_label_values(&["ping"])
1170            .sub((pings.len() + abandoned_pings.len()) as i64);
1171
1172        let mut transactions = Vec::new();
1173        let mut entries = Vec::new();
1174        let mut system_tx_total_bytes = 0usize;
1175        let mut limit_reached = LimitReached::AllTransactionsIncluded;
1176
1177        let mut abandoned = Vec::new();
1178        while let Some(entry) = pool.system.front() {
1179            // Skip entries whose submitter stopped waiting (e.g. a checkpoint
1180            // signature whose checkpoint was already synced).
1181            if entry.ack.is_abandoned_system() {
1182                let entry = pool.system.pop_front().expect("front entry must exist");
1183                self.decrement_lane_metrics("system", &entry);
1184                abandoned.push(entry);
1185                continue;
1186            }
1187            if let Some(limit) = entry_limit(
1188                entry,
1189                transactions.len(),
1190                system_tx_total_bytes,
1191                max_count,
1192                max_bytes,
1193            ) {
1194                limit_reached = limit;
1195                break;
1196            }
1197            let entry = pool.system.pop_front().expect("front entry must exist");
1198            system_tx_total_bytes += entry.total_bytes;
1199            transactions.extend(entry.transactions.iter().cloned());
1200            self.decrement_lane_metrics("system", &entry);
1201            entries.push(TakenEntry::new(TakenLane::System, entry));
1202        }
1203
1204        let now = Instant::now();
1205        let mut excluded = Vec::new();
1206        // The sweep runs on every proposal, independent of whether user transactions are
1207        // drained this round. A saturated system lane sets limit_reached to a block limit and
1208        // skips the drain below. Expired entries must still be removed during that overload,
1209        // so the sweep does not depend on limit_reached.
1210        let swept_user = match (self.max_queue_age, &mut pool.user) {
1211            (Some(ttl), UserLane::Open(user)) => {
1212                let mut swept = 0;
1213                user.pop_front_while(|entry| {
1214                    let expired = swept < MAX_EXPIRED_PER_PROPOSAL
1215                        && now.saturating_duration_since(entry.ack.created) >= ttl;
1216                    swept += usize::from(expired);
1217                    expired
1218                })
1219            }
1220            _ => Vec::new(),
1221        };
1222        if !user_take_disabled
1223            && matches!(limit_reached, LimitReached::AllTransactionsIncluded)
1224            && let UserLane::Open(user) = &mut pool.user
1225        {
1226            let mut pending_count = transactions.len();
1227            let mut pending_bytes = system_tx_total_bytes;
1228            let user_max_bytes = match &mut pool.user_byte_budget {
1229                Some(budget) => {
1230                    max_bytes.min(system_tx_total_bytes.saturating_add(budget.available(now)))
1231                }
1232                None => max_bytes,
1233            };
1234            let popped;
1235            (popped, excluded) = user.pop_batch_while(|entry| {
1236                // An already-processed entry is excluded without consuming block budget.
1237                // Checked before the staggered hold, so a copy committed elsewhere
1238                // resolves a held entry immediately instead of after its delay.
1239                if all_processed(&mut entry.processed).is_some() {
1240                    return PopAction::Exclude;
1241                }
1242                // A staggered entry stays queued (occupying pool capacity, which is the
1243                // overload backstop) until its delay elapses — even if staggering was
1244                // disarmed after the hold was stamped, bounding the residual by
1245                // max_delay. Held entries deliberately leave `limit_reached` untouched:
1246                // no block limit was hit.
1247                if entry
1248                    .staggered_slot
1249                    .as_ref()
1250                    .is_some_and(|slot| slot.eligible_at() > now)
1251                {
1252                    return PopAction::Skip;
1253                }
1254                // Checked before the block limits so a rejected entry consumes neither block
1255                // space nor byte budget, and never stops the iteration.
1256                if let Err(error) = self
1257                    .cache_writer
1258                    .validate_owned_object_versions(&entry.owned_object_refs)
1259                {
1260                    entry.rejection = Some(error);
1261                    return PopAction::Exclude;
1262                }
1263                match entry_limit(
1264                    entry,
1265                    pending_count,
1266                    pending_bytes,
1267                    max_count,
1268                    user_max_bytes,
1269                ) {
1270                    Some(limit) => {
1271                        limit_reached = limit;
1272                        PopAction::Stop
1273                    }
1274                    None => {
1275                        pending_count += entry.transactions.len();
1276                        pending_bytes += entry.total_bytes;
1277                        PopAction::Include
1278                    }
1279                }
1280            });
1281            if let Some(budget) = &mut pool.user_byte_budget {
1282                budget.consume(pending_bytes.saturating_sub(system_tx_total_bytes));
1283                self.metrics
1284                    .pool_byte_budget_bytes
1285                    .set(budget.tokens as i64);
1286                if user_max_bytes < max_bytes && limit_reached == LimitReached::MaxBytes {
1287                    self.metrics.pool_byte_budget_exhausted.inc();
1288                }
1289            }
1290            for entry in popped {
1291                self.decrement_lane_metrics("user", &entry);
1292                transactions.extend(entry.transactions.iter().cloned());
1293                entries.push(TakenEntry::new(TakenLane::User, entry));
1294            }
1295        }
1296        drop(inner);
1297
1298        // Resolve flushed/processed items outside the mutex.
1299        self.resolve_flushed_user_entries(flushed);
1300        for entry in swept_user {
1301            self.resolve_swept_user_entry(entry);
1302        }
1303        for entry in abandoned {
1304            self.metrics
1305                .pool_abandoned
1306                .with_label_values(&["system"])
1307                .inc();
1308            entry
1309                .ack
1310                .drop_deliberately("submitter stopped waiting before proposal");
1311        }
1312        for ping in abandoned_pings {
1313            self.metrics
1314                .pool_abandoned
1315                .with_label_values(&["ping"])
1316                .inc();
1317            ping.drop_deliberately("submitter stopped waiting before proposal");
1318        }
1319        for mut entry in excluded {
1320            self.decrement_lane_metrics("user", &entry);
1321            if let Some(error) = entry.rejection.take() {
1322                self.resolve_rejected(entry, error);
1323                continue;
1324            }
1325            let method = all_processed(&mut entry.processed)
1326                .expect("excluded entries are already processed");
1327            self.resolve_already_processed(entry, "proposal", method);
1328        }
1329
1330        self.metrics
1331            .pool_taken_per_proposal
1332            .observe(transactions.len() as f64);
1333        let guard = TakenTransactionsGuard {
1334            epoch: self.epoch(),
1335            inner: self.inner.clone(),
1336            metrics: self.metrics.clone(),
1337            entries: Some(entries),
1338            pings: Some(pings),
1339        };
1340        (
1341            transactions,
1342            Box::new(move |block_ref| guard.acknowledge(block_ref)),
1343            limit_reached,
1344        )
1345    }
1346
1347    // Same semantics as consensus-core's TransactionConsumer::notify_own_blocks_status:
1348    // committed blocks resolve Sequenced first, then every remaining subscription at
1349    // round <= gc_round resolves GarbageCollected (those blocks can never commit).
1350    fn notify_committed(&self, own_committed_blocks: Vec<BlockRef>, gc_round: Round) {
1351        let mut sequenced = Vec::new();
1352        let mut garbage_collected = Vec::new();
1353        {
1354            let mut inner = self.inner.lock();
1355            let Inner::Open(pool) = &mut *inner else {
1356                return;
1357            };
1358            for block_ref in own_committed_blocks {
1359                if let Some(block) = pool.blocks.remove(&block_ref) {
1360                    for subscriber in block.subscribers {
1361                        let _ = subscriber.send(BlockStatus::Sequenced(block_ref));
1362                    }
1363                    sequenced.extend(block.entries);
1364                }
1365            }
1366            while let Some((block_ref, block)) = pool.blocks.pop_first() {
1367                if block_ref.round > gc_round {
1368                    pool.blocks.insert(block_ref, block);
1369                    break;
1370                }
1371                self.metrics
1372                    .pool_gc_notified
1373                    .inc_by(block.subscribers.len() as u64);
1374                for subscriber in block.subscribers {
1375                    let _ = subscriber.send(BlockStatus::GarbageCollected(block_ref));
1376                }
1377                garbage_collected.extend(block.entries);
1378            }
1379        }
1380
1381        // Metrics are updated with the mutex released.
1382        self.report_proposed_status(&sequenced, "sequenced");
1383        self.report_proposed_status(&garbage_collected, "garbage_collected");
1384        self.report_commit_latency(&sequenced);
1385    }
1386}
1387
1388impl ConsensusTransactionPool {
1389    /// Reports user entries to `sequencing_certificate_status*` metrics.
1390    /// System entries are reported by the adapter itself, which waits on their block
1391    /// status.
1392    fn report_proposed_status(&self, entries: &[ProposedEntry], status: &'static str) {
1393        let mut counts: Vec<(&'static str, u64)> = Vec::new();
1394        for entry in entries
1395            .iter()
1396            .filter(|entry| matches!(entry.lane, TakenLane::User))
1397        {
1398            match counts
1399                .iter_mut()
1400                .find(|(tx_type, _)| *tx_type == entry.tx_type)
1401            {
1402                Some((_, count)) => *count += 1,
1403                None => counts.push((entry.tx_type, 1)),
1404            }
1405        }
1406        for (tx_type, count) in counts {
1407            self.adapter_metrics
1408                .sequencing_certificate_status
1409                .with_label_values(&[tx_type, status])
1410                .inc_by(count);
1411        }
1412    }
1413
1414    fn report_commit_latency(&self, entries: &[ProposedEntry]) {
1415        let now = Instant::now();
1416        for entry in entries {
1417            let lane = match entry.lane {
1418                TakenLane::User => "user",
1419                TakenLane::System => "system",
1420            };
1421            let histogram = self
1422                .metrics
1423                .pool_commit_latency
1424                .with_label_values(&[lane, entry.proposers]);
1425            histogram.observe(now.saturating_duration_since(entry.created).as_secs_f64());
1426        }
1427    }
1428}
1429
1430/// Which block limit the entry would exceed, if any. Entries are all-or-nothing:
1431/// a bundle that does not fit stays queued in full for the next proposal.
1432fn entry_limit(
1433    entry: &PoolEntry,
1434    current_count: usize,
1435    current_bytes: usize,
1436    max_count: usize,
1437    max_bytes: usize,
1438) -> Option<LimitReached> {
1439    if current_bytes.saturating_add(entry.total_bytes) > max_bytes {
1440        return Some(LimitReached::MaxBytes);
1441    }
1442    if current_count.saturating_add(entry.transactions.len()) > max_count {
1443        return Some(LimitReached::MaxNumOfTransactions);
1444    }
1445    None
1446}
1447
1448fn consensus_client_error(error: ClientError) -> SuiError {
1449    SuiErrorKind::FailedToSubmitToConsensus(error.to_string()).into()
1450}
1451
1452/// Owned input object refs of the user transactions in `transactions`, excluding the
1453/// objects their submitter claimed immutable. Mirrors the set `handle_vote_transaction`
1454/// validates at admission.
1455fn owned_object_refs(transactions: &[ConsensusTransaction]) -> Vec<ObjectRef> {
1456    let mut refs = Vec::new();
1457    for transaction in transactions {
1458        let ConsensusTransactionKind::UserTransactionV2(tx) = &transaction.kind else {
1459            continue;
1460        };
1461        let immutable = tx.get_immutable_objects();
1462        let Ok(input_objects) = tx.tx().data().transaction_data().input_objects() else {
1463            continue;
1464        };
1465        refs.extend(input_objects.into_iter().filter_map(|input| match input {
1466            InputObjectKind::ImmOrOwnedMoveObject(obj_ref) if !immutable.contains(&obj_ref.0) => {
1467                Some(obj_ref)
1468            }
1469            _ => None,
1470        }));
1471    }
1472    refs
1473}
1474
1475#[derive(Clone)]
1476enum PoolState {
1477    /// Before the first consensus start.
1478    Absent,
1479    Active(EpochId, Weak<ConsensusTransactionPool>),
1480    /// No pool will be installed for this epoch (the node is not a validator, or is
1481    /// shutting down); waiters must fail rather than wait for it.
1482    Unavailable(EpochId),
1483}
1484
1485/// Process-lifetime handle connecting submitters to the current epoch's pool.
1486/// The watch channel is the single authoritative state, always updated with
1487/// `send_replace` so an installation is never lost while no receiver is subscribed.
1488///
1489/// This closes the reconfiguration window in which the new epoch store is already
1490/// published but the new epoch's consensus (and pool) has not started yet: callers
1491/// wait on the watch instead of failing or, worse, inserting into the previous
1492/// epoch's still-installed pool.
1493pub struct TransactionPoolContext {
1494    state: watch::Sender<PoolState>,
1495    metrics: Arc<AdmissionQueueMetrics>,
1496    adapter_metrics: ConsensusAdapterMetrics,
1497    cache_writer: Arc<dyn ExecutionCacheWrite>,
1498}
1499
1500impl TransactionPoolContext {
1501    pub fn new(
1502        metrics: Arc<AdmissionQueueMetrics>,
1503        adapter_metrics: ConsensusAdapterMetrics,
1504        cache_writer: Arc<dyn ExecutionCacheWrite>,
1505    ) -> Self {
1506        let (state, _) = watch::channel(PoolState::Absent);
1507        Self {
1508            state,
1509            metrics,
1510            adapter_metrics,
1511            cache_writer,
1512        }
1513    }
1514
1515    pub fn set_active(&self, epoch: EpochId, pool: Arc<ConsensusTransactionPool>) {
1516        assert_eq!(epoch, pool.epoch());
1517        drop(
1518            self.state
1519                .send_replace(PoolState::Active(epoch, Arc::downgrade(&pool))),
1520        );
1521    }
1522
1523    /// Marks `epoch` as one that will never get a pool, promptly failing current and
1524    /// future waiters that would otherwise hang until their transport deadline.
1525    pub fn set_unavailable(&self, epoch: EpochId) {
1526        drop(self.state.send_replace(PoolState::Unavailable(epoch)));
1527    }
1528
1529    pub async fn try_insert(
1530        &self,
1531        epoch: EpochId,
1532        gas_price: u64,
1533        transactions: Vec<ConsensusTransaction>,
1534    ) -> SuiResult<(PositionReceiver, bool)> {
1535        self.wait_for_pool(epoch)
1536            .await?
1537            .try_insert(epoch, gas_price, transactions)
1538    }
1539
1540    /// Resolves the pool for `caller_epoch`. Waits while the state is absent or older
1541    /// than the caller — e.g. during the gap between epoch-store publication and consensus
1542    /// start. Fails immediately with `ValidatorHaltedAtEpochEnd` when the state is *newer*
1543    /// (the caller raced with a stale epoch store and should re-validate), and with a
1544    /// retriable overload error when this epoch is `Unavailable` (client retries
1545    /// against another validator). Bounded only by the caller's own deadline or
1546    /// cancellation.
1547    pub async fn wait_for_pool(
1548        &self,
1549        caller_epoch: EpochId,
1550    ) -> SuiResult<Arc<ConsensusTransactionPool>> {
1551        self.wait_for_pool_after_subscribe(caller_epoch, || {})
1552            .await
1553    }
1554
1555    async fn wait_for_pool_after_subscribe(
1556        &self,
1557        caller_epoch: EpochId,
1558        after_subscribe: impl FnOnce(),
1559    ) -> SuiResult<Arc<ConsensusTransactionPool>> {
1560        let mut receiver = self.state.subscribe();
1561        after_subscribe();
1562        let start = Instant::now();
1563        loop {
1564            let state = receiver.borrow_and_update().clone();
1565            match state {
1566                PoolState::Active(epoch, pool) if epoch == caller_epoch => {
1567                    // A dead pool with no state change means consensus for this epoch
1568                    // was torn down abruptly; fail as for `Unavailable`.
1569                    return pool
1570                        .upgrade()
1571                        .ok_or_else(|| SuiErrorKind::TooManyTransactionsPendingConsensus.into());
1572                }
1573                PoolState::Active(epoch, _) if epoch > caller_epoch => {
1574                    return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1575                }
1576                PoolState::Unavailable(epoch) if epoch > caller_epoch => {
1577                    return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1578                }
1579                PoolState::Unavailable(epoch) if epoch == caller_epoch => {
1580                    return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
1581                }
1582                PoolState::Absent | PoolState::Active(_, _) | PoolState::Unavailable(_) => {}
1583            }
1584
1585            let _waiting = WaitingInsertGuard::new(self.metrics.pool_waiting_inserts.clone());
1586            match tokio::time::timeout(Duration::from_secs(10), receiver.changed()).await {
1587                Ok(Ok(())) => {}
1588                Ok(Err(_)) => {
1589                    // Unreachable: `changed()` errors only when the watch sender is dropped,
1590                    // but the sender is a field of this context and `&self` is borrowed
1591                    // across this await. All real teardown paths publish a state change
1592                    // (`set_active` / `set_unavailable`) instead of dropping the sender.
1593                    debug_fatal!("transaction pool watch sender dropped while context is alive");
1594                    return Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into());
1595                }
1596                Err(_) => {
1597                    warn!(
1598                        caller_epoch,
1599                        elapsed = ?start.elapsed(),
1600                        "Waiting for consensus transaction pool to initialize"
1601                    );
1602                }
1603            }
1604        }
1605    }
1606
1607    pub fn metrics(&self) -> &Arc<AdmissionQueueMetrics> {
1608        &self.metrics
1609    }
1610
1611    pub fn adapter_metrics(&self) -> &ConsensusAdapterMetrics {
1612        &self.adapter_metrics
1613    }
1614
1615    pub fn cache_writer(&self) -> &Arc<dyn ExecutionCacheWrite> {
1616        &self.cache_writer
1617    }
1618
1619    #[cfg(test)]
1620    pub(crate) fn new_for_tests(
1621        metrics: Arc<AdmissionQueueMetrics>,
1622        cache_writer: Arc<dyn ExecutionCacheWrite>,
1623    ) -> Self {
1624        Self::new(metrics, ConsensusAdapterMetrics::new_test(), cache_writer)
1625    }
1626}
1627
1628struct WaitingInsertGuard {
1629    gauge: IntGauge,
1630}
1631
1632impl WaitingInsertGuard {
1633    fn new(gauge: IntGauge) -> Self {
1634        gauge.inc();
1635        Self { gauge }
1636    }
1637}
1638
1639impl Drop for WaitingInsertGuard {
1640    fn drop(&mut self) {
1641        self.gauge.dec();
1642    }
1643}
1644
1645/// The `ConsensusClient` installed in pull mode in place of `LazyMysticetiClient`,
1646/// so the unchanged `ConsensusAdapter` (system transactions, pings, GC retries)
1647/// submits into the pool. Wraps the context rather than one pool, resolving the
1648/// right epoch's pool per submission.
1649pub struct TransactionPoolClient {
1650    context: Arc<TransactionPoolContext>,
1651}
1652
1653impl TransactionPoolClient {
1654    pub fn new(context: Arc<TransactionPoolContext>) -> Self {
1655        Self { context }
1656    }
1657}
1658
1659#[async_trait]
1660impl ConsensusClient for TransactionPoolClient {
1661    async fn submit(
1662        &self,
1663        transactions: &[ConsensusTransaction],
1664        epoch_store: &Arc<AuthorityPerEpochStore>,
1665    ) -> SuiResult<(Vec<ConsensusPosition>, BlockStatusReceiver)> {
1666        let epoch = epoch_store.epoch();
1667        let pool = self.context.wait_for_pool(epoch).await?;
1668        let receiver = pool.submit(epoch, transactions)?;
1669        let (block_ref, indices, status_receiver) = receiver.await.map_err(|error| {
1670            consensus_client_error(ClientError::ConsensusShuttingDown(error.to_string()))
1671        })?;
1672        let positions = indices
1673            .into_iter()
1674            .map(|index| ConsensusPosition {
1675                epoch,
1676                block: block_ref,
1677                index,
1678            })
1679            .collect();
1680        Ok((positions, status_receiver))
1681    }
1682}
1683
1684#[cfg(test)]
1685mod tests {
1686    use super::*;
1687    use crate::authority::AuthorityState;
1688    use crate::authority::test_authority_builder::TestAuthorityBuilder;
1689    use consensus_config::AuthorityIndex;
1690    use consensus_types::block::BlockDigest;
1691    use sui_macros::sim_test;
1692    use sui_protocol_config::ProtocolConfig;
1693    use sui_types::base_types::{AuthorityName, ObjectID, random_object_ref};
1694    use sui_types::crypto::{AccountKeyPair, get_key_pair};
1695    use sui_types::error::UserInputError;
1696    use sui_types::object::{Object, Owner};
1697    use sui_types::transaction::PlainTransactionWithClaims;
1698
1699    async fn test_state_and_pool(
1700        capacity: usize,
1701    ) -> (Arc<AuthorityState>, Arc<ConsensusTransactionPool>) {
1702        let state = TestAuthorityBuilder::new().build().await;
1703        let pool = pool_for_current_epoch(&state, capacity);
1704        (state, pool)
1705    }
1706
1707    async fn test_state_and_pool_with_protocol_config(
1708        capacity: usize,
1709        protocol_config: ProtocolConfig,
1710    ) -> (Arc<AuthorityState>, Arc<ConsensusTransactionPool>) {
1711        let state = TestAuthorityBuilder::new()
1712            .with_protocol_config(protocol_config)
1713            .build()
1714            .await;
1715        let pool = pool_for_current_epoch(&state, capacity);
1716        (state, pool)
1717    }
1718
1719    fn pool_for_current_epoch(
1720        state: &Arc<AuthorityState>,
1721        capacity: usize,
1722    ) -> Arc<ConsensusTransactionPool> {
1723        Arc::new(ConsensusTransactionPool::new_for_tests(
1724            state.epoch_store_for_testing().clone(),
1725            state.get_cache_writer().clone(),
1726            capacity,
1727            Arc::new(AdmissionQueueMetrics::new_for_tests()),
1728        ))
1729    }
1730
1731    // The budget is installed directly because max_transactions_in_block_bytes() is fixed
1732    // under msim and ignores protocol config overrides.
1733    /// `byte_budget` is `(max_block_bytes, max_user_bytes_per_second)`.
1734    async fn test_state_and_configured_pool(
1735        byte_budget: Option<(u64, u64)>,
1736        max_queue_age: Option<Duration>,
1737    ) -> (Arc<AuthorityState>, Arc<ConsensusTransactionPool>) {
1738        let state = TestAuthorityBuilder::new().build().await;
1739        let pool = Arc::new(ConsensusTransactionPool::new(
1740            state.epoch_store_for_testing().clone(),
1741            state.get_cache_writer().clone(),
1742            10,
1743            byte_budget.map(|(_, per_second)| per_second),
1744            max_queue_age,
1745            Arc::new(AdmissionQueueMetrics::new_for_tests()),
1746            ConsensusAdapterMetrics::new_test(),
1747        ));
1748        if let Some((max_block_bytes, max_user_bytes_per_second)) = byte_budget {
1749            let mut inner = pool.inner.lock();
1750            let Inner::Open(open) = &mut *inner else {
1751                unreachable!("new pool must be open");
1752            };
1753            open.user_byte_budget = Some(Box::new(UserByteBudget::new(
1754                max_user_bytes_per_second,
1755                max_block_bytes,
1756                Instant::now(),
1757            )));
1758        }
1759        (state, pool)
1760    }
1761
1762    fn user_byte_budget_tokens(pool: &ConsensusTransactionPool) -> f64 {
1763        let inner = pool.inner.lock();
1764        let Inner::Open(open) = &*inner else {
1765            unreachable!("pool must be open");
1766        };
1767        open.user_byte_budget.as_ref().unwrap().tokens
1768    }
1769
1770    fn transaction() -> ConsensusTransaction {
1771        ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO)
1772    }
1773
1774    /// A user transaction whose gas object exists in `state` at the referenced version, so
1775    /// it passes the owned-object validation in `take()`.
1776    fn user_transaction(state: &AuthorityState) -> ConsensusTransaction {
1777        let (sender, _) = get_key_pair::<AccountKeyPair>();
1778        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
1779        let gas_object_ref = gas_object.compute_object_reference();
1780        state.insert_genesis_object(gas_object);
1781        user_transaction_with_gas(gas_object_ref)
1782    }
1783
1784    /// A user transaction paying with `gas_object_ref`, which need not exist.
1785    fn user_transaction_with_gas(gas_object_ref: ObjectRef) -> ConsensusTransaction {
1786        let (sender, keypair) = get_key_pair::<AccountKeyPair>();
1787        let transaction = crate::test_utils::make_transfer_sui_transaction(
1788            gas_object_ref,
1789            sender,
1790            Some(1),
1791            sender,
1792            &keypair,
1793            1,
1794        );
1795        ConsensusTransaction::new_user_transaction_v2_message(
1796            &AuthorityName::ZERO,
1797            PlainTransactionWithClaims::no_aliases(transaction),
1798        )
1799    }
1800
1801    fn block(round: Round) -> BlockRef {
1802        BlockRef::new(round, AuthorityIndex::new_for_test(0), BlockDigest::MIN)
1803    }
1804
1805    fn consensus_key(transaction: &ConsensusTransaction) -> SequencedConsensusTransactionKey {
1806        SequencedConsensusTransactionKey::External(transaction.key())
1807    }
1808
1809    fn digest_of(transaction: &ConsensusTransaction) -> TransactionDigest {
1810        consensus_key(transaction)
1811            .user_transaction_digest()
1812            .expect("expected a user transaction key")
1813    }
1814
1815    #[cfg(debug_assertions)]
1816    #[tokio::test]
1817    #[should_panic(expected = "dropped without resolution")]
1818    async fn unresolved_pending_ack_panics() {
1819        let (sender, _receiver) = oneshot::channel();
1820        drop(PendingAck::new(
1821            EntryAck::User(sender),
1822            vec![transaction().key()],
1823        ));
1824    }
1825
1826    #[tokio::test]
1827    async fn user_lane_close_returns_pending_entries_and_closes_lane() {
1828        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1829        let mut lane = UserLane::Open(PriorityAdmissionQueue::new(1, metrics.clone()));
1830        let consensus_transaction = transaction();
1831        let serialized = bcs::to_bytes(&consensus_transaction).unwrap();
1832        let (sender, receiver) = oneshot::channel();
1833        let entry = PoolEntry {
1834            transactions: vec![Transaction::new(serialized.clone())],
1835            total_bytes: serialized.len(),
1836            gas_price: 1,
1837            tx_type: tx_type_label(std::slice::from_ref(&consensus_transaction)),
1838            proposers: "na",
1839            ack: PendingAck::new(EntryAck::User(sender), vec![consensus_transaction.key()]),
1840            metrics,
1841            processed: Vec::new(),
1842            owned_object_refs: Vec::new(),
1843            rejection: None,
1844            staggered_slot: None,
1845        };
1846        let UserLane::Open(user) = &mut lane else {
1847            unreachable!("new user lane must be open");
1848        };
1849        user.insert(entry).unwrap();
1850
1851        let mut entries = lane.close();
1852        assert!(matches!(lane, UserLane::Closed));
1853        assert_eq!(entries.len(), 1);
1854        entries
1855            .pop()
1856            .unwrap()
1857            .ack
1858            .resolve_error(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1859        assert!(matches!(
1860            receiver.await.unwrap().unwrap_err().as_inner(),
1861            SuiErrorKind::ValidatorHaltedAtEpochEnd
1862        ));
1863    }
1864
1865    #[sim_test]
1866    async fn locked_user_recheck_defuses_ack_after_pool_close() {
1867        let (_state, pool) = test_state_and_pool(1).await;
1868        pool.close();
1869        // The armed ack constructed inside try_insert must be defused when the locked
1870        // check fails — a regression detonates the drop bomb and panics this test.
1871        assert!(matches!(
1872            pool.try_insert(pool.epoch(), 1, vec![transaction()])
1873                .unwrap_err()
1874                .as_inner(),
1875            SuiErrorKind::ValidatorHaltedAtEpochEnd
1876        ));
1877    }
1878
1879    #[test]
1880    fn user_byte_budget_refills_at_rate_up_to_capacity() {
1881        let start = Instant::now();
1882        let mut budget = UserByteBudget::new(1_000, 4_000, start);
1883        assert_eq!(budget.available(start), 4_000);
1884        budget.consume(4_000);
1885        assert_eq!(budget.available(start), 0);
1886        assert_eq!(budget.available(start + Duration::from_millis(500)), 500);
1887        assert_eq!(budget.available(start + Duration::from_secs(10)), 4_000);
1888        budget.consume(1_000);
1889        budget.refund(10_000);
1890        assert_eq!(budget.available(start + Duration::from_secs(10)), 4_000);
1891
1892        let mut budget = UserByteBudget::new(5_000, 4_000, start);
1893        assert_eq!(budget.available(start), 5_000);
1894    }
1895
1896    #[sim_test]
1897    async fn take_rate_limits_user_bytes_but_not_system() {
1898        let serialized_len = bcs::to_bytes(&transaction()).unwrap().len();
1899        let block_bytes = u64::try_from(serialized_len * 2).unwrap();
1900        let (_state, pool) = test_state_and_configured_pool(Some((block_bytes, 1)), None).await;
1901        let epoch = pool.epoch();
1902        for gas_price in [10, 20, 30] {
1903            let (_receiver, _) = pool
1904                .try_insert(epoch, gas_price, vec![transaction()])
1905                .unwrap();
1906        }
1907
1908        let (transactions, ack, limit) = pool.take(usize::MAX, usize::MAX);
1909        assert_eq!(transactions.len(), 2);
1910        assert_eq!(limit, LimitReached::MaxBytes);
1911        ack(block(1));
1912        assert_eq!(pool.metrics.pool_byte_budget_exhausted.get(), 1);
1913
1914        let (transactions, ack, limit) = pool.take(usize::MAX, usize::MAX);
1915        assert!(transactions.is_empty());
1916        assert_eq!(limit, LimitReached::MaxBytes);
1917        drop(ack);
1918        assert_eq!(pool.queue_depth("user"), 1);
1919
1920        let _system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
1921        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
1922        assert_eq!(transactions.len(), 1);
1923        assert_eq!(pool.queue_depth("system"), 0);
1924        assert_eq!(pool.queue_depth("user"), 1);
1925        drop(ack);
1926        pool.close();
1927    }
1928
1929    #[sim_test]
1930    async fn expiry_runs_when_system_lane_saturates_the_block() {
1931        let ttl = Duration::from_secs(1);
1932        let (state, pool) = test_state_and_configured_pool(None, Some(ttl)).await;
1933        let epoch = pool.epoch();
1934
1935        let (user_receiver, _) = pool
1936            .try_insert(epoch, 10, vec![user_transaction(&state)])
1937            .unwrap();
1938        assert_eq!(pool.queue_depth("user"), 1);
1939
1940        // With two queued system transactions, a take limited to one transaction fills the
1941        // block from the system lane and skips the user drain.
1942        let _s1 = pool.submit(epoch, &[transaction()]).unwrap();
1943        let mut s2 = pool.submit(epoch, &[transaction()]).unwrap();
1944
1945        tokio::time::sleep(ttl + Duration::from_secs(1)).await;
1946
1947        let (transactions, ack, limit) = pool.take(1, usize::MAX);
1948        assert_eq!(transactions.len(), 1);
1949        assert_eq!(limit, LimitReached::MaxNumOfTransactions);
1950        ack(block(1));
1951
1952        assert_eq!(pool.queue_depth("user"), 0);
1953        assert_eq!(pool.queue_depth("system"), 1);
1954        assert!(s2.try_recv().is_err());
1955        assert!(matches!(
1956            user_receiver.await.unwrap().unwrap_err().as_inner(),
1957            SuiErrorKind::ValidatorOverloadedRetryAfter {
1958                retry_after_secs: 2
1959            }
1960        ));
1961        pool.close();
1962    }
1963
1964    #[sim_test]
1965    async fn expiry_releases_bytes_and_dedup_keys_without_refunding_budget() {
1966        let ttl = Duration::from_secs(1);
1967        let block_bytes = 1 << 20;
1968        let (state, pool) = test_state_and_configured_pool(Some((block_bytes, 1)), Some(ttl)).await;
1969        let epoch = pool.epoch();
1970        let user_tx = user_transaction(&state);
1971
1972        let (receiver, newly_inserted) = pool.try_insert(epoch, 10, vec![user_tx.clone()]).unwrap();
1973        assert!(newly_inserted);
1974        assert!(pool.metrics.pool_bytes.with_label_values(&["user"]).get() > 0);
1975
1976        tokio::time::sleep(ttl + Duration::from_secs(1)).await;
1977        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
1978        assert!(transactions.is_empty());
1979        drop(ack);
1980
1981        assert_eq!(pool.queue_depth("user"), 0);
1982        assert_eq!(
1983            pool.metrics.pool_bytes.with_label_values(&["user"]).get(),
1984            0
1985        );
1986        assert_eq!(pool.metrics.expired.get(), 1);
1987        assert_eq!(user_byte_budget_tokens(&pool), block_bytes as f64);
1988        assert!(matches!(
1989            receiver.await.unwrap().unwrap_err().as_inner(),
1990            SuiErrorKind::ValidatorOverloadedRetryAfter { .. }
1991        ));
1992
1993        let (_receiver, newly_inserted) = pool.try_insert(epoch, 10, vec![user_tx]).unwrap();
1994        assert!(newly_inserted);
1995        assert_eq!(pool.metrics.duplicate_inserts.get(), 0);
1996        pool.close();
1997    }
1998
1999    #[sim_test]
2000    async fn expiry_reports_already_processed_entries_as_processing() {
2001        let ttl = Duration::from_secs(1);
2002        let (state, pool) = test_state_and_configured_pool(None, Some(ttl)).await;
2003        let epoch = pool.epoch();
2004        let processed = user_transaction(&state);
2005        let (receiver, _) = pool.try_insert(epoch, 10, vec![processed.clone()]).unwrap();
2006
2007        state
2008            .epoch_store_for_testing()
2009            .process_notifications(std::iter::once(&consensus_key(&processed)));
2010        tokio::time::sleep(ttl + Duration::from_secs(1)).await;
2011        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2012        assert!(transactions.is_empty());
2013        drop(ack);
2014
2015        assert_eq!(
2016            pool.already_processed_count("sweep", "consensus_message"),
2017            1
2018        );
2019        assert_eq!(pool.queue_depth("user"), 0);
2020        assert_eq!(
2021            pool.metrics.pool_bytes.with_label_values(&["user"]).get(),
2022            0
2023        );
2024        assert_eq!(pool.metrics.expired.get(), 0);
2025        assert!(matches!(
2026            receiver.await.unwrap().unwrap_err().as_inner(),
2027            SuiErrorKind::TransactionProcessing { .. }
2028        ));
2029        pool.close();
2030    }
2031
2032    #[sim_test]
2033    async fn requeued_entry_keeps_its_age() {
2034        let ttl = Duration::from_secs(10);
2035        let (state, pool) = test_state_and_configured_pool(None, Some(ttl)).await;
2036        let epoch = pool.epoch();
2037        let (receiver, _) = pool
2038            .try_insert(epoch, 10, vec![user_transaction(&state)])
2039            .unwrap();
2040
2041        tokio::time::sleep(ttl - Duration::from_secs(1)).await;
2042        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2043        assert_eq!(transactions.len(), 1);
2044        drop(ack);
2045        assert_eq!(pool.queue_depth("user"), 1);
2046
2047        tokio::time::sleep(Duration::from_secs(2)).await;
2048        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2049        assert!(transactions.is_empty());
2050        drop(ack);
2051        assert_eq!(pool.queue_depth("user"), 0);
2052        assert!(matches!(
2053            receiver.await.unwrap().unwrap_err().as_inner(),
2054            SuiErrorKind::ValidatorOverloadedRetryAfter { .. }
2055        ));
2056        pool.close();
2057    }
2058
2059    #[sim_test]
2060    async fn expiry_reports_consumed_owned_inputs_as_rejected() {
2061        let ttl = Duration::from_secs(1);
2062        let (state, pool) = test_state_and_configured_pool(None, Some(ttl)).await;
2063        let epoch = pool.epoch();
2064        let (sender, _) = get_key_pair::<AccountKeyPair>();
2065        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
2066        let gas_object_ref = gas_object.compute_object_reference();
2067        state.insert_genesis_object(gas_object);
2068        let (receiver, _) = pool
2069            .try_insert(epoch, 10, vec![user_transaction_with_gas(gas_object_ref)])
2070            .unwrap();
2071        state.get_cache_writer().write_object_entry_for_test(
2072            Object::with_id_owner_version_for_testing(
2073                gas_object_ref.0,
2074                gas_object_ref.1.next(),
2075                Owner::AddressOwner(sender),
2076            ),
2077        );
2078
2079        tokio::time::sleep(ttl + Duration::from_secs(1)).await;
2080        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2081        assert!(transactions.is_empty());
2082        drop(ack);
2083
2084        assert_eq!(pool.queue_depth("user"), 0);
2085        assert_eq!(pool.metrics.expired.get(), 0);
2086        assert_eq!(
2087            pool.rejected_at_proposal_count("ObjectVersionUnavailableForConsumption"),
2088            1
2089        );
2090        assert!(matches!(
2091            receiver.await.unwrap().unwrap_err().as_inner(),
2092            SuiErrorKind::UserInputError {
2093                error: UserInputError::ObjectVersionUnavailableForConsumption { .. }
2094            }
2095        ));
2096        pool.close();
2097    }
2098
2099    #[sim_test]
2100    async fn dropped_ack_refunds_user_byte_budget() {
2101        let serialized_len = bcs::to_bytes(&transaction()).unwrap().len();
2102        let block_bytes = u64::try_from(serialized_len).unwrap();
2103        let (_state, pool) = test_state_and_configured_pool(Some((block_bytes, 1)), None).await;
2104        let epoch = pool.epoch();
2105        let (_receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2106
2107        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2108        assert_eq!(transactions.len(), 1);
2109        drop(ack);
2110        assert_eq!(pool.queue_depth("user"), 1);
2111
2112        let (transactions, ack, _) = pool.take(usize::MAX, usize::MAX);
2113        assert_eq!(transactions.len(), 1);
2114        ack(block(1));
2115
2116        let (transactions, ack, limit) = pool.take(usize::MAX, usize::MAX);
2117        assert!(transactions.is_empty());
2118        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2119        drop(ack);
2120        pool.close();
2121    }
2122
2123    #[sim_test]
2124    async fn take_respects_priority_budgets_and_acknowledges_positions() {
2125        let (_state, pool) = test_state_and_pool(10).await;
2126        let epoch = pool.epoch();
2127        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
2128        assert_eq!(pool.queue_depth("system"), 1);
2129        let (low_receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2130        let (high_receiver, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
2131
2132        let (transactions, ack, limit) = pool.take(2, usize::MAX);
2133        assert_eq!(transactions.len(), 2);
2134        assert_eq!(limit, LimitReached::MaxNumOfTransactions);
2135        assert_eq!(pool.queue_depth("system"), 0);
2136        let block_ref = block(5);
2137        ack(block_ref);
2138
2139        let (system_block, system_indices, system_status) = system_receiver.await.unwrap();
2140        assert_eq!(system_block, block_ref);
2141        assert_eq!(system_indices, vec![0]);
2142        let positions = high_receiver.await.unwrap().unwrap();
2143        assert_eq!(
2144            positions,
2145            vec![ConsensusPosition {
2146                epoch,
2147                block: block_ref,
2148                index: 1,
2149            }]
2150        );
2151        pool.notify_committed(vec![block_ref], 0);
2152        assert_eq!(
2153            system_status.await.unwrap(),
2154            BlockStatus::Sequenced(block_ref)
2155        );
2156
2157        let serialized_len = bcs::to_bytes(&transaction()).unwrap().len();
2158        let (transactions, ack, limit) = pool.take(1, serialized_len - 1);
2159        assert!(transactions.is_empty());
2160        assert_eq!(limit, LimitReached::MaxBytes);
2161        drop(ack);
2162
2163        let (transactions, ack, limit) = pool.take(1, serialized_len);
2164        assert_eq!(transactions.len(), 1);
2165        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2166        drop(ack);
2167        assert_eq!(pool.queue_depth("user"), 1);
2168
2169        let (transactions, ack, _) = pool.take(1, serialized_len);
2170        assert_eq!(transactions.len(), 1);
2171        ack(block(6));
2172        assert_eq!(low_receiver.await.unwrap().unwrap()[0].index, 0);
2173    }
2174
2175    #[sim_test]
2176    async fn bundles_are_atomic() {
2177        let (_state, pool) = test_state_and_pool(10).await;
2178        let (_receiver, _) = pool
2179            .try_insert(pool.epoch(), 10, vec![transaction(), transaction()])
2180            .unwrap();
2181        let (transactions, ack, limit) = pool.take(1, usize::MAX);
2182        assert!(transactions.is_empty());
2183        assert_eq!(limit, LimitReached::MaxNumOfTransactions);
2184        drop(ack);
2185
2186        let bundle_bytes = 2 * bcs::to_bytes(&transaction()).unwrap().len();
2187        let (transactions, ack, limit) = pool.take(2, bundle_bytes - 1);
2188        assert!(transactions.is_empty());
2189        assert_eq!(limit, LimitReached::MaxBytes);
2190        drop(ack);
2191
2192        let (transactions, ack, _) = pool.take(2, bundle_bytes);
2193        assert_eq!(transactions.len(), 2);
2194        drop(ack);
2195        pool.close();
2196    }
2197
2198    #[sim_test]
2199    async fn dropped_ack_preserves_front_order() {
2200        let (_state, pool) = test_state_and_pool(10).await;
2201        let epoch = pool.epoch();
2202        let (first, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2203        let (mut second, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2204        let (_, ack, _) = pool.take(2, usize::MAX);
2205        drop(ack);
2206
2207        let (_, ack, _) = pool.take(1, usize::MAX);
2208        ack(block(1));
2209        assert_eq!(first.await.unwrap().unwrap()[0].index, 0);
2210        assert!(second.try_recv().is_err());
2211        pool.close();
2212    }
2213
2214    #[sim_test]
2215    async fn ping_capacity_and_reserved_index() {
2216        let (_state, pool) = test_state_and_pool(1).await;
2217        let epoch = pool.epoch();
2218        let mut receivers = Vec::with_capacity(MAX_PENDING_PINGS);
2219        for _ in 0..MAX_PENDING_PINGS {
2220            receivers.push(pool.submit(epoch, &[]).unwrap());
2221        }
2222        assert!(matches!(
2223            pool.submit(epoch, &[]).err().unwrap().as_inner(),
2224            SuiErrorKind::TooManyTransactionsPendingConsensus
2225        ));
2226
2227        let (transactions, ack, _) = pool.take(0, 0);
2228        assert!(transactions.is_empty());
2229        let block_ref = block(1);
2230        ack(block_ref);
2231        let (ping_block, indices, status) = receivers.remove(0).await.unwrap();
2232        assert_eq!(ping_block, block_ref);
2233        assert_eq!(indices, vec![PING_TRANSACTION_INDEX]);
2234        pool.notify_committed(vec![block_ref], 0);
2235        assert_eq!(status.await.unwrap(), BlockStatus::Sequenced(block_ref));
2236    }
2237
2238    #[sim_test]
2239    async fn system_lane_is_not_bounded_by_user_capacity() {
2240        let (_state, pool) = test_state_and_pool(1).await;
2241        let receivers = (0..3)
2242            .map(|_| pool.submit(pool.epoch(), &[transaction()]).unwrap())
2243            .collect::<Vec<_>>();
2244        assert_eq!(pool.queue_depth("system"), 3);
2245        let (transactions, ack, _) = pool.take(3, usize::MAX);
2246        assert_eq!(transactions.len(), 3);
2247        ack(block(1));
2248        for receiver in receivers {
2249            assert_eq!(receiver.await.unwrap().1.len(), 1);
2250        }
2251    }
2252
2253    #[sim_test]
2254    async fn take_skips_abandoned_system_entries_and_pings() {
2255        let (_state, pool) = test_state_and_pool(1).await;
2256        let epoch = pool.epoch();
2257
2258        // Abandoned entry first, so skipping it must not consume the count budget
2259        // of take(1, ..) below.
2260        let stale = pool.submit(epoch, &[transaction()]).unwrap();
2261        drop(stale);
2262        let live = pool.submit(epoch, &[transaction()]).unwrap();
2263        let stale_ping = pool.submit(epoch, &[]).unwrap();
2264        drop(stale_ping);
2265        let live_ping = pool.submit(epoch, &[]).unwrap();
2266
2267        let (transactions, ack, _) = pool.take(1, usize::MAX);
2268        assert_eq!(transactions.len(), 1);
2269        assert_eq!(pool.queue_depth("system"), 0);
2270        assert_eq!(pool.queue_depth("ping"), 0);
2271        assert_eq!(pool.abandoned_count("system"), 1);
2272        assert_eq!(pool.abandoned_count("ping"), 1);
2273
2274        ack(block(1));
2275        assert_eq!(live.await.unwrap().1.len(), 1);
2276        assert_eq!(live_ping.await.unwrap().1, vec![PING_TRANSACTION_INDEX]);
2277    }
2278
2279    #[sim_test]
2280    async fn eviction_rejection_and_duplicate_detection_match_admission_queue() {
2281        let (_state, pool) = test_state_and_pool(2).await;
2282        let epoch = pool.epoch();
2283        let (first, newly_inserted) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2284        assert!(newly_inserted);
2285        let (_second, newly_inserted) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2286        assert!(!newly_inserted);
2287
2288        let (_high, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
2289        assert!(matches!(
2290            first.await.unwrap().unwrap_err().as_inner(),
2291            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 20 }
2292        ));
2293        assert!(matches!(
2294            pool.try_insert(epoch, 10, vec![transaction()])
2295                .err()
2296                .unwrap()
2297                .as_inner(),
2298            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 10 }
2299        ));
2300        pool.close();
2301    }
2302
2303    #[sim_test]
2304    async fn close_resolves_pending_and_taken_user_entries() {
2305        let (_state, pool) = test_state_and_pool(10).await;
2306        let epoch = pool.epoch();
2307        for wrong_epoch in [epoch + 1, epoch + 2] {
2308            assert!(matches!(
2309                pool.try_insert(wrong_epoch, 1, vec![transaction()])
2310                    .err()
2311                    .unwrap()
2312                    .as_inner(),
2313                SuiErrorKind::ValidatorHaltedAtEpochEnd
2314            ));
2315        }
2316        let (pending, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2317        let (taken, _) = pool.try_insert(epoch, 20, vec![transaction()]).unwrap();
2318        let (_, ack, _) = pool.take(1, usize::MAX);
2319        pool.close();
2320
2321        assert!(matches!(
2322            pending.await.unwrap().unwrap_err().as_inner(),
2323            SuiErrorKind::ValidatorHaltedAtEpochEnd
2324        ));
2325        ack(block(1));
2326        assert!(matches!(
2327            taken.await.unwrap().unwrap_err().as_inner(),
2328            SuiErrorKind::ValidatorHaltedAtEpochEnd
2329        ));
2330        assert!(matches!(
2331            pool.try_insert(epoch, 30, vec![transaction()])
2332                .err()
2333                .unwrap()
2334                .as_inner(),
2335            SuiErrorKind::ValidatorHaltedAtEpochEnd
2336        ));
2337    }
2338
2339    #[sim_test]
2340    async fn dropped_ack_after_close_does_not_requeue() {
2341        let (_state, pool) = test_state_and_pool(10).await;
2342        let epoch = pool.epoch();
2343        let (receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2344        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
2345        let (_, ack, _) = pool.take(2, usize::MAX);
2346        pool.close();
2347        drop(ack);
2348        assert!(matches!(
2349            receiver.await.unwrap().unwrap_err().as_inner(),
2350            SuiErrorKind::ValidatorHaltedAtEpochEnd
2351        ));
2352        assert!(system_receiver.await.is_err());
2353        assert_eq!(pool.queue_depth("user"), 0);
2354    }
2355
2356    #[sim_test]
2357    async fn take_closes_user_lane_when_epoch_stops_accepting_user_certs() {
2358        let (state, pool) = test_state_and_pool(10).await;
2359        let epoch = pool.epoch();
2360        let (user_receiver, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2361        let system_receiver = pool.submit(epoch, &[transaction()]).unwrap();
2362
2363        let epoch_store = state.epoch_store_for_testing();
2364        epoch_store.close_user_certs_for_manual_epoch_close(
2365            epoch_store.get_reconfig_state_write_lock_guard(),
2366        );
2367        let (transactions, ack, limit) = pool.take(10, usize::MAX);
2368        assert_eq!(transactions.len(), 1);
2369        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2370        assert!(matches!(
2371            user_receiver.await.unwrap().unwrap_err().as_inner(),
2372            SuiErrorKind::ValidatorHaltedAtEpochEnd
2373        ));
2374        assert!(matches!(
2375            pool.try_insert(epoch, 20, vec![transaction()])
2376                .err()
2377                .unwrap()
2378                .as_inner(),
2379            SuiErrorKind::ValidatorHaltedAtEpochEnd
2380        ));
2381
2382        let block_ref = block(1);
2383        ack(block_ref);
2384        assert_eq!(system_receiver.await.unwrap().0, block_ref);
2385    }
2386
2387    #[sim_test]
2388    async fn dropped_ack_after_user_lane_close_resolves_taken_entries() {
2389        let (state, pool) = test_state_and_pool(10).await;
2390        let epoch = pool.epoch();
2391        let (taken, _) = pool.try_insert(epoch, 10, vec![transaction()]).unwrap();
2392        let (transactions, ack, _) = pool.take(1, usize::MAX);
2393        assert_eq!(transactions.len(), 1);
2394
2395        // Close the user lane while the taken entry is still out with the proposer.
2396        let epoch_store = state.epoch_store_for_testing();
2397        epoch_store.close_user_certs_for_manual_epoch_close(
2398            epoch_store.get_reconfig_state_write_lock_guard(),
2399        );
2400        let (flushed, flush_ack, _) = pool.take(10, usize::MAX);
2401        assert!(flushed.is_empty());
2402        drop(flush_ack);
2403
2404        // The dropped ack must resolve the taken entry with the halted error instead
2405        // of re-queueing it into the closed lane.
2406        drop(ack);
2407        assert!(matches!(
2408            taken.await.unwrap().unwrap_err().as_inner(),
2409            SuiErrorKind::ValidatorHaltedAtEpochEnd
2410        ));
2411        assert_eq!(pool.queue_depth("user"), 0);
2412    }
2413
2414    #[sim_test]
2415    async fn notify_committed_sequences_commits_before_garbage_collection() {
2416        let (_state, pool) = test_state_and_pool(10).await;
2417        let epoch = pool.epoch();
2418        let first = pool.submit(epoch, &[transaction()]).unwrap();
2419        let (_, ack, _) = pool.take(1, usize::MAX);
2420        let first_block = block(1);
2421        ack(first_block);
2422        let (_, _, first_status) = first.await.unwrap();
2423
2424        let second = pool.submit(epoch, &[transaction()]).unwrap();
2425        let (_, ack, _) = pool.take(1, usize::MAX);
2426        let second_block = block(2);
2427        ack(second_block);
2428        let (_, _, second_status) = second.await.unwrap();
2429
2430        pool.notify_committed(vec![second_block], 1);
2431        assert_eq!(
2432            second_status.await.unwrap(),
2433            BlockStatus::Sequenced(second_block)
2434        );
2435        assert_eq!(
2436            first_status.await.unwrap(),
2437            BlockStatus::GarbageCollected(first_block)
2438        );
2439    }
2440
2441    #[sim_test]
2442    async fn notify_committed_reports_proposed_user_outcomes() {
2443        let (state, pool) = test_state_and_pool(10).await;
2444        let epoch = pool.epoch();
2445        let status = |status: &str| {
2446            pool.adapter_metrics
2447                .sequencing_certificate_status
2448                .with_label_values(&["owned_user_transaction_v2", status])
2449                .get()
2450        };
2451        let commit_latency_count = |lane: &str, proposers: &str| {
2452            pool.metrics
2453                .pool_commit_latency
2454                .with_label_values(&[lane, proposers])
2455                .get_sample_count()
2456        };
2457
2458        // Block 1: one user transaction, later garbage collected.
2459        let (_first, _) = pool
2460            .try_insert(epoch, 1, vec![user_transaction(&state)])
2461            .unwrap();
2462        let (_, ack, _) = pool.take(1, usize::MAX);
2463        ack(block(1));
2464        // Block 2: one user transaction and one system transaction, committed.
2465        let (_second, _) = pool
2466            .try_insert(epoch, 1, vec![user_transaction(&state)])
2467            .unwrap();
2468        let _system = pool.submit(epoch, &[transaction()]).unwrap();
2469        let (_, ack, _) = pool.take(2, usize::MAX);
2470        ack(block(2));
2471        assert_eq!(status("sequenced"), 0);
2472
2473        pool.notify_committed(vec![block(2)], 1);
2474        assert_eq!(status("sequenced"), 1);
2475        assert_eq!(status("garbage_collected"), 1);
2476        // System entries report through the adapter; only their latency is recorded here.
2477        assert_eq!(commit_latency_count("user", "unrestricted"), 1);
2478        assert_eq!(commit_latency_count("system", "na"), 1);
2479    }
2480
2481    #[tokio::test]
2482    async fn insert_validation_rejects_oversized_transactions_and_bundles() {
2483        let serialized_len = u64::try_from(bcs::to_bytes(&transaction()).unwrap().len()).unwrap();
2484
2485        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
2486        config.set_consensus_max_transaction_size_bytes_for_testing(serialized_len - 1);
2487        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
2488        assert!(
2489            pool.try_insert(pool.epoch(), 1, vec![transaction()])
2490                .err()
2491                .unwrap()
2492                .to_string()
2493                .contains("Transaction size")
2494        );
2495
2496        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
2497        config.set_consensus_max_num_transactions_in_block_for_testing(1);
2498        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
2499        assert!(
2500            pool.try_insert(pool.epoch(), 1, vec![transaction(), transaction()])
2501                .err()
2502                .unwrap()
2503                .to_string()
2504                .contains("bundle count")
2505        );
2506
2507        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
2508        config.set_consensus_max_transactions_in_block_bytes_for_testing(serialized_len);
2509        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
2510        assert!(
2511            pool.try_insert(pool.epoch(), 1, vec![transaction(), transaction()])
2512                .err()
2513                .unwrap()
2514                .to_string()
2515                .contains("bundle size")
2516        );
2517    }
2518
2519    #[cfg(debug_assertions)]
2520    #[tokio::test]
2521    #[should_panic(expected = "system transaction failed validation")]
2522    async fn oversized_system_transaction_is_an_invariant_violation() {
2523        let serialized_len = u64::try_from(bcs::to_bytes(&transaction()).unwrap().len()).unwrap();
2524        let mut config = ProtocolConfig::get_for_max_version_UNSAFE();
2525        config.set_consensus_max_transaction_size_bytes_for_testing(serialized_len - 1);
2526        let (_state, pool) = test_state_and_pool_with_protocol_config(10, config).await;
2527        let _ = pool.submit(pool.epoch(), &[transaction()]);
2528    }
2529
2530    #[sim_test]
2531    async fn take_rejects_entries_with_stale_owned_objects_without_consuming_budget() {
2532        let block_bytes = 1 << 20;
2533        let (state, pool) = test_state_and_configured_pool(Some((block_bytes, 1)), None).await;
2534        let epoch = pool.epoch();
2535
2536        // Both bid highest, so they head the pop order: one whose gas object never
2537        // existed, one whose gas object was consumed after it entered the pool.
2538        let missing = user_transaction_with_gas(random_object_ref());
2539        let (missing_receiver, _) = pool.try_insert(epoch, 100, vec![missing]).unwrap();
2540        let (sender, _) = get_key_pair::<AccountKeyPair>();
2541        let gas_object = Object::with_id_owner_for_testing(ObjectID::random(), sender);
2542        let gas_object_ref = gas_object.compute_object_reference();
2543        state.insert_genesis_object(gas_object);
2544        let stale = user_transaction_with_gas(gas_object_ref);
2545        let (stale_receiver, _) = pool.try_insert(epoch, 90, vec![stale]).unwrap();
2546        state.get_cache_writer().write_object_entry_for_test(
2547            Object::with_id_owner_version_for_testing(
2548                gas_object_ref.0,
2549                gas_object_ref.1.next(),
2550                Owner::AddressOwner(sender),
2551            ),
2552        );
2553
2554        let mut live_bytes = 0;
2555        let live = (0..2)
2556            .map(|_| {
2557                let transaction = user_transaction(&state);
2558                live_bytes += bcs::to_bytes(&transaction).unwrap().len() as i64;
2559                pool.try_insert(epoch, 10, vec![transaction]).unwrap().0
2560            })
2561            .collect::<Vec<_>>();
2562
2563        let (transactions, ack, limit) = pool.take(usize::MAX, usize::MAX);
2564        assert_eq!(transactions.len(), 2);
2565        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2566        assert_eq!(pool.queue_depth("user"), 0);
2567        assert_eq!(
2568            pool.metrics.pool_byte_budget_bytes.get(),
2569            block_bytes as i64 - live_bytes
2570        );
2571        assert_eq!(pool.rejected_at_proposal_count("ObjectNotFound"), 1);
2572        assert_eq!(
2573            pool.rejected_at_proposal_count("ObjectVersionUnavailableForConsumption"),
2574            1
2575        );
2576        assert!(matches!(
2577            missing_receiver.await.unwrap().unwrap_err().as_inner(),
2578            SuiErrorKind::UserInputError {
2579                error: UserInputError::ObjectNotFound { .. }
2580            }
2581        ));
2582        assert!(matches!(
2583            stale_receiver.await.unwrap().unwrap_err().as_inner(),
2584            SuiErrorKind::UserInputError {
2585                error: UserInputError::ObjectVersionUnavailableForConsumption { .. }
2586            }
2587        ));
2588
2589        ack(block(1));
2590        for receiver in live {
2591            assert_eq!(receiver.await.unwrap().unwrap().len(), 1);
2592        }
2593    }
2594
2595    #[sim_test]
2596    async fn bundle_is_rejected_at_proposal_when_any_owned_object_is_stale() {
2597        let (state, pool) = test_state_and_pool(10).await;
2598        let epoch = pool.epoch();
2599
2600        let (receiver, _) = pool
2601            .try_insert(
2602                epoch,
2603                10,
2604                vec![
2605                    user_transaction(&state),
2606                    user_transaction_with_gas(random_object_ref()),
2607                ],
2608            )
2609            .unwrap();
2610
2611        let (transactions, ack, limit) = pool.take(usize::MAX, usize::MAX);
2612        assert!(transactions.is_empty());
2613        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2614        assert_eq!(pool.queue_depth("user"), 0);
2615        assert!(matches!(
2616            receiver.await.unwrap().unwrap_err().as_inner(),
2617            SuiErrorKind::UserInputError {
2618                error: UserInputError::ObjectNotFound { .. }
2619            }
2620        ));
2621        drop(ack);
2622    }
2623
2624    #[sim_test]
2625    async fn take_skips_already_processed_entries_without_shrinking_the_block() {
2626        let (state, pool) = test_state_and_pool(10).await;
2627        let epoch = pool.epoch();
2628        let epoch_store = state.epoch_store_for_testing();
2629
2630        // Both bid highest, so they head the pop order and would consume the whole
2631        // budget below if they were not skipped — one observed through consensus
2632        // output, one through checkpoint execution.
2633        let processed = user_transaction(&state);
2634        let (processed_consensus, _) = pool
2635            .try_insert(epoch, 100, vec![processed.clone()])
2636            .unwrap();
2637        let executed = user_transaction(&state);
2638        let (processed_checkpoint, _) = pool.try_insert(epoch, 90, vec![executed.clone()]).unwrap();
2639        let live = (0..3)
2640            .map(|_| {
2641                pool.try_insert(epoch, 10, vec![user_transaction(&state)])
2642                    .unwrap()
2643                    .0
2644            })
2645            .collect::<Vec<_>>();
2646
2647        epoch_store.process_notifications(std::iter::once(&consensus_key(&processed)));
2648        epoch_store
2649            .insert_finalized_transactions(&[digest_of(&executed)], 1)
2650            .unwrap();
2651
2652        let (transactions, ack, limit) = pool.take(3, usize::MAX);
2653        assert_eq!(transactions.len(), 3);
2654        assert_eq!(limit, LimitReached::AllTransactionsIncluded);
2655        assert_eq!(pool.queue_depth("user"), 0);
2656        assert_eq!(
2657            pool.already_processed_count("proposal", "consensus_message"),
2658            1
2659        );
2660        assert_eq!(
2661            pool.already_processed_count("proposal", "checkpoint_execution"),
2662            1
2663        );
2664        for receiver in [processed_consensus, processed_checkpoint] {
2665            assert!(matches!(
2666                receiver.await.unwrap().unwrap_err().as_inner(),
2667                SuiErrorKind::TransactionProcessing { .. }
2668            ));
2669        }
2670
2671        ack(block(1));
2672        for receiver in live {
2673            assert_eq!(receiver.await.unwrap().unwrap().len(), 1);
2674        }
2675    }
2676
2677    #[sim_test]
2678    async fn bundles_are_skipped_only_when_every_transaction_is_already_processed() {
2679        let (state, pool) = test_state_and_pool(10).await;
2680        let epoch = pool.epoch();
2681        let epoch_store = state.epoch_store_for_testing();
2682
2683        let first = user_transaction(&state);
2684        let second = user_transaction(&state);
2685        let (receiver, _) = pool
2686            .try_insert(epoch, 10, vec![first.clone(), second.clone()])
2687            .unwrap();
2688
2689        epoch_store.process_notifications(std::iter::once(&consensus_key(&first)));
2690        let (transactions, ack, _) = pool.take(10, usize::MAX);
2691        assert_eq!(transactions.len(), 2);
2692        assert_eq!(
2693            pool.already_processed_count("proposal", "consensus_message"),
2694            0
2695        );
2696        // Requeue instead of acknowledging: the next proposal must still remember that
2697        // `first` was observed as processed, since its notification is delivered once.
2698        drop(ack);
2699
2700        epoch_store.process_notifications(std::iter::once(&consensus_key(&second)));
2701        let (transactions, ack, _) = pool.take(10, usize::MAX);
2702        assert!(transactions.is_empty());
2703        assert_eq!(
2704            pool.already_processed_count("proposal", "consensus_message"),
2705            1
2706        );
2707        assert!(matches!(
2708            receiver.await.unwrap().unwrap_err().as_inner(),
2709            SuiErrorKind::TransactionProcessing { .. }
2710        ));
2711        drop(ack);
2712    }
2713
2714    #[sim_test]
2715    async fn insert_rejects_already_processed_transactions() {
2716        let (state, pool) = test_state_and_pool(10).await;
2717        let epoch = pool.epoch();
2718        let epoch_store = state.epoch_store_for_testing();
2719
2720        // An already-processed insert must fail before the ack is armed — a regression detonates
2721        // the drop bomb and panics this test.
2722        let via_consensus = user_transaction(&state);
2723        epoch_store.test_insert_user_signature(digest_of(&via_consensus), vec![]);
2724        assert!(matches!(
2725            pool.try_insert(epoch, 10, vec![via_consensus])
2726                .unwrap_err()
2727                .as_inner(),
2728            SuiErrorKind::TransactionProcessing { .. }
2729        ));
2730        assert_eq!(
2731            pool.already_processed_count("insert", "consensus_message"),
2732            1
2733        );
2734
2735        let via_checkpoint = user_transaction(&state);
2736        epoch_store
2737            .insert_finalized_transactions(&[digest_of(&via_checkpoint)], 1)
2738            .unwrap();
2739        assert!(matches!(
2740            pool.try_insert(epoch, 10, vec![via_checkpoint])
2741                .unwrap_err()
2742                .as_inner(),
2743            SuiErrorKind::TransactionProcessing { .. }
2744        ));
2745        assert_eq!(
2746            pool.already_processed_count("insert", "checkpoint_execution"),
2747            1
2748        );
2749
2750        assert_eq!(pool.queue_depth("user"), 0);
2751        let (transactions, ack, _) = pool.take(10, usize::MAX);
2752        assert!(transactions.is_empty());
2753        drop(ack);
2754    }
2755
2756    #[sim_test]
2757    async fn departing_entries_deregister_their_processed_watches() {
2758        let (state, pool) = test_state_and_pool(1).await;
2759        let epoch = pool.epoch();
2760        let epoch_store = state.epoch_store_for_testing();
2761        // Each user key registers with both the consensus and the checkpoint registry.
2762        let baseline = epoch_store.num_pending_processed_notifications();
2763
2764        let evicted_tx = user_transaction(&state);
2765        let (evicted, _) = pool
2766            .try_insert(epoch, 10, vec![evicted_tx.clone()])
2767            .unwrap();
2768        assert_eq!(
2769            epoch_store.num_pending_processed_notifications(),
2770            baseline + 2
2771        );
2772
2773        let taken_tx = user_transaction(&state);
2774        let (taken, _) = pool.try_insert(epoch, 20, vec![taken_tx.clone()]).unwrap();
2775        assert!(matches!(
2776            evicted.await.unwrap().unwrap_err().as_inner(),
2777            SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { .. }
2778        ));
2779        assert_eq!(
2780            epoch_store.num_pending_processed_notifications(),
2781            baseline + 2
2782        );
2783
2784        let (transactions, ack, _) = pool.take(10, usize::MAX);
2785        assert_eq!(transactions.len(), 1);
2786        ack(block(1));
2787        taken.await.unwrap().unwrap();
2788        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2789
2790        let closed_tx = user_transaction(&state);
2791        let (closed, _) = pool.try_insert(epoch, 10, vec![closed_tx.clone()]).unwrap();
2792        assert_eq!(
2793            epoch_store.num_pending_processed_notifications(),
2794            baseline + 2
2795        );
2796        pool.close();
2797        assert!(matches!(
2798            closed.await.unwrap().unwrap_err().as_inner(),
2799            SuiErrorKind::ValidatorHaltedAtEpochEnd
2800        ));
2801        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2802
2803        for transaction in [&evicted_tx, &taken_tx, &closed_tx] {
2804            epoch_store.process_notifications(std::iter::once(&consensus_key(transaction)));
2805            epoch_store
2806                .insert_finalized_transactions(&[digest_of(transaction)], 1)
2807                .unwrap();
2808        }
2809        assert_eq!(epoch_store.num_pending_processed_notifications(), baseline);
2810    }
2811
2812    #[sim_test]
2813    async fn zero_user_capacity_is_rejected_at_startup() {
2814        let state = TestAuthorityBuilder::new().build().await;
2815        let epoch_store = state.epoch_store_for_testing().clone();
2816        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2817            ConsensusTransactionPool::new_for_tests(
2818                epoch_store,
2819                state.get_cache_writer().clone(),
2820                0,
2821                Arc::new(AdmissionQueueMetrics::new_for_tests()),
2822            )
2823        }));
2824        assert!(result.is_err());
2825    }
2826
2827    #[sim_test]
2828    async fn context_observes_installed_pool_and_waits_for_matching_epoch() {
2829        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
2830        let state = TestAuthorityBuilder::new().build().await;
2831        let context = Arc::new(TransactionPoolContext::new_for_tests(
2832            metrics,
2833            state.get_cache_writer().clone(),
2834        ));
2835        let pool = pool_for_current_epoch(&state, 10);
2836        let epoch = pool.epoch();
2837        let absent_waiter = tokio::spawn({
2838            let context = context.clone();
2839            async move { context.wait_for_pool(epoch).await }
2840        });
2841        while context.metrics().pool_waiting_inserts.get() == 0 {
2842            tokio::task::yield_now().await;
2843        }
2844        assert_eq!(context.metrics().pool_waiting_inserts.get(), 1);
2845        context.set_active(epoch, pool.clone());
2846        assert_eq!(absent_waiter.await.unwrap().unwrap().epoch(), epoch);
2847
2848        // send_replace retains the active state after the only receiver is gone.
2849        assert_eq!(context.wait_for_pool(epoch).await.unwrap().epoch(), epoch);
2850
2851        let race_context = Arc::new(TransactionPoolContext::new_for_tests(
2852            Arc::new(AdmissionQueueMetrics::new_for_tests()),
2853            state.get_cache_writer().clone(),
2854        ));
2855        let setter = race_context.clone();
2856        let race_pool = pool.clone();
2857        assert_eq!(
2858            race_context
2859                .wait_for_pool_after_subscribe(epoch, move || {
2860                    setter.set_active(epoch, race_pool);
2861                })
2862                .await
2863                .unwrap()
2864                .epoch(),
2865            epoch
2866        );
2867
2868        let next_epoch = epoch + 1;
2869        let context_waiter = context.clone();
2870        let waiter = tokio::spawn(async move { context_waiter.wait_for_pool(next_epoch).await });
2871        while context.metrics().pool_waiting_inserts.get() == 0 {
2872            tokio::task::yield_now().await;
2873        }
2874        assert_eq!(context.metrics().pool_waiting_inserts.get(), 1);
2875        state.reconfigure_for_testing().await;
2876        let next_pool = pool_for_current_epoch(&state, 10);
2877        assert_eq!(next_pool.epoch(), next_epoch);
2878        context.set_active(next_epoch, next_pool.clone());
2879        assert_eq!(waiter.await.unwrap().unwrap().epoch(), next_epoch);
2880    }
2881
2882    #[sim_test]
2883    async fn context_fails_promptly_when_active_pool_is_dropped() {
2884        let state = TestAuthorityBuilder::new().build().await;
2885        let context = TransactionPoolContext::new_for_tests(
2886            Arc::new(AdmissionQueueMetrics::new_for_tests()),
2887            state.get_cache_writer().clone(),
2888        );
2889        let pool = pool_for_current_epoch(&state, 10);
2890        let epoch = pool.epoch();
2891        context.set_active(epoch, pool.clone());
2892        assert_eq!(context.wait_for_pool(epoch).await.unwrap().epoch(), epoch);
2893
2894        // The context holds the pool weakly, so an abrupt teardown that drops the
2895        // pool without publishing a new state must fail waiters rather than hang.
2896        drop(pool);
2897        let Err(error) = context.wait_for_pool(epoch).await else {
2898            panic!("waiting on a dropped pool must fail");
2899        };
2900        assert!(matches!(
2901            error.as_inner(),
2902            SuiErrorKind::TooManyTransactionsPendingConsensus
2903        ));
2904    }
2905
2906    #[sim_test]
2907    async fn context_fails_stale_and_unavailable_epochs_promptly() {
2908        // Advance to epoch 1 so a stale (older-epoch) caller can exist.
2909        let state = TestAuthorityBuilder::new().build().await;
2910        let context = TransactionPoolContext::new_for_tests(
2911            Arc::new(AdmissionQueueMetrics::new_for_tests()),
2912            state.get_cache_writer().clone(),
2913        );
2914        state.reconfigure_for_testing().await;
2915        let pool = pool_for_current_epoch(&state, 10);
2916        let active_epoch = pool.epoch();
2917        context.set_active(active_epoch, pool);
2918        assert!(matches!(
2919            context
2920                .wait_for_pool(active_epoch - 1)
2921                .await
2922                .err()
2923                .unwrap()
2924                .as_inner(),
2925            SuiErrorKind::ValidatorHaltedAtEpochEnd
2926        ));
2927        context.set_unavailable(active_epoch + 1);
2928        assert!(matches!(
2929            context
2930                .wait_for_pool(active_epoch + 1)
2931                .await
2932                .err()
2933                .unwrap()
2934                .as_inner(),
2935            SuiErrorKind::TooManyTransactionsPendingConsensus
2936        ));
2937        assert!(matches!(
2938            context
2939                .wait_for_pool(active_epoch)
2940                .await
2941                .err()
2942                .unwrap()
2943                .as_inner(),
2944            SuiErrorKind::ValidatorHaltedAtEpochEnd
2945        ));
2946    }
2947
2948    #[tokio::test]
2949    async fn transaction_pool_client_maps_ping_ack_and_status() {
2950        let state = TestAuthorityBuilder::new().build().await;
2951        let epoch_store = state.epoch_store_for_testing().clone();
2952        let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
2953        let context = Arc::new(TransactionPoolContext::new_for_tests(
2954            metrics.clone(),
2955            state.get_cache_writer().clone(),
2956        ));
2957        let pool = Arc::new(ConsensusTransactionPool::new_for_tests(
2958            epoch_store.clone(),
2959            state.get_cache_writer().clone(),
2960            10,
2961            metrics,
2962        ));
2963        context.set_active(epoch_store.epoch(), pool.clone());
2964        let client = Arc::new(TransactionPoolClient::new(context));
2965
2966        let task = tokio::spawn({
2967            let client = client.clone();
2968            let epoch_store = epoch_store.clone();
2969            async move { client.submit(&[], &epoch_store).await }
2970        });
2971        while pool.queue_depth("ping") == 0 {
2972            tokio::task::yield_now().await;
2973        }
2974        let (_, ack, _) = pool.take(0, 0);
2975        let block_ref = block(3);
2976        ack(block_ref);
2977        let (positions, status) = task.await.unwrap().unwrap();
2978        assert_eq!(positions[0].block, block_ref);
2979        assert_eq!(positions[0].index, PING_TRANSACTION_INDEX);
2980        pool.notify_committed(vec![block_ref], 0);
2981        assert_eq!(status.await.unwrap(), BlockStatus::Sequenced(block_ref));
2982
2983        let user_transaction = user_transaction(&state);
2984        let task = tokio::spawn({
2985            let client = client.clone();
2986            let epoch_store = epoch_store.clone();
2987            async move { client.submit(&[user_transaction], &epoch_store).await }
2988        });
2989        match task.await {
2990            // debug_fatal panics in debug/sim builds.
2991            Err(join_error) => assert!(join_error.is_panic()),
2992            // Release builds log and return the rejection instead.
2993            Ok(result) => assert!(result.is_err()),
2994        }
2995        assert_eq!(pool.queue_depth("user"), 0);
2996        assert_eq!(pool.queue_depth("system"), 0);
2997    }
2998}