Skip to main content

sui_core/
authority_server.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::Result;
6use async_trait::async_trait;
7use fastcrypto::traits::KeyPair;
8use futures::{TryFutureExt, future};
9use itertools::Itertools as _;
10use moka::sync::Cache;
11use mysten_common::ZipDebugEqIteratorExt;
12use mysten_common::{assert_reachable, debug_fatal};
13use mysten_metrics::spawn_monitored_task;
14use parking_lot::Mutex;
15use prometheus::{
16    Gauge, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, Registry,
17    register_gauge_with_registry, register_histogram_vec_with_registry,
18    register_histogram_with_registry, register_int_counter_vec_with_registry,
19    register_int_counter_with_registry, register_int_gauge_with_registry,
20};
21use std::{
22    collections::{HashMap, HashSet},
23    io,
24    net::{IpAddr, SocketAddr},
25    sync::Arc,
26    time::{Duration, Instant, SystemTime},
27};
28use sui_network::{
29    api::{Validator, ValidatorServer},
30    tonic,
31    validator::server::SUI_TLS_SERVER_NAME,
32};
33use sui_types::effects::TransactionEffectsAPI;
34use sui_types::message_envelope::Message;
35use sui_types::messages_consensus::{ConsensusTransaction, ConsensusTransactionKey};
36use sui_types::messages_grpc::{
37    ObjectInfoRequest, ObjectInfoResponse, RawSubmitTxResponse, SystemStateRequest,
38    TransactionInfoRequest, TransactionInfoResponse,
39};
40use sui_types::multiaddr::Multiaddr;
41use sui_types::object::Object;
42use sui_types::sui_system_state::SuiSystemState;
43use sui_types::traffic_control::{ClientIdSource, Weight};
44use sui_types::{
45    base_types::ObjectID,
46    digests::{TransactionDigest, TransactionEffectsDigest},
47    error::{SuiErrorKind, UserInputError},
48};
49use sui_types::{
50    effects::TransactionEffects,
51    messages_grpc::{
52        ExecutedData, RawSubmitTxRequest, RawWaitForEffectsRequest, RawWaitForEffectsResponse,
53        SubmitTxResult, WaitForEffectsRequest, WaitForEffectsResponse,
54    },
55};
56use sui_types::{effects::TransactionEvents, messages_grpc::SubmitTxType};
57use sui_types::{error::*, transaction::*};
58use sui_types::{
59    fp_ensure,
60    messages_checkpoint::{
61        CheckpointRequest, CheckpointRequestV2, CheckpointResponse, CheckpointResponseV2,
62    },
63};
64use tokio::time::timeout;
65use tonic::metadata::{Ascii, MetadataValue};
66use tracing::{debug, error, info, instrument};
67
68use crate::admission_queue::{AdmissionQueueContext, AdmissionQueueManager};
69use crate::gasless_rate_limiter::GaslessRateLimiter;
70use crate::{
71    authority::{AuthorityState, consensus_tx_status_cache::ConsensusTxStatus},
72    consensus_adapter::{ConsensusAdapter, ConsensusAdapterMetrics, ConsensusOverloadChecker},
73    consensus_handler::SequencedConsensusTransactionKey,
74    traffic_controller::{TrafficController, parse_ip, policies::TrafficTally},
75};
76use crate::{
77    authority::{
78        authority_per_epoch_store::AuthorityPerEpochStore,
79        consensus_tx_status_cache::NotifyReadConsensusTxStatusResult,
80    },
81    checkpoints::CheckpointStore,
82    mysticeti_adapter::LazyMysticetiClient,
83};
84use sui_config::local_ip_utils::new_local_tcp_address_for_testing;
85
86#[cfg(test)]
87#[path = "unit_tests/server_tests.rs"]
88mod server_tests;
89
90#[cfg(test)]
91#[path = "unit_tests/wait_for_effects_tests.rs"]
92mod wait_for_effects_tests;
93
94#[cfg(test)]
95#[path = "unit_tests/submit_transaction_tests.rs"]
96mod submit_transaction_tests;
97
98pub struct AuthorityServerHandle {
99    server_handle: sui_network::validator::server::Server,
100}
101
102impl AuthorityServerHandle {
103    pub async fn join(self) -> Result<(), io::Error> {
104        self.server_handle.handle().wait_for_shutdown().await;
105        Ok(())
106    }
107
108    pub async fn kill(self) -> Result<(), io::Error> {
109        self.server_handle.handle().shutdown().await;
110        Ok(())
111    }
112
113    pub fn address(&self) -> &Multiaddr {
114        self.server_handle.local_addr()
115    }
116}
117
118pub struct AuthorityServer {
119    address: Multiaddr,
120    pub state: Arc<AuthorityState>,
121    consensus_adapter: Arc<ConsensusAdapter>,
122    pub metrics: Arc<ValidatorServiceMetrics>,
123}
124
125impl AuthorityServer {
126    pub fn new_for_test_with_consensus_adapter(
127        state: Arc<AuthorityState>,
128        consensus_adapter: Arc<ConsensusAdapter>,
129    ) -> Self {
130        let address = new_local_tcp_address_for_testing();
131        let metrics = Arc::new(ValidatorServiceMetrics::new_for_tests());
132
133        Self {
134            address,
135            state,
136            consensus_adapter,
137            metrics,
138        }
139    }
140
141    pub fn new_for_test(state: Arc<AuthorityState>) -> Self {
142        let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
143        let consensus_adapter = Arc::new(ConsensusAdapter::new(
144            Arc::new(LazyMysticetiClient::new()),
145            CheckpointStore::new_for_tests(),
146            state.name,
147            100_000,
148            100_000,
149            ConsensusAdapterMetrics::new_test(),
150            slot_freed_notify,
151        ));
152        Self::new_for_test_with_consensus_adapter(state, consensus_adapter)
153    }
154
155    pub async fn spawn_for_test(self) -> Result<AuthorityServerHandle, io::Error> {
156        let address = self.address.clone();
157        self.spawn_with_bind_address_for_test(address).await
158    }
159
160    pub async fn spawn_with_bind_address_for_test(
161        self,
162        address: Multiaddr,
163    ) -> Result<AuthorityServerHandle, io::Error> {
164        let tls_config = sui_tls::create_rustls_server_config(
165            self.state.config.network_key_pair().copy().private(),
166            SUI_TLS_SERVER_NAME.to_string(),
167        );
168        let config = mysten_network::config::Config::new();
169        let server = sui_network::validator::server::ServerBuilder::from_config(
170            &config,
171            mysten_network::metrics::DefaultMetricsCallbackProvider::default(),
172        )
173        .add_service(ValidatorServer::new(ValidatorService::new_for_tests(
174            self.state,
175            self.consensus_adapter,
176            self.metrics,
177        )))
178        .bind(&address, Some(tls_config))
179        .await
180        .unwrap();
181        let local_addr = server.local_addr().to_owned();
182        info!("Listening to traffic on {local_addr}");
183        let handle = AuthorityServerHandle {
184            server_handle: server,
185        };
186        Ok(handle)
187    }
188}
189
190pub struct ValidatorServiceMetrics {
191    pub signature_errors: IntCounter,
192    pub tx_verification_latency: Histogram,
193    pub handle_transaction_latency: Histogram,
194    pub handle_transaction_consensus_latency: Histogram,
195    pub handle_submit_transaction_consensus_latency: HistogramVec,
196    pub handle_wait_for_effects_ping_latency: HistogramVec,
197
198    handle_submit_transaction_latency: HistogramVec,
199    handle_submit_transaction_bytes: HistogramVec,
200    handle_submit_transaction_batch_size: HistogramVec,
201
202    num_rejected_tx_during_overload: IntCounterVec,
203    submission_rejected_transactions: IntCounterVec,
204    submission_suppressed_already_processed: IntCounterVec,
205    submission_suppressed_recently_submitted: IntCounterVec,
206    recently_submitted_cache_size: IntGauge,
207    recently_submitted_resubmission_interval: Histogram,
208    submission_suppressed_inflight: IntCounterVec,
209    inflight_transactions: IntGauge,
210    connection_ip_not_found: IntCounter,
211    forwarded_header_parse_error: IntCounter,
212    forwarded_header_invalid: IntCounter,
213    forwarded_header_not_included: IntCounter,
214    client_id_source_config_mismatch: IntCounter,
215    x_forwarded_for_num_hops: Gauge,
216    pub gasless_rate_limited_count: IntCounter,
217    pub gasless_submission_outcomes: IntCounterVec,
218}
219
220impl ValidatorServiceMetrics {
221    pub fn new(registry: &Registry) -> Self {
222        Self {
223            signature_errors: register_int_counter_with_registry!(
224                "total_signature_errors",
225                "Number of transaction signature errors",
226                registry,
227            )
228            .unwrap(),
229            tx_verification_latency: register_histogram_with_registry!(
230                "validator_service_tx_verification_latency",
231                "Latency of verifying a transaction",
232                mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
233                registry,
234            )
235            .unwrap(),
236            handle_transaction_latency: register_histogram_with_registry!(
237                "validator_service_handle_transaction_latency",
238                "Latency of handling a transaction",
239                mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
240                registry,
241            )
242            .unwrap(),
243            handle_transaction_consensus_latency: register_histogram_with_registry!(
244                "validator_service_handle_transaction_consensus_latency",
245                "Latency of handling a user transaction sent through consensus",
246                mysten_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
247                registry,
248            )
249            .unwrap(),
250            handle_submit_transaction_consensus_latency: register_histogram_vec_with_registry!(
251                "validator_service_submit_transaction_consensus_latency",
252                "Latency of submitting a user transaction sent through consensus",
253                &["req_type"],
254                mysten_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
255                registry,
256            )
257            .unwrap(),
258            handle_submit_transaction_latency: register_histogram_vec_with_registry!(
259                "validator_service_submit_transaction_latency",
260                "Latency of submit transaction handler",
261                &["req_type"],
262                mysten_metrics::LATENCY_SEC_BUCKETS.to_vec(),
263                registry,
264            )
265            .unwrap(),
266            handle_wait_for_effects_ping_latency: register_histogram_vec_with_registry!(
267                "validator_service_handle_wait_for_effects_ping_latency",
268                "Latency of handling a ping request for wait_for_effects",
269                &["req_type"],
270                mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
271                registry,
272            )
273            .unwrap(),
274            handle_submit_transaction_bytes: register_histogram_vec_with_registry!(
275                "validator_service_submit_transaction_bytes",
276                "The size of transactions in the submit transaction request",
277                &["req_type"],
278                mysten_metrics::BYTES_BUCKETS.to_vec(),
279                registry,
280            )
281            .unwrap(),
282            handle_submit_transaction_batch_size: register_histogram_vec_with_registry!(
283                "validator_service_submit_transaction_batch_size",
284                "The number of transactions in the submit transaction request",
285                &["req_type"],
286                mysten_metrics::COUNT_BUCKETS.to_vec(),
287                registry,
288            )
289            .unwrap(),
290            num_rejected_tx_during_overload: register_int_counter_vec_with_registry!(
291                "validator_service_num_rejected_tx_during_overload",
292                "Number of rejected transaction due to system overload",
293                &["error_type"],
294                registry,
295            )
296            .unwrap(),
297            submission_rejected_transactions: register_int_counter_vec_with_registry!(
298                "validator_service_submission_rejected_transactions",
299                "Number of transactions rejected during submission",
300                &["reason"],
301                registry,
302            )
303            .unwrap(),
304            submission_suppressed_already_processed: register_int_counter_vec_with_registry!(
305                "validator_service_submission_suppressed_already_processed",
306                "Number of submitted transactions suppressed because consensus had already \
307                 processed them this epoch (re-submission of already-processed transactions)",
308                &["req_type"],
309                registry,
310            )
311            .unwrap(),
312            submission_suppressed_recently_submitted: register_int_counter_vec_with_registry!(
313                "validator_service_submission_suppressed_recently_submitted",
314                "Number of submitted transactions suppressed because the same transaction was \
315                 submitted within the recent-submission window",
316                &["req_type"],
317                registry,
318            )
319            .unwrap(),
320            recently_submitted_cache_size: register_int_gauge_with_registry!(
321                "validator_service_recently_submitted_cache_size",
322                "Approximate number of transaction digests held in the recent-submission duplicate-suppression cache",
323                registry,
324            )
325            .unwrap(),
326            recently_submitted_resubmission_interval: register_histogram_with_registry!(
327                "validator_service_recently_submitted_resubmission_interval_seconds",
328                "Time between a transaction being recorded and a duplicate resubmission of it being suppressed",
329                mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
330                registry,
331            )
332            .unwrap(),
333            submission_suppressed_inflight: register_int_counter_vec_with_registry!(
334                "validator_service_submission_suppressed_inflight",
335                "Number of submitted transactions suppressed because the same transaction was \
336                 already being handled by a concurrent in-flight submit request",
337                &["req_type"],
338                registry,
339            )
340            .unwrap(),
341            inflight_transactions: register_int_gauge_with_registry!(
342                "validator_service_inflight_transactions",
343                "Number of transactions from inflight submit requests",
344                registry,
345            )
346            .unwrap(),
347            connection_ip_not_found: register_int_counter_with_registry!(
348                "validator_service_connection_ip_not_found",
349                "Number of times connection IP was not extractable from request",
350                registry,
351            )
352            .unwrap(),
353            forwarded_header_parse_error: register_int_counter_with_registry!(
354                "validator_service_forwarded_header_parse_error",
355                "Number of times x-forwarded-for header could not be parsed",
356                registry,
357            )
358            .unwrap(),
359            forwarded_header_invalid: register_int_counter_with_registry!(
360                "validator_service_forwarded_header_invalid",
361                "Number of times x-forwarded-for header was invalid",
362                registry,
363            )
364            .unwrap(),
365            forwarded_header_not_included: register_int_counter_with_registry!(
366                "validator_service_forwarded_header_not_included",
367                "Number of times x-forwarded-for header was (unexpectedly) not included in request",
368                registry,
369            )
370            .unwrap(),
371            client_id_source_config_mismatch: register_int_counter_with_registry!(
372                "validator_service_client_id_source_config_mismatch",
373                "Number of times detected that client id source config doesn't agree with x-forwarded-for header",
374                registry,
375            )
376            .unwrap(),
377            x_forwarded_for_num_hops: register_gauge_with_registry!(
378                "validator_service_x_forwarded_for_num_hops",
379                "Number of hops in x-forwarded-for header",
380                registry,
381            )
382            .unwrap(),
383            gasless_rate_limited_count: register_int_counter_with_registry!(
384                "validator_service_gasless_rate_limited_count",
385                "Number of gasless transactions rejected by rate limiter",
386                registry,
387            )
388            .unwrap(),
389            gasless_submission_outcomes: register_int_counter_vec_with_registry!(
390                "validator_service_gasless_submission_outcomes",
391                "Number of valid gasless transaction submissions by outcome",
392                &["outcome"],
393                registry,
394            )
395            .unwrap(),
396        }
397    }
398
399    pub fn new_for_tests() -> Self {
400        let registry = Registry::new();
401        Self::new(&registry)
402    }
403}
404
405/// Where `handle_submit_transaction` routes a request.
406enum AdmissionQueueSubmitMode {
407    /// Admit via the gas-price priority queue.
408    Queue,
409    /// Submit directly to consensus, bypassing the queue — used when the queue
410    /// is turned off by config, temporarily disabled by failover, or for a ping
411    /// request. Individual txs are rejected when consensus is saturated
412    /// (pre-queue behavior).
413    Direct,
414}
415
416#[derive(Clone)]
417pub struct ValidatorService {
418    state: Arc<AuthorityState>,
419    consensus_adapter: Arc<ConsensusAdapter>,
420    metrics: Arc<ValidatorServiceMetrics>,
421    traffic_controller: Option<Arc<TrafficController>>,
422    client_id_source: Option<ClientIdSource>,
423    gasless_limiter: GaslessRateLimiter,
424    admission_queue: Option<AdmissionQueueContext>,
425    /// Digests submitted within the last `recent_submission_window` (value: when recorded), to
426    /// drop duplicate resubmissions before they reach consensus.
427    recently_submitted: Cache<TransactionDigest, Instant>,
428    /// How long a transaction is suppressed after submission (from node config).
429    recent_submission_window: Duration,
430    /// Digests currently being handled by an in-flight submit handler. Acquired atomically at
431    /// entry and removed (then demoted into `recently_submitted`) when the handler returns.
432    inflight_transactions: Arc<Mutex<HashSet<TransactionDigest>>>,
433}
434
435/// Assumed peak distinct-submission rate, used to size the dedup cache (per window).
436const RECENT_SUBMISSION_PEAK_TPS: u64 = 50_000;
437
438impl ValidatorService {
439    pub fn new(
440        state: Arc<AuthorityState>,
441        consensus_adapter: Arc<ConsensusAdapter>,
442        validator_metrics: Arc<ValidatorServiceMetrics>,
443        client_id_source: Option<ClientIdSource>,
444        admission_queue: Option<AdmissionQueueContext>,
445    ) -> Self {
446        let traffic_controller = state.traffic_controller.clone();
447        let gasless_limiter = GaslessRateLimiter::new(state.consensus_gasless_counter.clone());
448        let recent_submission_window = state.config.recent_submission_dedup_window();
449        Self {
450            state,
451            consensus_adapter,
452            metrics: validator_metrics,
453            traffic_controller,
454            client_id_source,
455            gasless_limiter,
456            admission_queue,
457            recently_submitted: Self::new_recently_submitted_cache(recent_submission_window),
458            recent_submission_window,
459            inflight_transactions: Arc::new(Mutex::new(HashSet::new())),
460        }
461    }
462
463    fn new_recently_submitted_cache(window: Duration) -> Cache<TransactionDigest, Instant> {
464        // Memory backstop only; the window bounds the cache, and amplified duplicates do not add
465        // entries (they share a digest). Sized for roughly one window at peak throughput.
466        let max_capacity = window.as_secs().max(1) * RECENT_SUBMISSION_PEAK_TPS;
467        Cache::builder()
468            .time_to_live(window)
469            .max_capacity(max_capacity)
470            .build()
471    }
472
473    pub fn new_for_tests(
474        state: Arc<AuthorityState>,
475        consensus_adapter: Arc<ConsensusAdapter>,
476        metrics: Arc<ValidatorServiceMetrics>,
477    ) -> Self {
478        let gasless_limiter = GaslessRateLimiter::new(state.consensus_gasless_counter.clone());
479        let epoch_store = state.epoch_store_for_testing().clone();
480        let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
481        let manager = Arc::new(AdmissionQueueManager::new_for_tests(
482            consensus_adapter.clone(),
483            slot_freed_notify,
484        ));
485        let admission_queue = Some(AdmissionQueueContext::spawn(manager, epoch_store));
486        let recent_submission_window = state.config.recent_submission_dedup_window();
487        Self {
488            state,
489            consensus_adapter,
490            metrics,
491            traffic_controller: None,
492            client_id_source: None,
493            gasless_limiter,
494            admission_queue,
495            recently_submitted: Self::new_recently_submitted_cache(recent_submission_window),
496            recent_submission_window,
497            inflight_transactions: Arc::new(Mutex::new(HashSet::new())),
498        }
499    }
500
501    pub fn validator_state(&self) -> &Arc<AuthorityState> {
502        &self.state
503    }
504
505    /// Test method that performs transaction validation without going through gRPC.
506    pub fn handle_transaction_for_testing(&self, transaction: Transaction) -> SuiResult<()> {
507        let epoch_store = self.state.load_epoch_store_one_call_per_task();
508
509        // Validity check (basic structural validation)
510        transaction.validity_check(&epoch_store.tx_validity_check_context())?;
511
512        // Signature verification
513        let transaction = epoch_store
514            .verify_transaction_require_no_aliases(transaction)?
515            .into_tx();
516
517        // Validate the transaction
518        self.state
519            .handle_vote_transaction(&epoch_store, transaction)?;
520
521        Ok(())
522    }
523
524    /// Test method that performs transaction validation with overload checking.
525    /// Used for testing validator overload behavior.
526    pub fn handle_transaction_for_testing_with_overload_check(
527        &self,
528        transaction: Transaction,
529    ) -> SuiResult<()> {
530        let epoch_store = self.state.load_epoch_store_one_call_per_task();
531
532        // Validity check (basic structural validation)
533        transaction.validity_check(&epoch_store.tx_validity_check_context())?;
534
535        // Check system overload
536        self.state.check_system_overload(
537            transaction.data(),
538            self.state.check_system_overload_at_signing(),
539        )?;
540
541        // Signature verification
542        let transaction = epoch_store
543            .verify_transaction_require_no_aliases(transaction)?
544            .into_tx();
545
546        // Validate the transaction
547        self.state
548            .handle_vote_transaction(&epoch_store, transaction)?;
549
550        Ok(())
551    }
552
553    /// Collect the IDs of input objects that are immutable.
554    /// This is used to create the ImmutableInputObjects claim for consensus messages.
555    async fn collect_immutable_object_ids(
556        &self,
557        tx: &VerifiedTransaction,
558        state: &AuthorityState,
559    ) -> SuiResult<Vec<ObjectID>> {
560        let input_objects = tx.data().transaction_data().input_objects()?;
561
562        // Collect object IDs from ImmOrOwnedMoveObject inputs
563        let object_ids: Vec<ObjectID> = input_objects
564            .iter()
565            .filter_map(|obj| match obj {
566                InputObjectKind::ImmOrOwnedMoveObject((id, _, _)) => Some(*id),
567                _ => None,
568            })
569            .collect();
570        if object_ids.is_empty() {
571            return Ok(vec![]);
572        }
573
574        // Load objects from cache and filter to immutable ones
575        let objects = state.get_object_cache_reader().get_objects(&object_ids);
576
577        // All objects should be found, since owned input objects have been validated to exist.
578        objects
579            .into_iter()
580            .zip_debug_eq(object_ids.iter())
581            .filter_map(|(obj, id)| {
582                let Some(o) = obj else {
583                    return Some(Err::<ObjectID, SuiError>(
584                        SuiErrorKind::UserInputError {
585                            error: UserInputError::ObjectNotFound {
586                                object_id: *id,
587                                version: None,
588                            },
589                        }
590                        .into(),
591                    ));
592                };
593                if o.is_immutable() {
594                    Some(Ok(*id))
595                } else {
596                    None
597                }
598            })
599            .collect::<SuiResult<Vec<ObjectID>>>()
600    }
601
602    #[instrument(
603        name = "ValidatorService::handle_submit_transaction",
604        level = "error",
605        skip_all,
606        err(level = "debug")
607    )]
608    async fn handle_submit_transaction(
609        &self,
610        request: tonic::Request<RawSubmitTxRequest>,
611    ) -> WrappedServiceResponse<RawSubmitTxResponse> {
612        let Self {
613            state,
614            consensus_adapter: _,
615            metrics,
616            traffic_controller: _,
617            client_id_source,
618            gasless_limiter: _,
619            admission_queue: _,
620            recently_submitted: _,
621            recent_submission_window: _,
622            inflight_transactions: _,
623        } = self.clone();
624
625        let submitter_client_addr = if let Some(client_id_source) = &client_id_source {
626            self.get_client_ip_addr(&request, client_id_source)
627        } else {
628            self.get_client_ip_addr(&request, &ClientIdSource::SocketAddr)
629        };
630
631        let inner = request.into_inner();
632        let start_epoch = state.load_epoch_store_one_call_per_task().epoch();
633
634        let next_epoch = start_epoch + 1;
635        let mut max_retries = 1;
636
637        let mut inflight_guard = InflightTransactionsGuard::new(self);
638        loop {
639            let res = self
640                .handle_submit_transaction_inner(
641                    &state,
642                    &metrics,
643                    &inner,
644                    submitter_client_addr,
645                    &mut inflight_guard,
646                )
647                .await;
648            match res {
649                Ok((response, weight)) => return Ok((tonic::Response::new(response), weight)),
650                Err(err) => {
651                    if max_retries > 0
652                        && let SuiErrorKind::ValidatorHaltedAtEpochEnd = err.as_inner()
653                    {
654                        max_retries -= 1;
655
656                        debug!(
657                            "ValidatorHaltedAtEpochEnd. Will retry after validator reconfigures"
658                        );
659
660                        if let Ok(Ok(new_epoch)) =
661                            timeout(Duration::from_secs(15), state.wait_for_epoch(next_epoch)).await
662                        {
663                            assert_reachable!("retry submission at epoch end");
664                            if new_epoch >= next_epoch {
665                                continue;
666                            }
667                            // wait_for_epoch guarantees >= target; < would indicate a bug there.
668                            debug_fatal!(
669                                "wait_for_epoch returned early: expected >= {}, got {}",
670                                next_epoch,
671                                new_epoch
672                            );
673                        }
674                    }
675                    return Err(err.into());
676                }
677            }
678        }
679    }
680
681    async fn handle_submit_transaction_inner(
682        &self,
683        state: &AuthorityState,
684        metrics: &ValidatorServiceMetrics,
685        request: &RawSubmitTxRequest,
686        submitter_client_addr: Option<IpAddr>,
687        inflight_guard: &mut InflightTransactionsGuard,
688    ) -> SuiResult<(RawSubmitTxResponse, Weight)> {
689        let epoch_store = state.load_epoch_store_one_call_per_task();
690        let submit_type = SubmitTxType::try_from(request.submit_type).map_err(|e| {
691            SuiErrorKind::GrpcMessageDeserializeError {
692                type_info: "RawSubmitTxRequest.submit_type".to_string(),
693                error: e.to_string(),
694            }
695        })?;
696
697        let is_ping_request = submit_type == SubmitTxType::Ping;
698        if is_ping_request {
699            fp_ensure!(
700                request.transactions.is_empty(),
701                SuiErrorKind::InvalidRequest(format!(
702                    "Ping request cannot contain {} transactions",
703                    request.transactions.len()
704                ))
705                .into()
706            );
707        } else {
708            // Ensure default and soft bundle requests contain at least one transaction.
709            fp_ensure!(
710                !request.transactions.is_empty(),
711                SuiErrorKind::InvalidRequest(
712                    "At least one transaction needs to be submitted".to_string(),
713                )
714                .into()
715            );
716        }
717
718        // NOTE: for soft bundle requests, the system tries to sequence the transactions in the same order
719        // if they use the same gas price. But this is only done with best effort.
720        // Transactions in a soft bundle can be individually rejected or deferred, without affecting
721        // other transactions in the same bundle.
722        let is_soft_bundle_request = submit_type == SubmitTxType::SoftBundle;
723
724        let max_num_transactions = if is_soft_bundle_request {
725            // Soft bundle cannot contain too many transactions.
726            // Otherwise it is hard to include all of them in a single block.
727            epoch_store.protocol_config().max_soft_bundle_size()
728        } else {
729            // Still enforce a limit even when transactions do not need to be in the same block.
730            epoch_store
731                .protocol_config()
732                .max_num_transactions_in_block()
733        };
734        fp_ensure!(
735            request.transactions.len() <= max_num_transactions as usize,
736            SuiErrorKind::InvalidRequest(format!(
737                "Too many transactions in request: {} vs {}",
738                request.transactions.len(),
739                max_num_transactions
740            ))
741            .into()
742        );
743
744        // Transaction digests.
745        let mut tx_digests = Vec::with_capacity(request.transactions.len());
746        // Transactions to submit to consensus.
747        let mut consensus_transactions = Vec::with_capacity(request.transactions.len());
748        // Indexes of transactions above in the request transactions.
749        let mut transaction_indexes = Vec::with_capacity(request.transactions.len());
750        // Results corresponding to each transaction in the request.
751        let mut results: Vec<Option<SubmitTxResult>> = vec![None; request.transactions.len()];
752        // Total size of all transactions in the request.
753        let mut total_size_bytes = 0;
754        // Whether the request contains any gasless transaction.
755        let mut has_gasless = false;
756        // Set when a transaction duplicates an in-flight submission at admission. Tracked
757        // separately because it is detected after the per-tx results are finalized (those
758        // remain Submitted), so it cannot be derived from the results alone.
759        let mut duplicate_at_admission = false;
760        // First gas price seen in this soft bundle.
761        let mut expected_soft_bundle_gas_price = None;
762        // Transaction digests seen in this request attempt, used to reject repeated transactions.
763        let mut request_digests = HashSet::new();
764
765        let req_type = if is_ping_request {
766            "ping"
767        } else if request.transactions.len() == 1 {
768            "single_transaction"
769        } else if is_soft_bundle_request {
770            "soft_bundle"
771        } else {
772            "batch"
773        };
774
775        let _handle_tx_metrics_guard = metrics
776            .handle_submit_transaction_latency
777            .with_label_values(&[req_type])
778            .start_timer();
779
780        let submit_mode = self.classify_submit_mode(is_ping_request);
781
782        for (idx, tx_bytes) in request.transactions.iter().enumerate() {
783            let transaction = match bcs::from_bytes::<Transaction>(tx_bytes) {
784                Ok(txn) => txn,
785                Err(e) => {
786                    // Ok to fail the request when any transaction is invalid.
787                    return Err(SuiErrorKind::TransactionDeserializationError {
788                        error: format!("Failed to deserialize transaction at index {}: {}", idx, e),
789                    }
790                    .into());
791                }
792            };
793
794            // Ok to fail the request when any transaction is invalid.
795            let tx_size = transaction.validity_check(&epoch_store.tx_validity_check_context())?;
796            let tx_digest = *transaction.digest();
797
798            // A request must not repeat a transaction.
799            if !request_digests.insert(tx_digest) {
800                let error: SuiError = SuiErrorKind::UserInputError {
801                    error: UserInputError::RepeatedTransactions { digest: tx_digest },
802                }
803                .into();
804                // Reject individual repeated transactions in batch.
805                if is_soft_bundle_request {
806                    return Err(error);
807                }
808                results[idx] = Some(SubmitTxResult::Rejected { error });
809                continue;
810            }
811
812            // Soft bundles require all transactions to use the same gas price.
813            if is_soft_bundle_request {
814                let gas_price = transaction.data().transaction_data().gas_price();
815                if let Some(expected) = expected_soft_bundle_gas_price {
816                    fp_ensure!(
817                        gas_price == expected,
818                        SuiErrorKind::UserInputError {
819                            error: UserInputError::GasPriceMismatchError {
820                                digest: tx_digest,
821                                expected,
822                                actual: gas_price,
823                            }
824                        }
825                        .into()
826                    );
827                } else {
828                    expected_soft_bundle_gas_price = Some(gas_price);
829                }
830            }
831
832            let is_gasless = transaction
833                .data()
834                .transaction_data()
835                .is_gasless_transaction();
836
837            if is_gasless {
838                has_gasless = true;
839                metrics
840                    .gasless_submission_outcomes
841                    .with_label_values(&["attempted"])
842                    .inc();
843            }
844
845            let overload_check_res = state.check_system_overload(
846                transaction.data(),
847                state.check_system_overload_at_signing(),
848            );
849            if let Err(error) = overload_check_res {
850                metrics
851                    .num_rejected_tx_during_overload
852                    .with_label_values(&[error.as_ref()])
853                    .inc();
854                if is_gasless {
855                    metrics
856                        .gasless_submission_outcomes
857                        .with_label_values(&["rejected_overload"])
858                        .inc();
859                }
860                results[idx] = Some(SubmitTxResult::Rejected { error });
861                continue;
862            }
863
864            // Use the pre-queue per-tx consensus overload reject on the direct
865            // submission path (queue off, failover, or ping).
866            if matches!(submit_mode, AdmissionQueueSubmitMode::Direct)
867                && let Err(error) = self.consensus_adapter.check_consensus_overload()
868            {
869                state.update_overload_metrics("consensus");
870                metrics
871                    .num_rejected_tx_during_overload
872                    .with_label_values(&[error.as_ref()])
873                    .inc();
874                if is_gasless {
875                    metrics
876                        .gasless_submission_outcomes
877                        .with_label_values(&["rejected_overload"])
878                        .inc();
879                }
880                results[idx] = Some(SubmitTxResult::Rejected { error });
881                continue;
882            }
883
884            if is_gasless
885                && !self
886                    .gasless_limiter
887                    .try_acquire(epoch_store.protocol_config())
888            {
889                metrics.gasless_rate_limited_count.inc();
890                metrics
891                    .gasless_submission_outcomes
892                    .with_label_values(&["rejected_rate_limited"])
893                    .inc();
894                results[idx] = Some(SubmitTxResult::Rejected {
895                    error: SuiErrorKind::ValidatorOverloadedRetryAfter {
896                        retry_after_secs: 1,
897                    }
898                    .into(),
899                });
900                continue;
901            }
902
903            // Ok to fail the request when any signature is invalid.
904            let verified_transaction = {
905                let _metrics_guard = metrics.tx_verification_latency.start_timer();
906                if epoch_store.protocol_config().address_aliases() {
907                    match epoch_store.verify_transaction_with_current_aliases(transaction) {
908                        Ok(tx) => tx,
909                        Err(e) => {
910                            metrics.signature_errors.inc();
911                            return Err(e);
912                        }
913                    }
914                } else {
915                    match epoch_store.verify_transaction_require_no_aliases(transaction) {
916                        Ok(tx) => tx,
917                        Err(e) => {
918                            metrics.signature_errors.inc();
919                            return Err(e);
920                        }
921                    }
922                }
923            };
924
925            debug!(
926                ?tx_digest,
927                "handle_submit_transaction: verified transaction"
928            );
929
930            // Check if the transaction has executed, before checking input objects
931            // which could have been consumed.
932            if let Some(effects) = state
933                .get_transaction_cache_reader()
934                .get_executed_effects(&tx_digest)
935            {
936                let effects_digest = effects.digest();
937                if let Err(error) = state.check_effects_against_previously_signed(
938                    &epoch_store,
939                    &tx_digest,
940                    &effects_digest,
941                    "submit_transaction",
942                ) {
943                    results[idx] = Some(SubmitTxResult::Rejected { error });
944                    continue;
945                }
946                if let Ok(executed_data) = self.complete_executed_data(effects).await {
947                    let executed_result = SubmitTxResult::Executed {
948                        effects_digest,
949                        details: Some(executed_data),
950                    };
951                    results[idx] = Some(executed_result);
952                    debug!(?tx_digest, "handle_submit_transaction: already executed");
953                    continue;
954                }
955            }
956
957            if self
958                .state
959                .get_transaction_cache_reader()
960                .transaction_executed_in_last_epoch(&tx_digest, epoch_store.epoch())
961            {
962                results[idx] = Some(SubmitTxResult::Rejected {
963                    error: UserInputError::TransactionAlreadyExecuted { digest: tx_digest }.into(),
964                });
965                debug!(
966                    ?tx_digest,
967                    "handle_submit_transaction: transaction already executed in previous epoch"
968                );
969                continue;
970            }
971
972            // Suppress resubmission of transactions consensus already processed this epoch:
973            // executed transactions whose effects details could not be reconstructed above (e.g.
974            // objects pruned), sequenced-but-deferred transactions, and dropped transactions.
975            let consensus_key = SequencedConsensusTransactionKey::External(
976                ConsensusTransactionKey::Certificate(tx_digest),
977            );
978            if epoch_store.is_consensus_message_processed(&consensus_key)? {
979                // Prefer a concrete, non-retriable error over the generic, retriable
980                // TransactionProcessing suppression. A processed-but-unexecuted digest is
981                // commonly a dropped owned-object conflict loser; surfacing the terminal
982                // error lets the client stop retrying instead of polling for effects that
983                // will never come.
984                //
985                // First check the epoch owned-object lock table with the same conflict
986                // logic the consensus handler uses post-consensus. Locks are never
987                // released within an epoch, so this reports the conflict even before the
988                // winner executes, while the loser's input versions still validate as
989                // live.
990                if let Ok(input_objects) = verified_transaction
991                    .tx()
992                    .data()
993                    .transaction_data()
994                    .input_objects()
995                {
996                    let immutable_object_ids = self
997                        .collect_immutable_object_ids(verified_transaction.tx(), state)
998                        .await?;
999                    let owned_object_refs: Vec<_> = input_objects
1000                        .iter()
1001                        .filter_map(|obj| match obj {
1002                            InputObjectKind::ImmOrOwnedMoveObject(obj_ref)
1003                                if !immutable_object_ids.contains(&obj_ref.0) =>
1004                            {
1005                                Some(*obj_ref)
1006                            }
1007                            _ => None,
1008                        })
1009                        .collect();
1010                    let existing_locks =
1011                        epoch_store.get_owned_object_locks_map(&owned_object_refs)?;
1012                    if let Err(error) = epoch_store.try_acquire_owned_object_locks_post_consensus(
1013                        &owned_object_refs,
1014                        tx_digest,
1015                        &HashMap::new(),
1016                        &existing_locks,
1017                    ) {
1018                        debug!(
1019                            ?tx_digest,
1020                            "handle_submit_transaction: processed transaction rejected on lock conflict: {error}"
1021                        );
1022                        metrics
1023                            .submission_rejected_transactions
1024                            .with_label_values(&[error.to_variant_name()])
1025                            .inc();
1026                        results[idx] = Some(SubmitTxResult::Rejected { error });
1027                        continue;
1028                    }
1029                }
1030                // Then revalidate against live state, which surfaces the terminal
1031                // stale-version error once the conflict winner has executed.
1032                if let Err(error) =
1033                    state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone())
1034                {
1035                    // The transaction may have executed while being validated (e.g. it was
1036                    // deferred rather than dropped).
1037                    if let Some(effects) = state
1038                        .get_transaction_cache_reader()
1039                        .get_executed_effects(&tx_digest)
1040                    {
1041                        let effects_digest = effects.digest();
1042                        if let Err(error) = state.check_effects_against_previously_signed(
1043                            &epoch_store,
1044                            &tx_digest,
1045                            &effects_digest,
1046                            "submit_transaction",
1047                        ) {
1048                            results[idx] = Some(SubmitTxResult::Rejected { error });
1049                            continue;
1050                        }
1051                        if let Ok(executed_data) = self.complete_executed_data(effects).await {
1052                            results[idx] = Some(SubmitTxResult::Executed {
1053                                effects_digest,
1054                                details: Some(executed_data),
1055                            });
1056                            continue;
1057                        }
1058                    }
1059                    debug!(
1060                        ?tx_digest,
1061                        "handle_submit_transaction: processed transaction rejected on revalidation: {error}"
1062                    );
1063                    metrics
1064                        .submission_rejected_transactions
1065                        .with_label_values(&[error.to_variant_name()])
1066                        .inc();
1067                    results[idx] = Some(SubmitTxResult::Rejected { error });
1068                    continue;
1069                }
1070                // Validation passed, so this processed digest may still be executable.
1071                // Return retriable TransactionProcessing rather than resubmitting it to consensus.
1072                // A later client retry can observe effects or a concrete terminal validation error.
1073                metrics
1074                    .submission_suppressed_already_processed
1075                    .with_label_values(&[req_type])
1076                    .inc();
1077                results[idx] = Some(SubmitTxResult::Rejected {
1078                    error: SuiErrorKind::TransactionProcessing {
1079                        digest: tx_digest,
1080                        status: "consensus message processed".to_string(),
1081                    }
1082                    .into(),
1083                });
1084                debug!(
1085                    ?tx_digest,
1086                    "handle_submit_transaction: consensus message already processed"
1087                );
1088                continue;
1089            }
1090
1091            // Atomically acquire the digest for the duration of this handler. Reject concurrent
1092            // and recent duplicates and record the result per result index.
1093            match inflight_guard.try_acquire(tx_digest) {
1094                AcquireOutcome::Acquired | AcquireOutcome::AlreadyAcquiredByThisRequest => {
1095                    // Continue to process the transaction and submit to consensus.
1096                }
1097                AcquireOutcome::AlreadyAcquiredByAnotherRequest => {
1098                    metrics
1099                        .submission_suppressed_inflight
1100                        .with_label_values(&[req_type])
1101                        .inc();
1102                    results[idx] = Some(SubmitTxResult::Rejected {
1103                        error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1104                    });
1105                    debug!(
1106                        ?tx_digest,
1107                        "handle_submit_transaction: concurrent submission in progress"
1108                    );
1109                    continue;
1110                }
1111                AcquireOutcome::RecentlyProcessed { since } => {
1112                    metrics
1113                        .submission_suppressed_recently_submitted
1114                        .with_label_values(&[req_type])
1115                        .inc();
1116                    metrics
1117                        .recently_submitted_resubmission_interval
1118                        .observe(since.as_secs_f64());
1119                    results[idx] = Some(SubmitTxResult::Rejected {
1120                        error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1121                    });
1122                    debug!(?tx_digest, "handle_submit_transaction: recently processed");
1123                    continue;
1124                }
1125            }
1126
1127            debug!(
1128                ?tx_digest,
1129                "handle_submit_transaction: waiting for fastpath dependency objects"
1130            );
1131            if !state
1132                .wait_for_fastpath_dependency_objects(
1133                    verified_transaction.tx(),
1134                    epoch_store.epoch(),
1135                )
1136                .await?
1137            {
1138                debug!(
1139                    ?tx_digest,
1140                    "fastpath input objects are still unavailable after waiting"
1141                );
1142            }
1143
1144            match state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone()) {
1145                Ok(_) => { /* continue processing */ }
1146                Err(e) => {
1147                    // Check if transaction has been executed while being validated.
1148                    // This is an edge case so checking executed effects twice is acceptable.
1149                    if let Some(effects) = state
1150                        .get_transaction_cache_reader()
1151                        .get_executed_effects(&tx_digest)
1152                    {
1153                        let effects_digest = effects.digest();
1154                        if let Err(error) = state.check_effects_against_previously_signed(
1155                            &epoch_store,
1156                            &tx_digest,
1157                            &effects_digest,
1158                            "submit_transaction",
1159                        ) {
1160                            results[idx] = Some(SubmitTxResult::Rejected { error });
1161                            continue;
1162                        }
1163                        if let Ok(executed_data) = self.complete_executed_data(effects).await {
1164                            let executed_result = SubmitTxResult::Executed {
1165                                effects_digest,
1166                                details: Some(executed_data),
1167                            };
1168                            results[idx] = Some(executed_result);
1169                            continue;
1170                        }
1171                    }
1172
1173                    // When the transaction has not been executed, record the error for the transaction.
1174                    debug!(?tx_digest, "Transaction rejected during submission: {e}");
1175                    metrics
1176                        .submission_rejected_transactions
1177                        .with_label_values(&[e.to_variant_name()])
1178                        .inc();
1179                    results[idx] = Some(SubmitTxResult::Rejected { error: e });
1180                    continue;
1181                }
1182            }
1183
1184            // Create claims with aliases and / or immutable objects.
1185            let mut claims = vec![];
1186
1187            let immutable_object_ids = self
1188                .collect_immutable_object_ids(verified_transaction.tx(), state)
1189                .await?;
1190            if !immutable_object_ids.is_empty() {
1191                claims.push(TransactionClaim::ImmutableInputObjects(
1192                    immutable_object_ids,
1193                ));
1194            }
1195
1196            let (tx, aliases) = verified_transaction.into_inner();
1197            if epoch_store.protocol_config().address_aliases() {
1198                if epoch_store
1199                    .protocol_config()
1200                    .fix_checkpoint_signature_mapping()
1201                {
1202                    claims.push(TransactionClaim::AddressAliasesV2(aliases));
1203                } else {
1204                    let v1_aliases: Vec<_> = tx
1205                        .data()
1206                        .intent_message()
1207                        .value
1208                        .required_signers()
1209                        .into_iter()
1210                        .zip_eq(aliases.into_iter().map(|(_, seq)| seq))
1211                        .collect();
1212                    #[allow(deprecated)]
1213                    claims.push(TransactionClaim::AddressAliases(
1214                        nonempty::NonEmpty::from_vec(v1_aliases)
1215                            .expect("must have at least one required_signer"),
1216                    ));
1217                }
1218            }
1219
1220            let tx_with_claims = TransactionWithClaims::new(tx.into(), claims);
1221
1222            consensus_transactions.push(ConsensusTransaction::new_user_transaction_v2_message(
1223                &state.name,
1224                tx_with_claims,
1225            ));
1226            if is_gasless {
1227                metrics
1228                    .gasless_submission_outcomes
1229                    .with_label_values(&["submitted"])
1230                    .inc();
1231            }
1232
1233            transaction_indexes.push(idx);
1234            tx_digests.push(tx_digest);
1235            total_size_bytes += tx_size;
1236        }
1237
1238        if consensus_transactions.is_empty() && !is_ping_request {
1239            let spam_weight = Self::request_spam_weight(
1240                &results,
1241                has_gasless,
1242                duplicate_at_admission,
1243                is_ping_request,
1244            );
1245            let response = Self::try_from_submit_tx_response(results)?;
1246            return Ok((response, spam_weight));
1247        }
1248
1249        // Set the max bytes size of the soft bundle to be half of the consensus max transactions in block size.
1250        // We do this to account for serialization overheads and to ensure that the soft bundle is not too large
1251        // when is attempted to be posted via consensus.
1252        let max_transaction_bytes = if is_soft_bundle_request {
1253            epoch_store
1254                .protocol_config()
1255                .consensus_max_transactions_in_block_bytes()
1256                / 2
1257        } else {
1258            epoch_store
1259                .protocol_config()
1260                .consensus_max_transactions_in_block_bytes()
1261        };
1262        fp_ensure!(
1263            total_size_bytes <= max_transaction_bytes as usize,
1264            SuiErrorKind::UserInputError {
1265                error: UserInputError::TotalTransactionSizeTooLargeInBatch {
1266                    size: total_size_bytes,
1267                    limit: max_transaction_bytes,
1268                },
1269            }
1270            .into()
1271        );
1272
1273        metrics
1274            .handle_submit_transaction_bytes
1275            .with_label_values(&[req_type])
1276            .observe(total_size_bytes as f64);
1277        metrics
1278            .handle_submit_transaction_batch_size
1279            .with_label_values(&[req_type])
1280            .observe(consensus_transactions.len() as f64);
1281
1282        let _latency_metric_guard = metrics
1283            .handle_submit_transaction_consensus_latency
1284            .with_label_values(&[req_type])
1285            .start_timer();
1286
1287        if is_soft_bundle_request {
1288            // We only allow the `consensus_transactions` to be empty for ping requests. This is how it should and is be treated from the downstream components.
1289            // For any other case, having an empty `consensus_transactions` vector is an invalid state and we should have never reached at this point.
1290            assert!(
1291                !consensus_transactions.is_empty(),
1292                "A valid soft bundle must have at least one transaction"
1293            );
1294        }
1295
1296        // Soft bundles are inserted as a single queue entry.
1297        // Individual transactions are each inserted separately.
1298        let tx_groups: Vec<Vec<ConsensusTransaction>> = if is_soft_bundle_request || is_ping_request
1299        {
1300            vec![consensus_transactions]
1301        } else {
1302            consensus_transactions
1303                .into_iter()
1304                .map(|t| vec![t])
1305                .collect()
1306        };
1307
1308        // Map each submission group back to the (result index, digest) of the transactions it
1309        // contains, so a per-group outcome — consensus positions, or an "already processing"
1310        // error — can be recorded against each individual transaction. Soft bundles submit as a
1311        // single group; individual transactions submit one group each.
1312        let group_tx_meta = if is_soft_bundle_request {
1313            vec![
1314                transaction_indexes
1315                    .into_iter()
1316                    .zip_eq(tx_digests)
1317                    .collect::<Vec<_>>(),
1318            ]
1319        } else {
1320            transaction_indexes
1321                .into_iter()
1322                .zip_eq(tx_digests)
1323                .map(|pair| vec![pair])
1324                .collect::<Vec<_>>()
1325        };
1326
1327        // Collect one result per submission group WITHOUT short-circuiting. An
1328        // already-processing transaction is reported per-tx as a retriable below;
1329        // any other error fails the whole request, after all groups have settled.
1330        // Soft bundles submit as a single group; individual transactions submit one group each.
1331        let group_results = match submit_mode {
1332            AdmissionQueueSubmitMode::Direct => {
1333                let futures = tx_groups.into_iter().map(|txns| {
1334                    debug!(
1335                        "handle_submit_transaction: submitting consensus transactions ({}): {}",
1336                        req_type,
1337                        txns.iter().map(|t| t.local_display()).join(", ")
1338                    );
1339                    self.consensus_adapter.submit_and_get_positions(
1340                        txns,
1341                        &epoch_store,
1342                        submitter_client_addr,
1343                    )
1344                });
1345                future::join_all(futures).await
1346            }
1347            AdmissionQueueSubmitMode::Queue => {
1348                let aq = self
1349                    .admission_queue
1350                    .as_ref()
1351                    .expect("Queue mode implies admission_queue is Some")
1352                    .load();
1353                let mut receivers = Vec::with_capacity(tx_groups.len());
1354                for txns in tx_groups {
1355                    let gas_price = Self::extract_gas_price(&txns);
1356                    let (rx, newly_inserted) = aq
1357                        .try_insert(gas_price, txns, submitter_client_addr)
1358                        .await?;
1359                    if !newly_inserted {
1360                        // Duplicate of an in-flight submission; flag the request as spam. The
1361                        // per-tx result is still Submitted, so this is tracked separately.
1362                        duplicate_at_admission = true;
1363                    }
1364                    receivers.push(rx);
1365                }
1366                future::join_all(receivers.into_iter().map(|rx| async move {
1367                    match rx.await {
1368                        Ok(result) => result.map_err(SuiError::from),
1369                        Err(_) => Err(SuiError::from(
1370                            SuiErrorKind::TooManyTransactionsPendingConsensus,
1371                        )),
1372                    }
1373                }))
1374                .await
1375            }
1376        };
1377
1378        if is_ping_request {
1379            // For ping requests there is a single group returning the special consensus position.
1380            let consensus_positions = group_results
1381                .into_iter()
1382                .next()
1383                .expect("Ping request must have exactly one submission group")?;
1384            assert_eq!(consensus_positions.len(), 1);
1385            results.push(Some(SubmitTxResult::Submitted {
1386                consensus_position: consensus_positions[0],
1387            }));
1388        } else {
1389            for (group_result, txns_meta) in group_results.into_iter().zip_debug_eq(group_tx_meta) {
1390                match group_result {
1391                    Ok(consensus_positions) => {
1392                        for ((idx, tx_digest), consensus_position) in
1393                            txns_meta.into_iter().zip_debug_eq(consensus_positions)
1394                        {
1395                            debug!(
1396                                ?tx_digest,
1397                                "handle_submit_transaction: submitted consensus transaction at {}",
1398                                consensus_position,
1399                            );
1400                            results[idx] = Some(SubmitTxResult::Submitted { consensus_position });
1401                        }
1402                    }
1403                    // The transaction(s) in this group are already being processed by consensus.
1404                    // Report per-tx as a retriable rejection rather than failing the whole request.
1405                    Err(err) => {
1406                        let SuiErrorKind::TransactionProcessing { status, .. } =
1407                            err.as_inner().clone()
1408                        else {
1409                            return Err(err);
1410                        };
1411                        // For TransactionProcessing error, ensure the per txn result has the correct digest.
1412                        for (idx, tx_digest) in txns_meta {
1413                            debug!(
1414                                ?tx_digest,
1415                                "handle_submit_transaction: transaction already processing: {err}"
1416                            );
1417                            // Same suppression the upfront `is_consensus_message_processed` check
1418                            // records, just detected during submission instead of before it. The
1419                            // two paths are mutually exclusive, so this does not double-count.
1420                            metrics
1421                                .submission_suppressed_already_processed
1422                                .with_label_values(&[req_type])
1423                                .inc();
1424                            results[idx] = Some(SubmitTxResult::Rejected {
1425                                error: SuiErrorKind::TransactionProcessing {
1426                                    digest: tx_digest,
1427                                    status: status.clone(),
1428                                }
1429                                .into(),
1430                            });
1431                        }
1432                    }
1433                }
1434            }
1435        }
1436
1437        let spam_weight = Self::request_spam_weight(
1438            &results,
1439            has_gasless,
1440            duplicate_at_admission,
1441            is_ping_request,
1442        );
1443        let response = Self::try_from_submit_tx_response(results)?;
1444        Ok((response, spam_weight))
1445    }
1446
1447    /// Traffic-control spam weight for a whole submit request. The request is spam unless it is
1448    /// entirely accepted gas-chargable work.
1449    fn request_spam_weight(
1450        results: &[Option<SubmitTxResult>],
1451        has_gasless: bool,
1452        duplicate_at_admission: bool,
1453        is_ping: bool,
1454    ) -> Weight {
1455        if is_ping || has_gasless || duplicate_at_admission {
1456            return Weight::one();
1457        }
1458        for result in results {
1459            let Some(result) = result else {
1460                // `results` is expected to be fully populated (every entry `Some`) for the
1461                // request's transactions; a missing entry is a bug and is conservatively
1462                // treated as spam.
1463                debug_fatal!("transaction outcome unset when computing spam weight");
1464                return Weight::one();
1465            };
1466            if Self::submission_spam_weight(result) == Weight::one() {
1467                return Weight::one();
1468            }
1469        }
1470        Weight::zero()
1471    }
1472
1473    fn submission_spam_weight(result: &SubmitTxResult) -> Weight {
1474        match result {
1475            SubmitTxResult::Submitted { .. } => Weight::zero(),
1476            // Non-submitted results can't be charged.
1477            SubmitTxResult::Executed { .. } | SubmitTxResult::Rejected { .. } => Weight::one(),
1478        }
1479    }
1480
1481    fn try_from_submit_tx_response(
1482        results: Vec<Option<SubmitTxResult>>,
1483    ) -> Result<RawSubmitTxResponse, SuiError> {
1484        let mut raw_results = Vec::new();
1485        for (i, result) in results.into_iter().enumerate() {
1486            let result = result.ok_or_else(|| SuiErrorKind::GenericAuthorityError {
1487                error: format!("Missing transaction result at {}", i),
1488            })?;
1489            let raw_result = result.try_into()?;
1490            raw_results.push(raw_result);
1491        }
1492        Ok(RawSubmitTxResponse {
1493            results: raw_results,
1494        })
1495    }
1496
1497    /// Extract the gas price from a batch of consensus transactions.
1498    /// Returns the minimum gas price in the batch, or 0 if no user transactions.
1499    fn extract_gas_price(transactions: &[ConsensusTransaction]) -> u64 {
1500        use sui_types::messages_consensus::ConsensusTransactionKind;
1501        transactions
1502            .iter()
1503            .filter_map(|tx| match &tx.kind {
1504                ConsensusTransactionKind::CertifiedTransaction(cert) => Some(cert.gas_price()),
1505                ConsensusTransactionKind::UserTransaction(t) => {
1506                    Some(t.data().transaction_data().gas_price())
1507                }
1508                ConsensusTransactionKind::UserTransactionV2(t) => {
1509                    Some(t.tx().data().transaction_data().gas_price())
1510                }
1511                _ => None,
1512            })
1513            .min()
1514            .unwrap_or(0)
1515    }
1516
1517    fn classify_submit_mode(&self, is_ping_request: bool) -> AdmissionQueueSubmitMode {
1518        let Some(aq) = &self.admission_queue else {
1519            return AdmissionQueueSubmitMode::Direct;
1520        };
1521
1522        // Ping requests carry no transactions and must not wait behind queued
1523        // work; submit them directly to consensus.
1524        if is_ping_request {
1525            return AdmissionQueueSubmitMode::Direct;
1526        }
1527
1528        // If the queue actor is stuck, fall back to direct submission with the
1529        // pre-queue saturation reject until it resumes making progress.
1530        if aq.load().failover_tripped() {
1531            return AdmissionQueueSubmitMode::Direct;
1532        }
1533
1534        AdmissionQueueSubmitMode::Queue
1535    }
1536
1537    async fn collect_effects_data(
1538        &self,
1539        effects: &TransactionEffects,
1540        include_events: bool,
1541        include_input_objects: bool,
1542        include_output_objects: bool,
1543    ) -> SuiResult<(Option<TransactionEvents>, Vec<Object>, Vec<Object>)> {
1544        let events = if include_events && effects.events_digest().is_some() {
1545            Some(
1546                self.state
1547                    .get_transaction_events(effects.transaction_digest())?,
1548            )
1549        } else {
1550            None
1551        };
1552
1553        let input_objects = if include_input_objects {
1554            self.state.get_transaction_input_objects(effects)?
1555        } else {
1556            vec![]
1557        };
1558
1559        let output_objects = if include_output_objects {
1560            self.state.get_transaction_output_objects(effects)?
1561        } else {
1562            vec![]
1563        };
1564
1565        Ok((events, input_objects, output_objects))
1566    }
1567}
1568
1569type WrappedServiceResponse<T> = Result<(tonic::Response<T>, Weight), tonic::Status>;
1570
1571/// RAII guard tracking the transaction digests a single submit request is actively handling, so
1572/// concurrent duplicates can be rejected. On drop, each acquired digest is removed from
1573/// the in-flight set and demoted into `recently_submitted` cache, so resubmissions
1574/// arriving shortly after the handler returns are still suppressed.
1575struct InflightTransactionsGuard {
1576    // Handle to inflight map and recently submitted cache.
1577    inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
1578    recently_submitted: Cache<TransactionDigest, Instant>,
1579    window: Duration,
1580    metrics: Arc<ValidatorServiceMetrics>,
1581    /// Digests this request successfully acquired.
1582    acquired: HashSet<TransactionDigest>,
1583}
1584
1585enum AcquireOutcome {
1586    /// Transaction digest newly acquired by this request.
1587    Acquired,
1588    /// Transaction digest already acquired by this request before this internal retry attempt.
1589    AlreadyAcquiredByThisRequest,
1590    /// Transaction digest being handled by another concurrent request — reject this index.
1591    AlreadyAcquiredByAnotherRequest,
1592    /// Transaction digest recently processed — reject this index.
1593    RecentlyProcessed { since: Duration },
1594}
1595
1596impl InflightTransactionsGuard {
1597    fn new(service: &ValidatorService) -> Self {
1598        Self {
1599            inflight: service.inflight_transactions.clone(),
1600            recently_submitted: service.recently_submitted.clone(),
1601            window: service.recent_submission_window,
1602            metrics: service.metrics.clone(),
1603            acquired: HashSet::new(),
1604        }
1605    }
1606
1607    fn try_acquire(&mut self, digest: TransactionDigest) -> AcquireOutcome {
1608        // A retry of this own request re-acquires the same digests.
1609        if self.acquired.contains(&digest) {
1610            return AcquireOutcome::AlreadyAcquiredByThisRequest;
1611        }
1612
1613        // Suppress resubmissions of recently processed transactions.
1614        if let Some(outcome) = self.recently_processed_outcome(digest) {
1615            return outcome;
1616        }
1617
1618        // Atomic check-and-acquire against concurrent in-flight transactions.
1619        {
1620            let mut set = self.inflight.lock();
1621            // Only continue processing the transaction if it is not already inflight.
1622            if !set.insert(digest) {
1623                return AcquireOutcome::AlreadyAcquiredByAnotherRequest;
1624            }
1625            self.metrics.inflight_transactions.set(set.len() as i64);
1626        }
1627
1628        // Without this re-check, a duplicated transaction arriving between the first check and
1629        // the digest acquisition could slip through.
1630        if let Some(outcome) = self.recently_processed_outcome(digest) {
1631            let mut set = self.inflight.lock();
1632            set.remove(&digest);
1633            self.metrics.inflight_transactions.set(set.len() as i64);
1634            return outcome;
1635        }
1636
1637        self.acquired.insert(digest);
1638        AcquireOutcome::Acquired
1639    }
1640
1641    fn recently_processed_outcome(&self, digest: TransactionDigest) -> Option<AcquireOutcome> {
1642        let recorded_at = self.recently_submitted.get(&digest)?;
1643        let since = recorded_at.elapsed();
1644        (since < self.window).then_some(AcquireOutcome::RecentlyProcessed { since })
1645    }
1646}
1647
1648impl Drop for InflightTransactionsGuard {
1649    fn drop(&mut self) {
1650        if self.acquired.is_empty() {
1651            return;
1652        }
1653        // Demote inflight transactions to recently submitted cache before taking the in-flight lock,
1654        // to avoid cleaning up the cache with the lock.
1655        let now = Instant::now();
1656        for digest in &self.acquired {
1657            self.recently_submitted.insert(*digest, now);
1658        }
1659        {
1660            let mut set = self.inflight.lock();
1661            for digest in &self.acquired {
1662                set.remove(digest);
1663            }
1664            self.metrics.inflight_transactions.set(set.len() as i64);
1665        }
1666        self.metrics
1667            .recently_submitted_cache_size
1668            .set(self.recently_submitted.entry_count() as i64);
1669    }
1670}
1671
1672impl ValidatorService {
1673    async fn handle_submit_transaction_impl(
1674        &self,
1675        request: tonic::Request<RawSubmitTxRequest>,
1676    ) -> WrappedServiceResponse<RawSubmitTxResponse> {
1677        self.handle_submit_transaction(request).await
1678    }
1679
1680    async fn wait_for_effects_impl(
1681        &self,
1682        request: tonic::Request<RawWaitForEffectsRequest>,
1683    ) -> WrappedServiceResponse<RawWaitForEffectsResponse> {
1684        let request: WaitForEffectsRequest = request.into_inner().try_into()?;
1685        let epoch_store = self.state.load_epoch_store_one_call_per_task();
1686        let response = timeout(
1687            // TODO(fastpath): Tune this once we have a good estimate of the typical delay.
1688            Duration::from_secs(20),
1689            epoch_store
1690                .within_alive_epoch(self.wait_for_effects_response(request, &epoch_store))
1691                .map_err(|_| SuiErrorKind::EpochEnded(epoch_store.epoch())),
1692        )
1693        .await
1694        .map_err(|_| tonic::Status::internal("Timeout waiting for effects"))???
1695        .try_into()?;
1696        Ok((tonic::Response::new(response), Weight::zero()))
1697    }
1698
1699    #[instrument(name= "ValidatorService::wait_for_effects_response", level = "debug", skip_all, fields(consensus_position = ?request.consensus_position))]
1700    async fn wait_for_effects_response(
1701        &self,
1702        request: WaitForEffectsRequest,
1703        epoch_store: &Arc<AuthorityPerEpochStore>,
1704    ) -> SuiResult<WaitForEffectsResponse> {
1705        if request.ping_type.is_some() {
1706            return timeout(
1707                Duration::from_secs(10),
1708                self.ping_response(request, epoch_store),
1709            )
1710            .await
1711            .map_err(|_| SuiErrorKind::TimeoutError)?;
1712        }
1713
1714        let Some(tx_digest) = request.transaction_digest else {
1715            return Err(SuiErrorKind::InvalidRequest(
1716                "Transaction digest is required for wait for effects requests".to_string(),
1717            )
1718            .into());
1719        };
1720        let tx_digests = [tx_digest];
1721
1722        // When consensus_position is provided, also watch the consensus status cache
1723        // so rejected/dropped transactions get a timely response instead of waiting
1724        // forever for effects that will never be produced.
1725        let consensus_status_future = async {
1726            let consensus_position = match request.consensus_position {
1727                Some(pos) => pos,
1728                None => return futures::future::pending().await,
1729            };
1730            let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1731            consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1732            match consensus_tx_status_cache
1733                .notify_read_transaction_status(consensus_position)
1734                .await
1735            {
1736                NotifyReadConsensusTxStatusResult::Status(
1737                    ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped,
1738                ) => Ok(WaitForEffectsResponse::Rejected {
1739                    error: epoch_store.get_rejection_vote_reason(consensus_position),
1740                }),
1741                NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized) => {
1742                    // Effects will be produced — yield to let the effects future win.
1743                    futures::future::pending().await
1744                }
1745                NotifyReadConsensusTxStatusResult::Expired(round) => {
1746                    Ok(WaitForEffectsResponse::Expired {
1747                        epoch: epoch_store.epoch(),
1748                        round: Some(round),
1749                    })
1750                }
1751            }
1752        };
1753
1754        tokio::select! {
1755            effects_result = self.state
1756                .get_transaction_cache_reader()
1757                .notify_read_executed_effects_may_fail(
1758                    "AuthorityServer::wait_for_effects::notify_read_executed_effects_finalized",
1759                    &tx_digests,
1760                ) => {
1761                let effects = effects_result?.pop().unwrap();
1762                let effects_digest = effects.digest();
1763                self.state.check_effects_against_previously_signed(
1764                    epoch_store,
1765                    &tx_digest,
1766                    &effects_digest,
1767                    "wait_for_effects",
1768                )?;
1769                let details = if request.include_details {
1770                    Some(self.complete_executed_data(effects).await?)
1771                } else {
1772                    None
1773                };
1774                Ok(WaitForEffectsResponse::Executed {
1775                    effects_digest,
1776                    details,
1777                })
1778            }
1779            status_response = consensus_status_future => {
1780                status_response
1781            }
1782        }
1783    }
1784
1785    #[instrument(level = "error", skip_all, err(level = "debug"))]
1786    async fn ping_response(
1787        &self,
1788        request: WaitForEffectsRequest,
1789        epoch_store: &Arc<AuthorityPerEpochStore>,
1790    ) -> SuiResult<WaitForEffectsResponse> {
1791        let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1792
1793        let Some(consensus_position) = request.consensus_position else {
1794            return Err(SuiErrorKind::InvalidRequest(
1795                "Consensus position is required for Ping requests".to_string(),
1796            )
1797            .into());
1798        };
1799
1800        // We assume that the caller has already checked for the existence of the `ping` field, but handling it gracefully here.
1801        let Some(ping) = request.ping_type else {
1802            return Err(SuiErrorKind::InvalidRequest(
1803                "Ping type is required for ping requests".to_string(),
1804            )
1805            .into());
1806        };
1807
1808        let _metrics_guard = self
1809            .metrics
1810            .handle_wait_for_effects_ping_latency
1811            .with_label_values(&[ping.as_str()])
1812            .start_timer();
1813
1814        consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1815
1816        let details = if request.include_details {
1817            Some(Box::new(ExecutedData::default()))
1818        } else {
1819            None
1820        };
1821
1822        let status = consensus_tx_status_cache
1823            .notify_read_transaction_status(consensus_position)
1824            .await;
1825        match status {
1826            NotifyReadConsensusTxStatusResult::Status(status) => match status {
1827                ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped => {
1828                    Ok(WaitForEffectsResponse::Rejected {
1829                        error: epoch_store.get_rejection_vote_reason(consensus_position),
1830                    })
1831                }
1832                ConsensusTxStatus::Finalized => Ok(WaitForEffectsResponse::Executed {
1833                    effects_digest: TransactionEffectsDigest::ZERO,
1834                    details,
1835                }),
1836            },
1837            NotifyReadConsensusTxStatusResult::Expired(round) => {
1838                Ok(WaitForEffectsResponse::Expired {
1839                    epoch: epoch_store.epoch(),
1840                    round: Some(round),
1841                })
1842            }
1843        }
1844    }
1845
1846    async fn complete_executed_data(
1847        &self,
1848        effects: TransactionEffects,
1849    ) -> SuiResult<Box<ExecutedData>> {
1850        let (events, input_objects, output_objects) = self
1851            .collect_effects_data(
1852                &effects, /* include_events */ true, /* include_input_objects */ true,
1853                /* include_output_objects */ true,
1854            )
1855            .await?;
1856        Ok(Box::new(ExecutedData {
1857            effects,
1858            events,
1859            input_objects,
1860            output_objects,
1861        }))
1862    }
1863
1864    async fn object_info_impl(
1865        &self,
1866        request: tonic::Request<ObjectInfoRequest>,
1867    ) -> WrappedServiceResponse<ObjectInfoResponse> {
1868        let request = request.into_inner();
1869        let response = self.state.handle_object_info_request(request).await?;
1870        Ok((tonic::Response::new(response), Weight::one()))
1871    }
1872
1873    async fn transaction_info_impl(
1874        &self,
1875        request: tonic::Request<TransactionInfoRequest>,
1876    ) -> WrappedServiceResponse<TransactionInfoResponse> {
1877        let request = request.into_inner();
1878        let response = self.state.handle_transaction_info_request(request).await?;
1879        Ok((tonic::Response::new(response), Weight::one()))
1880    }
1881
1882    async fn checkpoint_impl(
1883        &self,
1884        request: tonic::Request<CheckpointRequest>,
1885    ) -> WrappedServiceResponse<CheckpointResponse> {
1886        let request = request.into_inner();
1887        let response = self.state.handle_checkpoint_request(&request)?;
1888        Ok((tonic::Response::new(response), Weight::one()))
1889    }
1890
1891    async fn checkpoint_v2_impl(
1892        &self,
1893        request: tonic::Request<CheckpointRequestV2>,
1894    ) -> WrappedServiceResponse<CheckpointResponseV2> {
1895        let request = request.into_inner();
1896        let response = self.state.handle_checkpoint_request_v2(&request)?;
1897        Ok((tonic::Response::new(response), Weight::one()))
1898    }
1899
1900    async fn get_system_state_object_impl(
1901        &self,
1902        _request: tonic::Request<SystemStateRequest>,
1903    ) -> WrappedServiceResponse<SuiSystemState> {
1904        let response = self
1905            .state
1906            .get_object_cache_reader()
1907            .get_sui_system_state_object_unsafe()?;
1908        Ok((tonic::Response::new(response), Weight::one()))
1909    }
1910
1911    async fn validator_health_impl(
1912        &self,
1913        _request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
1914    ) -> WrappedServiceResponse<sui_types::messages_grpc::RawValidatorHealthResponse> {
1915        let state = &self.state;
1916
1917        // Get epoch store once for both metrics
1918        let epoch_store = state.load_epoch_store_one_call_per_task();
1919
1920        // Get in-flight execution transactions from execution scheduler
1921        let num_inflight_execution_transactions =
1922            state.execution_scheduler().num_pending_certificates() as u64;
1923
1924        // Get in-flight consensus transactions from consensus adapter
1925        let num_inflight_consensus_transactions =
1926            self.consensus_adapter.num_inflight_transactions();
1927
1928        // Get last committed leader round from epoch store
1929        let last_committed_leader_round = epoch_store
1930            .consensus_tx_status_cache
1931            .get_last_committed_leader_round()
1932            .unwrap_or(0);
1933
1934        // Get last locally built checkpoint sequence
1935        let last_locally_built_checkpoint = epoch_store
1936            .last_built_checkpoint_summary()
1937            .ok()
1938            .flatten()
1939            .map(|(_, summary)| summary.sequence_number)
1940            .unwrap_or(0);
1941
1942        let typed_response = sui_types::messages_grpc::ValidatorHealthResponse {
1943            num_inflight_consensus_transactions,
1944            num_inflight_execution_transactions,
1945            last_locally_built_checkpoint,
1946            last_committed_leader_round,
1947        };
1948
1949        let raw_response = typed_response
1950            .try_into()
1951            .map_err(|e: sui_types::error::SuiError| {
1952                tonic::Status::internal(format!("Failed to serialize health response: {}", e))
1953            })?;
1954
1955        Ok((tonic::Response::new(raw_response), Weight::one()))
1956    }
1957
1958    fn get_client_ip_addr<T>(
1959        &self,
1960        request: &tonic::Request<T>,
1961        source: &ClientIdSource,
1962    ) -> Option<IpAddr> {
1963        let forwarded_header = request.metadata().get_all("x-forwarded-for").iter().next();
1964
1965        if let Some(header) = forwarded_header {
1966            let num_hops = header
1967                .to_str()
1968                .map(|h| h.split(',').count().saturating_sub(1))
1969                .unwrap_or(0);
1970
1971            self.metrics.x_forwarded_for_num_hops.set(num_hops as f64);
1972        }
1973
1974        match source {
1975            ClientIdSource::SocketAddr => {
1976                let socket_addr: Option<SocketAddr> = request.remote_addr();
1977
1978                // We will hit this case if the IO type used does not
1979                // implement Connected or when using a unix domain socket.
1980                // TODO: once we have confirmed that no legitimate traffic
1981                // is hitting this case, we should reject such requests that
1982                // hit this case.
1983                if let Some(socket_addr) = socket_addr {
1984                    Some(socket_addr.ip())
1985                } else {
1986                    if cfg!(msim) {
1987                        // Ignore the error from simtests.
1988                    } else if cfg!(test) {
1989                        panic!("Failed to get remote address from request");
1990                    } else {
1991                        self.metrics.connection_ip_not_found.inc();
1992                        error!("Failed to get remote address from request");
1993                    }
1994                    None
1995                }
1996            }
1997            ClientIdSource::XForwardedFor(num_hops) => {
1998                let do_header_parse = |op: &MetadataValue<Ascii>| {
1999                    match op.to_str() {
2000                        Ok(header_val) => {
2001                            let header_contents =
2002                                header_val.split(',').map(str::trim).collect::<Vec<_>>();
2003                            if *num_hops == 0 {
2004                                error!(
2005                                    "x-forwarded-for: 0 specified. x-forwarded-for contents: {:?}. Please assign nonzero value for \
2006                                    number of hops here, or use `socket-addr` client-id-source type if requests are not being proxied \
2007                                    to this node. Skipping traffic controller request handling.",
2008                                    header_contents,
2009                                );
2010                                return None;
2011                            }
2012                            let contents_len = header_contents.len();
2013                            if contents_len < *num_hops {
2014                                error!(
2015                                    "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2016                                    Expected at least {} values. Please correctly set the `x-forwarded-for` value under \
2017                                    `client-id-source` in the node config.",
2018                                    header_contents, contents_len, num_hops, contents_len,
2019                                );
2020                                self.metrics.client_id_source_config_mismatch.inc();
2021                                return None;
2022                            }
2023                            let Some(client_ip) = header_contents.get(contents_len - num_hops)
2024                            else {
2025                                error!(
2026                                    "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2027                                    Expected at least {} values. Skipping traffic controller request handling.",
2028                                    header_contents, contents_len, num_hops, contents_len,
2029                                );
2030                                return None;
2031                            };
2032                            parse_ip(client_ip).or_else(|| {
2033                                self.metrics.forwarded_header_parse_error.inc();
2034                                None
2035                            })
2036                        }
2037                        Err(e) => {
2038                            // TODO: once we have confirmed that no legitimate traffic
2039                            // is hitting this case, we should reject such requests that
2040                            // hit this case.
2041                            self.metrics.forwarded_header_invalid.inc();
2042                            error!("Invalid UTF-8 in x-forwarded-for header: {:?}", e);
2043                            None
2044                        }
2045                    }
2046                };
2047                if let Some(op) = request.metadata().get("x-forwarded-for") {
2048                    do_header_parse(op)
2049                } else if let Some(op) = request.metadata().get("X-Forwarded-For") {
2050                    do_header_parse(op)
2051                } else {
2052                    self.metrics.forwarded_header_not_included.inc();
2053                    error!(
2054                        "x-forwarded-for header not present for request despite node configuring x-forwarded-for tracking type"
2055                    );
2056                    None
2057                }
2058            }
2059        }
2060    }
2061
2062    async fn handle_traffic_req(&self, client: Option<IpAddr>) -> Result<(), tonic::Status> {
2063        if let Some(traffic_controller) = &self.traffic_controller {
2064            if !traffic_controller.check(&client, &None).await {
2065                // Entity in blocklist
2066                Err(tonic::Status::from_error(
2067                    SuiErrorKind::TooManyRequests.into(),
2068                ))
2069            } else {
2070                Ok(())
2071            }
2072        } else {
2073            Ok(())
2074        }
2075    }
2076
2077    fn handle_traffic_resp<T>(
2078        &self,
2079        client: Option<IpAddr>,
2080        wrapped_response: WrappedServiceResponse<T>,
2081        method_name: &str,
2082    ) -> Result<tonic::Response<T>, tonic::Status> {
2083        let (error, spam_weight, unwrapped_response) = match wrapped_response {
2084            Ok((result, spam_weight)) => (None, spam_weight.clone(), Ok(result)),
2085            Err(status) => (
2086                Some(SuiError::from(status.clone())),
2087                Weight::zero(),
2088                Err(status.clone()),
2089            ),
2090        };
2091
2092        if let Some(traffic_controller) = self.traffic_controller.clone() {
2093            traffic_controller.tally(TrafficTally {
2094                direct: client,
2095                through_fullnode: None,
2096                error_info: error.map(|e| {
2097                    let error_type = String::from(e.clone().as_ref());
2098                    let error_weight = normalize(e);
2099                    (error_weight, error_type)
2100                }),
2101                spam_weight,
2102                timestamp: SystemTime::now(),
2103                method: Some(method_name.to_string()),
2104            })
2105        }
2106        unwrapped_response
2107    }
2108}
2109
2110// TODO: refine error matching here
2111fn normalize(err: SuiError) -> Weight {
2112    match err.as_inner() {
2113        SuiErrorKind::UserInputError {
2114            error: UserInputError::IncorrectUserSignature { .. },
2115        } => Weight::one(),
2116        SuiErrorKind::InvalidSignature { .. }
2117        | SuiErrorKind::SignerSignatureAbsent { .. }
2118        | SuiErrorKind::SignerSignatureNumberMismatch { .. }
2119        | SuiErrorKind::IncorrectSigner { .. }
2120        | SuiErrorKind::UnknownSigner { .. }
2121        | SuiErrorKind::WrongEpoch { .. } => Weight::one(),
2122        _ => Weight::zero(),
2123    }
2124}
2125
2126/// Implements generic pre- and post-processing. Since this is on the critical
2127/// path, any heavy lifting should be done in a separate non-blocking task
2128/// unless it is necessary to override the return value.
2129#[macro_export]
2130macro_rules! handle_with_decoration {
2131    ($self:ident, $func_name:ident, $request:ident, $method_name:expr) => {{
2132        if $self.client_id_source.is_none() {
2133            return $self.$func_name($request).await.map(|(result, _)| result);
2134        }
2135
2136        let client = $self.get_client_ip_addr(&$request, $self.client_id_source.as_ref().unwrap());
2137
2138        // check if either IP is blocked, in which case return early
2139        $self.handle_traffic_req(client.clone()).await?;
2140
2141        // handle traffic tallying
2142        let wrapped_response = $self.$func_name($request).await;
2143        $self.handle_traffic_resp(client, wrapped_response, $method_name)
2144    }};
2145}
2146
2147#[async_trait]
2148impl Validator for ValidatorService {
2149    async fn submit_transaction(
2150        &self,
2151        request: tonic::Request<RawSubmitTxRequest>,
2152    ) -> Result<tonic::Response<RawSubmitTxResponse>, tonic::Status> {
2153        let validator_service = self.clone();
2154
2155        // Spawns a task which handles the transaction. The task will unconditionally continue
2156        // processing in the event that the client connection is dropped.
2157        spawn_monitored_task!(async move {
2158            // NB: traffic tally wrapping handled within the task rather than on task exit
2159            // to prevent an attacker from subverting traffic control by severing the connection
2160            handle_with_decoration!(
2161                validator_service,
2162                handle_submit_transaction_impl,
2163                request,
2164                "submit_transaction"
2165            )
2166        })
2167        .await
2168        .unwrap()
2169    }
2170
2171    async fn wait_for_effects(
2172        &self,
2173        request: tonic::Request<RawWaitForEffectsRequest>,
2174    ) -> Result<tonic::Response<RawWaitForEffectsResponse>, tonic::Status> {
2175        handle_with_decoration!(self, wait_for_effects_impl, request, "wait_for_effects")
2176    }
2177
2178    async fn object_info(
2179        &self,
2180        request: tonic::Request<ObjectInfoRequest>,
2181    ) -> Result<tonic::Response<ObjectInfoResponse>, tonic::Status> {
2182        handle_with_decoration!(self, object_info_impl, request, "object_info")
2183    }
2184
2185    async fn transaction_info(
2186        &self,
2187        request: tonic::Request<TransactionInfoRequest>,
2188    ) -> Result<tonic::Response<TransactionInfoResponse>, tonic::Status> {
2189        handle_with_decoration!(self, transaction_info_impl, request, "transaction_info")
2190    }
2191
2192    async fn checkpoint(
2193        &self,
2194        request: tonic::Request<CheckpointRequest>,
2195    ) -> Result<tonic::Response<CheckpointResponse>, tonic::Status> {
2196        handle_with_decoration!(self, checkpoint_impl, request, "checkpoint")
2197    }
2198
2199    async fn checkpoint_v2(
2200        &self,
2201        request: tonic::Request<CheckpointRequestV2>,
2202    ) -> Result<tonic::Response<CheckpointResponseV2>, tonic::Status> {
2203        handle_with_decoration!(self, checkpoint_v2_impl, request, "checkpoint_v2")
2204    }
2205
2206    async fn get_system_state_object(
2207        &self,
2208        request: tonic::Request<SystemStateRequest>,
2209    ) -> Result<tonic::Response<SuiSystemState>, tonic::Status> {
2210        handle_with_decoration!(
2211            self,
2212            get_system_state_object_impl,
2213            request,
2214            "get_system_state_object"
2215        )
2216    }
2217
2218    async fn validator_health(
2219        &self,
2220        request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
2221    ) -> Result<tonic::Response<sui_types::messages_grpc::RawValidatorHealthResponse>, tonic::Status>
2222    {
2223        handle_with_decoration!(self, validator_health_impl, request, "validator_health")
2224    }
2225}
2226
2227#[cfg(test)]
2228mod inflight_guard_tests {
2229    use super::*;
2230    use prometheus::Registry;
2231
2232    fn make_guard(
2233        inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
2234        cache: Cache<TransactionDigest, Instant>,
2235        metrics: Arc<ValidatorServiceMetrics>,
2236    ) -> InflightTransactionsGuard {
2237        InflightTransactionsGuard {
2238            inflight,
2239            recently_submitted: cache,
2240            window: Duration::from_secs(10),
2241            metrics,
2242            acquired: HashSet::new(),
2243        }
2244    }
2245
2246    #[test]
2247    fn concurrent_acquire_rejects_other_request_and_is_idempotent_for_owner() {
2248        let inflight = Arc::new(Mutex::new(HashSet::new()));
2249        let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2250        let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2251        let digest = TransactionDigest::random();
2252
2253        let mut g1 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2254        let mut g2 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2255
2256        // First handler acquires it.
2257        assert!(matches!(g1.try_acquire(digest), AcquireOutcome::Acquired));
2258        assert_eq!(metrics.inflight_transactions.get(), 1);
2259
2260        // A concurrent handler sees another in-flight owner and is rejected.
2261        assert!(matches!(
2262            g2.try_acquire(digest),
2263            AcquireOutcome::AlreadyAcquiredByAnotherRequest
2264        ));
2265
2266        // The owning handler re-acquiring (epoch-end retry) is NOT a duplicate.
2267        assert!(matches!(
2268            g1.try_acquire(digest),
2269            AcquireOutcome::AlreadyAcquiredByThisRequest
2270        ));
2271    }
2272
2273    #[test]
2274    fn drop_demotes_into_recently_processed_outcome() {
2275        let inflight = Arc::new(Mutex::new(HashSet::new()));
2276        let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2277        let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2278        let digest = TransactionDigest::random();
2279
2280        {
2281            let mut g = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2282            assert!(matches!(g.try_acquire(digest), AcquireOutcome::Acquired));
2283            assert_eq!(inflight.lock().len(), 1);
2284        } // guard dropped here -> remove from set + demote to TTL cache
2285
2286        assert_eq!(
2287            inflight.lock().len(),
2288            0,
2289            "acquired digest must be removed from the in-flight set on drop"
2290        );
2291        assert_eq!(metrics.inflight_transactions.get(), 0);
2292
2293        // Make moka's read view deterministic before asserting the demoted entry.
2294        cache.run_pending_tasks();
2295
2296        // A fresh request now sees the digest as recently processed (tail window).
2297        let mut g_after = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2298        assert!(matches!(
2299            g_after.try_acquire(digest),
2300            AcquireOutcome::RecentlyProcessed { .. }
2301        ));
2302    }
2303}