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            // Reject up front rather than proposing a block that peers would reject: the client
799            // must submit to one of the proposers the transaction allows.
800            epoch_store.check_self_allowed_proposer(transaction.data().transaction_data())?;
801
802            // A request must not repeat a transaction.
803            if !request_digests.insert(tx_digest) {
804                let error: SuiError = SuiErrorKind::UserInputError {
805                    error: UserInputError::RepeatedTransactions { digest: tx_digest },
806                }
807                .into();
808                // Reject individual repeated transactions in batch.
809                if is_soft_bundle_request {
810                    return Err(error);
811                }
812                results[idx] = Some(SubmitTxResult::Rejected { error });
813                continue;
814            }
815
816            // Soft bundles require all transactions to use the same gas price.
817            if is_soft_bundle_request {
818                let gas_price = transaction.data().transaction_data().gas_price();
819                if let Some(expected) = expected_soft_bundle_gas_price {
820                    fp_ensure!(
821                        gas_price == expected,
822                        SuiErrorKind::UserInputError {
823                            error: UserInputError::GasPriceMismatchError {
824                                digest: tx_digest,
825                                expected,
826                                actual: gas_price,
827                            }
828                        }
829                        .into()
830                    );
831                } else {
832                    expected_soft_bundle_gas_price = Some(gas_price);
833                }
834            }
835
836            let is_gasless = transaction
837                .data()
838                .transaction_data()
839                .is_gasless_transaction();
840
841            if is_gasless {
842                has_gasless = true;
843                metrics
844                    .gasless_submission_outcomes
845                    .with_label_values(&["attempted"])
846                    .inc();
847            }
848
849            let overload_check_res = state.check_system_overload(
850                transaction.data(),
851                state.check_system_overload_at_signing(),
852            );
853            if let Err(error) = overload_check_res {
854                metrics
855                    .num_rejected_tx_during_overload
856                    .with_label_values(&[error.as_ref()])
857                    .inc();
858                if is_gasless {
859                    metrics
860                        .gasless_submission_outcomes
861                        .with_label_values(&["rejected_overload"])
862                        .inc();
863                }
864                results[idx] = Some(SubmitTxResult::Rejected { error });
865                continue;
866            }
867
868            // Use the pre-queue per-tx consensus overload reject on the direct
869            // submission path (queue off, failover, or ping).
870            if matches!(submit_mode, AdmissionQueueSubmitMode::Direct)
871                && let Err(error) = self.consensus_adapter.check_consensus_overload()
872            {
873                state.update_overload_metrics("consensus");
874                metrics
875                    .num_rejected_tx_during_overload
876                    .with_label_values(&[error.as_ref()])
877                    .inc();
878                if is_gasless {
879                    metrics
880                        .gasless_submission_outcomes
881                        .with_label_values(&["rejected_overload"])
882                        .inc();
883                }
884                results[idx] = Some(SubmitTxResult::Rejected { error });
885                continue;
886            }
887
888            if is_gasless
889                && !self
890                    .gasless_limiter
891                    .try_acquire(epoch_store.protocol_config())
892            {
893                metrics.gasless_rate_limited_count.inc();
894                metrics
895                    .gasless_submission_outcomes
896                    .with_label_values(&["rejected_rate_limited"])
897                    .inc();
898                results[idx] = Some(SubmitTxResult::Rejected {
899                    error: SuiErrorKind::ValidatorOverloadedRetryAfter {
900                        retry_after_secs: 1,
901                    }
902                    .into(),
903                });
904                continue;
905            }
906
907            // Ok to fail the request when any signature is invalid.
908            let verified_transaction = {
909                let _metrics_guard = metrics.tx_verification_latency.start_timer();
910                if epoch_store.protocol_config().address_aliases() {
911                    match epoch_store.verify_transaction_with_current_aliases(transaction) {
912                        Ok(tx) => tx,
913                        Err(e) => {
914                            metrics.signature_errors.inc();
915                            return Err(e);
916                        }
917                    }
918                } else {
919                    match epoch_store.verify_transaction_require_no_aliases(transaction) {
920                        Ok(tx) => tx,
921                        Err(e) => {
922                            metrics.signature_errors.inc();
923                            return Err(e);
924                        }
925                    }
926                }
927            };
928
929            debug!(
930                ?tx_digest,
931                "handle_submit_transaction: verified transaction"
932            );
933
934            // Check if the transaction has executed, before checking input objects
935            // which could have been consumed.
936            if let Some(effects) = state
937                .get_transaction_cache_reader()
938                .get_executed_effects(&tx_digest)
939            {
940                let effects_digest = effects.digest();
941                if let Err(error) = state.check_effects_against_previously_signed(
942                    &epoch_store,
943                    &tx_digest,
944                    &effects_digest,
945                    "submit_transaction",
946                ) {
947                    results[idx] = Some(SubmitTxResult::Rejected { error });
948                    continue;
949                }
950                if let Ok(executed_data) = self.complete_executed_data(effects).await {
951                    let executed_result = SubmitTxResult::Executed {
952                        effects_digest,
953                        details: Some(executed_data),
954                    };
955                    results[idx] = Some(executed_result);
956                    debug!(?tx_digest, "handle_submit_transaction: already executed");
957                    continue;
958                }
959            }
960
961            if self
962                .state
963                .get_transaction_cache_reader()
964                .transaction_executed_in_last_epoch(&tx_digest, epoch_store.epoch())
965            {
966                results[idx] = Some(SubmitTxResult::Rejected {
967                    error: UserInputError::TransactionAlreadyExecuted { digest: tx_digest }.into(),
968                });
969                debug!(
970                    ?tx_digest,
971                    "handle_submit_transaction: transaction already executed in previous epoch"
972                );
973                continue;
974            }
975
976            // Suppress resubmission of transactions consensus already processed this epoch:
977            // executed transactions whose effects details could not be reconstructed above (e.g.
978            // objects pruned), sequenced-but-deferred transactions, and dropped transactions.
979            let consensus_key = SequencedConsensusTransactionKey::External(
980                ConsensusTransactionKey::Certificate(tx_digest),
981            );
982            if epoch_store.is_consensus_message_processed(&consensus_key)? {
983                // Prefer a concrete, non-retriable error over the generic, retriable
984                // TransactionProcessing suppression. A processed-but-unexecuted digest is
985                // commonly a dropped owned-object conflict loser; surfacing the terminal
986                // error lets the client stop retrying instead of polling for effects that
987                // will never come.
988                //
989                // First check the epoch owned-object lock table with the same conflict
990                // logic the consensus handler uses post-consensus. Locks are never
991                // released within an epoch, so this reports the conflict even before the
992                // winner executes, while the loser's input versions still validate as
993                // live.
994                if let Ok(input_objects) = verified_transaction
995                    .tx()
996                    .data()
997                    .transaction_data()
998                    .input_objects()
999                {
1000                    let immutable_object_ids = self
1001                        .collect_immutable_object_ids(verified_transaction.tx(), state)
1002                        .await?;
1003                    let owned_object_refs: Vec<_> = input_objects
1004                        .iter()
1005                        .filter_map(|obj| match obj {
1006                            InputObjectKind::ImmOrOwnedMoveObject(obj_ref)
1007                                if !immutable_object_ids.contains(&obj_ref.0) =>
1008                            {
1009                                Some(*obj_ref)
1010                            }
1011                            _ => None,
1012                        })
1013                        .collect();
1014                    let existing_locks =
1015                        epoch_store.get_owned_object_locks_map(&owned_object_refs)?;
1016                    if let Err(error) = epoch_store.try_acquire_owned_object_locks_post_consensus(
1017                        &owned_object_refs,
1018                        tx_digest,
1019                        &HashMap::new(),
1020                        &existing_locks,
1021                    ) {
1022                        debug!(
1023                            ?tx_digest,
1024                            "handle_submit_transaction: processed transaction rejected on lock conflict: {error}"
1025                        );
1026                        metrics
1027                            .submission_rejected_transactions
1028                            .with_label_values(&[error.to_variant_name()])
1029                            .inc();
1030                        results[idx] = Some(SubmitTxResult::Rejected { error });
1031                        continue;
1032                    }
1033                }
1034                // Then revalidate against live state, which surfaces the terminal
1035                // stale-version error once the conflict winner has executed.
1036                if let Err(error) =
1037                    state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone())
1038                {
1039                    // The transaction may have executed while being validated (e.g. it was
1040                    // deferred rather than dropped).
1041                    if let Some(effects) = state
1042                        .get_transaction_cache_reader()
1043                        .get_executed_effects(&tx_digest)
1044                    {
1045                        let effects_digest = effects.digest();
1046                        if let Err(error) = state.check_effects_against_previously_signed(
1047                            &epoch_store,
1048                            &tx_digest,
1049                            &effects_digest,
1050                            "submit_transaction",
1051                        ) {
1052                            results[idx] = Some(SubmitTxResult::Rejected { error });
1053                            continue;
1054                        }
1055                        if let Ok(executed_data) = self.complete_executed_data(effects).await {
1056                            results[idx] = Some(SubmitTxResult::Executed {
1057                                effects_digest,
1058                                details: Some(executed_data),
1059                            });
1060                            continue;
1061                        }
1062                    }
1063                    debug!(
1064                        ?tx_digest,
1065                        "handle_submit_transaction: processed transaction rejected on revalidation: {error}"
1066                    );
1067                    metrics
1068                        .submission_rejected_transactions
1069                        .with_label_values(&[error.to_variant_name()])
1070                        .inc();
1071                    results[idx] = Some(SubmitTxResult::Rejected { error });
1072                    continue;
1073                }
1074                // Validation passed, so this processed digest may still be executable.
1075                // Return retriable TransactionProcessing rather than resubmitting it to consensus.
1076                // A later client retry can observe effects or a concrete terminal validation error.
1077                metrics
1078                    .submission_suppressed_already_processed
1079                    .with_label_values(&[req_type])
1080                    .inc();
1081                results[idx] = Some(SubmitTxResult::Rejected {
1082                    error: SuiErrorKind::TransactionProcessing {
1083                        digest: tx_digest,
1084                        status: "consensus message processed".to_string(),
1085                    }
1086                    .into(),
1087                });
1088                debug!(
1089                    ?tx_digest,
1090                    "handle_submit_transaction: consensus message already processed"
1091                );
1092                continue;
1093            }
1094
1095            // Atomically acquire the digest for the duration of this handler. Reject concurrent
1096            // and recent duplicates and record the result per result index.
1097            match inflight_guard.try_acquire(tx_digest) {
1098                AcquireOutcome::Acquired | AcquireOutcome::AlreadyAcquiredByThisRequest => {
1099                    // Continue to process the transaction and submit to consensus.
1100                }
1101                AcquireOutcome::AlreadyAcquiredByAnotherRequest => {
1102                    metrics
1103                        .submission_suppressed_inflight
1104                        .with_label_values(&[req_type])
1105                        .inc();
1106                    results[idx] = Some(SubmitTxResult::Rejected {
1107                        error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1108                    });
1109                    debug!(
1110                        ?tx_digest,
1111                        "handle_submit_transaction: concurrent submission in progress"
1112                    );
1113                    continue;
1114                }
1115                AcquireOutcome::RecentlyProcessed { since } => {
1116                    metrics
1117                        .submission_suppressed_recently_submitted
1118                        .with_label_values(&[req_type])
1119                        .inc();
1120                    metrics
1121                        .recently_submitted_resubmission_interval
1122                        .observe(since.as_secs_f64());
1123                    results[idx] = Some(SubmitTxResult::Rejected {
1124                        error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1125                    });
1126                    debug!(?tx_digest, "handle_submit_transaction: recently processed");
1127                    continue;
1128                }
1129            }
1130
1131            debug!(
1132                ?tx_digest,
1133                "handle_submit_transaction: waiting for fastpath dependency objects"
1134            );
1135            if !state
1136                .wait_for_fastpath_dependency_objects(
1137                    verified_transaction.tx(),
1138                    epoch_store.epoch(),
1139                )
1140                .await?
1141            {
1142                debug!(
1143                    ?tx_digest,
1144                    "fastpath input objects are still unavailable after waiting"
1145                );
1146            }
1147
1148            match state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone()) {
1149                Ok(_) => { /* continue processing */ }
1150                Err(e) => {
1151                    // Check if transaction has been executed while being validated.
1152                    // This is an edge case so checking executed effects twice is acceptable.
1153                    if let Some(effects) = state
1154                        .get_transaction_cache_reader()
1155                        .get_executed_effects(&tx_digest)
1156                    {
1157                        let effects_digest = effects.digest();
1158                        if let Err(error) = state.check_effects_against_previously_signed(
1159                            &epoch_store,
1160                            &tx_digest,
1161                            &effects_digest,
1162                            "submit_transaction",
1163                        ) {
1164                            results[idx] = Some(SubmitTxResult::Rejected { error });
1165                            continue;
1166                        }
1167                        if let Ok(executed_data) = self.complete_executed_data(effects).await {
1168                            let executed_result = SubmitTxResult::Executed {
1169                                effects_digest,
1170                                details: Some(executed_data),
1171                            };
1172                            results[idx] = Some(executed_result);
1173                            continue;
1174                        }
1175                    }
1176
1177                    // When the transaction has not been executed, record the error for the transaction.
1178                    debug!(?tx_digest, "Transaction rejected during submission: {e}");
1179                    metrics
1180                        .submission_rejected_transactions
1181                        .with_label_values(&[e.to_variant_name()])
1182                        .inc();
1183                    results[idx] = Some(SubmitTxResult::Rejected { error: e });
1184                    continue;
1185                }
1186            }
1187
1188            // Create claims with aliases and / or immutable objects.
1189            let mut claims = vec![];
1190
1191            let immutable_object_ids = self
1192                .collect_immutable_object_ids(verified_transaction.tx(), state)
1193                .await?;
1194            if !immutable_object_ids.is_empty() {
1195                claims.push(TransactionClaim::ImmutableInputObjects(
1196                    immutable_object_ids,
1197                ));
1198            }
1199
1200            let (tx, aliases) = verified_transaction.into_inner();
1201            if epoch_store.protocol_config().address_aliases() {
1202                if epoch_store
1203                    .protocol_config()
1204                    .fix_checkpoint_signature_mapping()
1205                {
1206                    claims.push(TransactionClaim::AddressAliasesV2(aliases));
1207                } else {
1208                    let v1_aliases: Vec<_> = tx
1209                        .data()
1210                        .intent_message()
1211                        .value
1212                        .required_signers()
1213                        .into_iter()
1214                        .zip_eq(aliases.into_iter().map(|(_, seq)| seq))
1215                        .collect();
1216                    #[allow(deprecated)]
1217                    claims.push(TransactionClaim::AddressAliases(
1218                        nonempty::NonEmpty::from_vec(v1_aliases)
1219                            .expect("must have at least one required_signer"),
1220                    ));
1221                }
1222            }
1223
1224            let tx_with_claims = TransactionWithClaims::new(tx.into(), claims);
1225
1226            consensus_transactions.push(ConsensusTransaction::new_user_transaction_v2_message(
1227                &state.name,
1228                tx_with_claims,
1229            ));
1230            if is_gasless {
1231                metrics
1232                    .gasless_submission_outcomes
1233                    .with_label_values(&["submitted"])
1234                    .inc();
1235            }
1236
1237            transaction_indexes.push(idx);
1238            tx_digests.push(tx_digest);
1239            total_size_bytes += tx_size;
1240        }
1241
1242        if consensus_transactions.is_empty() && !is_ping_request {
1243            let spam_weight = Self::request_spam_weight(
1244                &results,
1245                has_gasless,
1246                duplicate_at_admission,
1247                is_ping_request,
1248            );
1249            let response = Self::try_from_submit_tx_response(results)?;
1250            return Ok((response, spam_weight));
1251        }
1252
1253        // Set the max bytes size of the soft bundle to be half of the consensus max transactions in block size.
1254        // We do this to account for serialization overheads and to ensure that the soft bundle is not too large
1255        // when is attempted to be posted via consensus.
1256        let max_transaction_bytes = if is_soft_bundle_request {
1257            epoch_store
1258                .protocol_config()
1259                .consensus_max_transactions_in_block_bytes()
1260                / 2
1261        } else {
1262            epoch_store
1263                .protocol_config()
1264                .consensus_max_transactions_in_block_bytes()
1265        };
1266        fp_ensure!(
1267            total_size_bytes <= max_transaction_bytes as usize,
1268            SuiErrorKind::UserInputError {
1269                error: UserInputError::TotalTransactionSizeTooLargeInBatch {
1270                    size: total_size_bytes,
1271                    limit: max_transaction_bytes,
1272                },
1273            }
1274            .into()
1275        );
1276
1277        metrics
1278            .handle_submit_transaction_bytes
1279            .with_label_values(&[req_type])
1280            .observe(total_size_bytes as f64);
1281        metrics
1282            .handle_submit_transaction_batch_size
1283            .with_label_values(&[req_type])
1284            .observe(consensus_transactions.len() as f64);
1285
1286        let _latency_metric_guard = metrics
1287            .handle_submit_transaction_consensus_latency
1288            .with_label_values(&[req_type])
1289            .start_timer();
1290
1291        if is_soft_bundle_request {
1292            // 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.
1293            // For any other case, having an empty `consensus_transactions` vector is an invalid state and we should have never reached at this point.
1294            assert!(
1295                !consensus_transactions.is_empty(),
1296                "A valid soft bundle must have at least one transaction"
1297            );
1298        }
1299
1300        // Soft bundles are inserted as a single queue entry.
1301        // Individual transactions are each inserted separately.
1302        let tx_groups: Vec<Vec<ConsensusTransaction>> = if is_soft_bundle_request || is_ping_request
1303        {
1304            vec![consensus_transactions]
1305        } else {
1306            consensus_transactions
1307                .into_iter()
1308                .map(|t| vec![t])
1309                .collect()
1310        };
1311
1312        // Map each submission group back to the (result index, digest) of the transactions it
1313        // contains, so a per-group outcome — consensus positions, or an "already processing"
1314        // error — can be recorded against each individual transaction. Soft bundles submit as a
1315        // single group; individual transactions submit one group each.
1316        let group_tx_meta = if is_soft_bundle_request {
1317            vec![
1318                transaction_indexes
1319                    .into_iter()
1320                    .zip_eq(tx_digests)
1321                    .collect::<Vec<_>>(),
1322            ]
1323        } else {
1324            transaction_indexes
1325                .into_iter()
1326                .zip_eq(tx_digests)
1327                .map(|pair| vec![pair])
1328                .collect::<Vec<_>>()
1329        };
1330
1331        // Collect one result per submission group WITHOUT short-circuiting. An
1332        // already-processing transaction is reported per-tx as a retriable below;
1333        // any other error fails the whole request, after all groups have settled.
1334        // Soft bundles submit as a single group; individual transactions submit one group each.
1335        let group_results = match submit_mode {
1336            AdmissionQueueSubmitMode::Direct => {
1337                let futures = tx_groups.into_iter().map(|txns| {
1338                    debug!(
1339                        "handle_submit_transaction: submitting consensus transactions ({}): {}",
1340                        req_type,
1341                        txns.iter().map(|t| t.local_display()).join(", ")
1342                    );
1343                    self.consensus_adapter.submit_and_get_positions(
1344                        txns,
1345                        &epoch_store,
1346                        submitter_client_addr,
1347                    )
1348                });
1349                future::join_all(futures).await
1350            }
1351            AdmissionQueueSubmitMode::Queue => {
1352                let aq = self
1353                    .admission_queue
1354                    .as_ref()
1355                    .expect("Queue mode implies admission_queue is Some")
1356                    .load();
1357                let mut receivers = Vec::with_capacity(tx_groups.len());
1358                for txns in tx_groups {
1359                    let gas_price = Self::extract_gas_price(&txns);
1360                    let (rx, newly_inserted) = aq
1361                        .try_insert(gas_price, txns, submitter_client_addr)
1362                        .await?;
1363                    if !newly_inserted {
1364                        // Duplicate of an in-flight submission; flag the request as spam. The
1365                        // per-tx result is still Submitted, so this is tracked separately.
1366                        duplicate_at_admission = true;
1367                    }
1368                    receivers.push(rx);
1369                }
1370                future::join_all(receivers.into_iter().map(|rx| async move {
1371                    match rx.await {
1372                        Ok(result) => result.map_err(SuiError::from),
1373                        Err(_) => Err(SuiError::from(
1374                            SuiErrorKind::TooManyTransactionsPendingConsensus,
1375                        )),
1376                    }
1377                }))
1378                .await
1379            }
1380        };
1381
1382        if is_ping_request {
1383            // For ping requests there is a single group returning the special consensus position.
1384            let consensus_positions = group_results
1385                .into_iter()
1386                .next()
1387                .expect("Ping request must have exactly one submission group")?;
1388            assert_eq!(consensus_positions.len(), 1);
1389            results.push(Some(SubmitTxResult::Submitted {
1390                consensus_position: consensus_positions[0],
1391            }));
1392        } else {
1393            for (group_result, txns_meta) in group_results.into_iter().zip_debug_eq(group_tx_meta) {
1394                match group_result {
1395                    Ok(consensus_positions) => {
1396                        for ((idx, tx_digest), consensus_position) in
1397                            txns_meta.into_iter().zip_debug_eq(consensus_positions)
1398                        {
1399                            debug!(
1400                                ?tx_digest,
1401                                "handle_submit_transaction: submitted consensus transaction at {}",
1402                                consensus_position,
1403                            );
1404                            results[idx] = Some(SubmitTxResult::Submitted { consensus_position });
1405                        }
1406                    }
1407                    // The transaction(s) in this group are already being processed by consensus.
1408                    // Report per-tx as a retriable rejection rather than failing the whole request.
1409                    Err(err) => {
1410                        let SuiErrorKind::TransactionProcessing { status, .. } =
1411                            err.as_inner().clone()
1412                        else {
1413                            return Err(err);
1414                        };
1415                        // For TransactionProcessing error, ensure the per txn result has the correct digest.
1416                        for (idx, tx_digest) in txns_meta {
1417                            debug!(
1418                                ?tx_digest,
1419                                "handle_submit_transaction: transaction already processing: {err}"
1420                            );
1421                            // Same suppression the upfront `is_consensus_message_processed` check
1422                            // records, just detected during submission instead of before it. The
1423                            // two paths are mutually exclusive, so this does not double-count.
1424                            metrics
1425                                .submission_suppressed_already_processed
1426                                .with_label_values(&[req_type])
1427                                .inc();
1428                            results[idx] = Some(SubmitTxResult::Rejected {
1429                                error: SuiErrorKind::TransactionProcessing {
1430                                    digest: tx_digest,
1431                                    status: status.clone(),
1432                                }
1433                                .into(),
1434                            });
1435                        }
1436                    }
1437                }
1438            }
1439        }
1440
1441        let spam_weight = Self::request_spam_weight(
1442            &results,
1443            has_gasless,
1444            duplicate_at_admission,
1445            is_ping_request,
1446        );
1447        let response = Self::try_from_submit_tx_response(results)?;
1448        Ok((response, spam_weight))
1449    }
1450
1451    /// Traffic-control spam weight for a whole submit request. The request is spam unless it is
1452    /// entirely accepted gas-chargable work.
1453    fn request_spam_weight(
1454        results: &[Option<SubmitTxResult>],
1455        has_gasless: bool,
1456        duplicate_at_admission: bool,
1457        is_ping: bool,
1458    ) -> Weight {
1459        if is_ping || has_gasless || duplicate_at_admission {
1460            return Weight::one();
1461        }
1462        for result in results {
1463            let Some(result) = result else {
1464                // `results` is expected to be fully populated (every entry `Some`) for the
1465                // request's transactions; a missing entry is a bug and is conservatively
1466                // treated as spam.
1467                debug_fatal!("transaction outcome unset when computing spam weight");
1468                return Weight::one();
1469            };
1470            if Self::submission_spam_weight(result) == Weight::one() {
1471                return Weight::one();
1472            }
1473        }
1474        Weight::zero()
1475    }
1476
1477    fn submission_spam_weight(result: &SubmitTxResult) -> Weight {
1478        match result {
1479            SubmitTxResult::Submitted { .. } => Weight::zero(),
1480            // Non-submitted results can't be charged.
1481            SubmitTxResult::Executed { .. } | SubmitTxResult::Rejected { .. } => Weight::one(),
1482        }
1483    }
1484
1485    fn try_from_submit_tx_response(
1486        results: Vec<Option<SubmitTxResult>>,
1487    ) -> Result<RawSubmitTxResponse, SuiError> {
1488        let mut raw_results = Vec::new();
1489        for (i, result) in results.into_iter().enumerate() {
1490            let result = result.ok_or_else(|| SuiErrorKind::GenericAuthorityError {
1491                error: format!("Missing transaction result at {}", i),
1492            })?;
1493            let raw_result = result.try_into()?;
1494            raw_results.push(raw_result);
1495        }
1496        Ok(RawSubmitTxResponse {
1497            results: raw_results,
1498        })
1499    }
1500
1501    /// Extract the gas price from a batch of consensus transactions.
1502    /// Returns the minimum gas price in the batch, or 0 if no user transactions.
1503    fn extract_gas_price(transactions: &[ConsensusTransaction]) -> u64 {
1504        use sui_types::messages_consensus::ConsensusTransactionKind;
1505        transactions
1506            .iter()
1507            .filter_map(|tx| match &tx.kind {
1508                ConsensusTransactionKind::CertifiedTransaction(cert) => Some(cert.gas_price()),
1509                ConsensusTransactionKind::UserTransaction(t) => {
1510                    Some(t.data().transaction_data().gas_price())
1511                }
1512                ConsensusTransactionKind::UserTransactionV2(t) => {
1513                    Some(t.tx().data().transaction_data().gas_price())
1514                }
1515                _ => None,
1516            })
1517            .min()
1518            .unwrap_or(0)
1519    }
1520
1521    fn classify_submit_mode(&self, is_ping_request: bool) -> AdmissionQueueSubmitMode {
1522        let Some(aq) = &self.admission_queue else {
1523            return AdmissionQueueSubmitMode::Direct;
1524        };
1525
1526        // Ping requests carry no transactions and must not wait behind queued
1527        // work; submit them directly to consensus.
1528        if is_ping_request {
1529            return AdmissionQueueSubmitMode::Direct;
1530        }
1531
1532        // If the queue actor is stuck, fall back to direct submission with the
1533        // pre-queue saturation reject until it resumes making progress.
1534        if aq.load().failover_tripped() {
1535            return AdmissionQueueSubmitMode::Direct;
1536        }
1537
1538        AdmissionQueueSubmitMode::Queue
1539    }
1540
1541    async fn collect_effects_data(
1542        &self,
1543        effects: &TransactionEffects,
1544        include_events: bool,
1545        include_input_objects: bool,
1546        include_output_objects: bool,
1547    ) -> SuiResult<(Option<TransactionEvents>, Vec<Object>, Vec<Object>)> {
1548        let events = if include_events && effects.events_digest().is_some() {
1549            Some(
1550                self.state
1551                    .get_transaction_events(effects.transaction_digest())?,
1552            )
1553        } else {
1554            None
1555        };
1556
1557        let input_objects = if include_input_objects {
1558            self.state.get_transaction_input_objects(effects)?
1559        } else {
1560            vec![]
1561        };
1562
1563        let output_objects = if include_output_objects {
1564            self.state.get_transaction_output_objects(effects)?
1565        } else {
1566            vec![]
1567        };
1568
1569        Ok((events, input_objects, output_objects))
1570    }
1571}
1572
1573type WrappedServiceResponse<T> = Result<(tonic::Response<T>, Weight), tonic::Status>;
1574
1575/// RAII guard tracking the transaction digests a single submit request is actively handling, so
1576/// concurrent duplicates can be rejected. On drop, each acquired digest is removed from
1577/// the in-flight set and demoted into `recently_submitted` cache, so resubmissions
1578/// arriving shortly after the handler returns are still suppressed.
1579struct InflightTransactionsGuard {
1580    // Handle to inflight map and recently submitted cache.
1581    inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
1582    recently_submitted: Cache<TransactionDigest, Instant>,
1583    window: Duration,
1584    metrics: Arc<ValidatorServiceMetrics>,
1585    /// Digests this request successfully acquired.
1586    acquired: HashSet<TransactionDigest>,
1587}
1588
1589enum AcquireOutcome {
1590    /// Transaction digest newly acquired by this request.
1591    Acquired,
1592    /// Transaction digest already acquired by this request before this internal retry attempt.
1593    AlreadyAcquiredByThisRequest,
1594    /// Transaction digest being handled by another concurrent request — reject this index.
1595    AlreadyAcquiredByAnotherRequest,
1596    /// Transaction digest recently processed — reject this index.
1597    RecentlyProcessed { since: Duration },
1598}
1599
1600impl InflightTransactionsGuard {
1601    fn new(service: &ValidatorService) -> Self {
1602        Self {
1603            inflight: service.inflight_transactions.clone(),
1604            recently_submitted: service.recently_submitted.clone(),
1605            window: service.recent_submission_window,
1606            metrics: service.metrics.clone(),
1607            acquired: HashSet::new(),
1608        }
1609    }
1610
1611    fn try_acquire(&mut self, digest: TransactionDigest) -> AcquireOutcome {
1612        // A retry of this own request re-acquires the same digests.
1613        if self.acquired.contains(&digest) {
1614            return AcquireOutcome::AlreadyAcquiredByThisRequest;
1615        }
1616
1617        // Suppress resubmissions of recently processed transactions.
1618        if let Some(outcome) = self.recently_processed_outcome(digest) {
1619            return outcome;
1620        }
1621
1622        // Atomic check-and-acquire against concurrent in-flight transactions.
1623        {
1624            let mut set = self.inflight.lock();
1625            // Only continue processing the transaction if it is not already inflight.
1626            if !set.insert(digest) {
1627                return AcquireOutcome::AlreadyAcquiredByAnotherRequest;
1628            }
1629            self.metrics.inflight_transactions.set(set.len() as i64);
1630        }
1631
1632        // Without this re-check, a duplicated transaction arriving between the first check and
1633        // the digest acquisition could slip through.
1634        if let Some(outcome) = self.recently_processed_outcome(digest) {
1635            let mut set = self.inflight.lock();
1636            set.remove(&digest);
1637            self.metrics.inflight_transactions.set(set.len() as i64);
1638            return outcome;
1639        }
1640
1641        self.acquired.insert(digest);
1642        AcquireOutcome::Acquired
1643    }
1644
1645    fn recently_processed_outcome(&self, digest: TransactionDigest) -> Option<AcquireOutcome> {
1646        let recorded_at = self.recently_submitted.get(&digest)?;
1647        let since = recorded_at.elapsed();
1648        (since < self.window).then_some(AcquireOutcome::RecentlyProcessed { since })
1649    }
1650}
1651
1652impl Drop for InflightTransactionsGuard {
1653    fn drop(&mut self) {
1654        if self.acquired.is_empty() {
1655            return;
1656        }
1657        // Demote inflight transactions to recently submitted cache before taking the in-flight lock,
1658        // to avoid cleaning up the cache with the lock.
1659        let now = Instant::now();
1660        for digest in &self.acquired {
1661            self.recently_submitted.insert(*digest, now);
1662        }
1663        {
1664            let mut set = self.inflight.lock();
1665            for digest in &self.acquired {
1666                set.remove(digest);
1667            }
1668            self.metrics.inflight_transactions.set(set.len() as i64);
1669        }
1670        self.metrics
1671            .recently_submitted_cache_size
1672            .set(self.recently_submitted.entry_count() as i64);
1673    }
1674}
1675
1676impl ValidatorService {
1677    async fn handle_submit_transaction_impl(
1678        &self,
1679        request: tonic::Request<RawSubmitTxRequest>,
1680    ) -> WrappedServiceResponse<RawSubmitTxResponse> {
1681        self.handle_submit_transaction(request).await
1682    }
1683
1684    async fn wait_for_effects_impl(
1685        &self,
1686        request: tonic::Request<RawWaitForEffectsRequest>,
1687    ) -> WrappedServiceResponse<RawWaitForEffectsResponse> {
1688        let request: WaitForEffectsRequest = request.into_inner().try_into()?;
1689        let epoch_store = self.state.load_epoch_store_one_call_per_task();
1690        let response = timeout(
1691            // TODO(fastpath): Tune this once we have a good estimate of the typical delay.
1692            Duration::from_secs(20),
1693            epoch_store
1694                .within_alive_epoch(self.wait_for_effects_response(request, &epoch_store))
1695                .map_err(|_| SuiErrorKind::EpochEnded(epoch_store.epoch())),
1696        )
1697        .await
1698        .map_err(|_| tonic::Status::internal("Timeout waiting for effects"))???
1699        .try_into()?;
1700        Ok((tonic::Response::new(response), Weight::zero()))
1701    }
1702
1703    #[instrument(name= "ValidatorService::wait_for_effects_response", level = "debug", skip_all, fields(consensus_position = ?request.consensus_position))]
1704    async fn wait_for_effects_response(
1705        &self,
1706        request: WaitForEffectsRequest,
1707        epoch_store: &Arc<AuthorityPerEpochStore>,
1708    ) -> SuiResult<WaitForEffectsResponse> {
1709        if request.ping_type.is_some() {
1710            return timeout(
1711                Duration::from_secs(10),
1712                self.ping_response(request, epoch_store),
1713            )
1714            .await
1715            .map_err(|_| SuiErrorKind::TimeoutError)?;
1716        }
1717
1718        let Some(tx_digest) = request.transaction_digest else {
1719            return Err(SuiErrorKind::InvalidRequest(
1720                "Transaction digest is required for wait for effects requests".to_string(),
1721            )
1722            .into());
1723        };
1724        let tx_digests = [tx_digest];
1725
1726        // When consensus_position is provided, also watch the consensus status cache
1727        // so rejected/dropped transactions get a timely response instead of waiting
1728        // forever for effects that will never be produced.
1729        let consensus_status_future = async {
1730            let consensus_position = match request.consensus_position {
1731                Some(pos) => pos,
1732                None => return futures::future::pending().await,
1733            };
1734            let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1735            consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1736            match consensus_tx_status_cache
1737                .notify_read_transaction_status(consensus_position)
1738                .await
1739            {
1740                NotifyReadConsensusTxStatusResult::Status(
1741                    ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped,
1742                ) => Ok(WaitForEffectsResponse::Rejected {
1743                    error: epoch_store.get_rejection_vote_reason(consensus_position),
1744                }),
1745                NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized) => {
1746                    // Effects will be produced — yield to let the effects future win.
1747                    futures::future::pending().await
1748                }
1749                NotifyReadConsensusTxStatusResult::Expired(round) => {
1750                    Ok(WaitForEffectsResponse::Expired {
1751                        epoch: epoch_store.epoch(),
1752                        round: Some(round),
1753                    })
1754                }
1755            }
1756        };
1757
1758        tokio::select! {
1759            effects_result = self.state
1760                .get_transaction_cache_reader()
1761                .notify_read_executed_effects_may_fail(
1762                    "AuthorityServer::wait_for_effects::notify_read_executed_effects_finalized",
1763                    &tx_digests,
1764                ) => {
1765                let effects = effects_result?.pop().unwrap();
1766                let effects_digest = effects.digest();
1767                self.state.check_effects_against_previously_signed(
1768                    epoch_store,
1769                    &tx_digest,
1770                    &effects_digest,
1771                    "wait_for_effects",
1772                )?;
1773                let details = if request.include_details {
1774                    Some(self.complete_executed_data(effects).await?)
1775                } else {
1776                    None
1777                };
1778                Ok(WaitForEffectsResponse::Executed {
1779                    effects_digest,
1780                    details,
1781                })
1782            }
1783            status_response = consensus_status_future => {
1784                status_response
1785            }
1786        }
1787    }
1788
1789    #[instrument(level = "error", skip_all, err(level = "debug"))]
1790    async fn ping_response(
1791        &self,
1792        request: WaitForEffectsRequest,
1793        epoch_store: &Arc<AuthorityPerEpochStore>,
1794    ) -> SuiResult<WaitForEffectsResponse> {
1795        let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1796
1797        let Some(consensus_position) = request.consensus_position else {
1798            return Err(SuiErrorKind::InvalidRequest(
1799                "Consensus position is required for Ping requests".to_string(),
1800            )
1801            .into());
1802        };
1803
1804        // We assume that the caller has already checked for the existence of the `ping` field, but handling it gracefully here.
1805        let Some(ping) = request.ping_type else {
1806            return Err(SuiErrorKind::InvalidRequest(
1807                "Ping type is required for ping requests".to_string(),
1808            )
1809            .into());
1810        };
1811
1812        let _metrics_guard = self
1813            .metrics
1814            .handle_wait_for_effects_ping_latency
1815            .with_label_values(&[ping.as_str()])
1816            .start_timer();
1817
1818        consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1819
1820        let details = if request.include_details {
1821            Some(Box::new(ExecutedData::default()))
1822        } else {
1823            None
1824        };
1825
1826        let status = consensus_tx_status_cache
1827            .notify_read_transaction_status(consensus_position)
1828            .await;
1829        match status {
1830            NotifyReadConsensusTxStatusResult::Status(status) => match status {
1831                ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped => {
1832                    Ok(WaitForEffectsResponse::Rejected {
1833                        error: epoch_store.get_rejection_vote_reason(consensus_position),
1834                    })
1835                }
1836                ConsensusTxStatus::Finalized => Ok(WaitForEffectsResponse::Executed {
1837                    effects_digest: TransactionEffectsDigest::ZERO,
1838                    details,
1839                }),
1840            },
1841            NotifyReadConsensusTxStatusResult::Expired(round) => {
1842                Ok(WaitForEffectsResponse::Expired {
1843                    epoch: epoch_store.epoch(),
1844                    round: Some(round),
1845                })
1846            }
1847        }
1848    }
1849
1850    async fn complete_executed_data(
1851        &self,
1852        effects: TransactionEffects,
1853    ) -> SuiResult<Box<ExecutedData>> {
1854        let (events, input_objects, output_objects) = self
1855            .collect_effects_data(
1856                &effects, /* include_events */ true, /* include_input_objects */ true,
1857                /* include_output_objects */ true,
1858            )
1859            .await?;
1860        Ok(Box::new(ExecutedData {
1861            effects,
1862            events,
1863            input_objects,
1864            output_objects,
1865        }))
1866    }
1867
1868    async fn object_info_impl(
1869        &self,
1870        request: tonic::Request<ObjectInfoRequest>,
1871    ) -> WrappedServiceResponse<ObjectInfoResponse> {
1872        let request = request.into_inner();
1873        let response = self.state.handle_object_info_request(request).await?;
1874        Ok((tonic::Response::new(response), Weight::one()))
1875    }
1876
1877    async fn transaction_info_impl(
1878        &self,
1879        request: tonic::Request<TransactionInfoRequest>,
1880    ) -> WrappedServiceResponse<TransactionInfoResponse> {
1881        let request = request.into_inner();
1882        let response = self.state.handle_transaction_info_request(request).await?;
1883        Ok((tonic::Response::new(response), Weight::one()))
1884    }
1885
1886    async fn checkpoint_impl(
1887        &self,
1888        request: tonic::Request<CheckpointRequest>,
1889    ) -> WrappedServiceResponse<CheckpointResponse> {
1890        let request = request.into_inner();
1891        let response = self.state.handle_checkpoint_request(&request)?;
1892        Ok((tonic::Response::new(response), Weight::one()))
1893    }
1894
1895    async fn checkpoint_v2_impl(
1896        &self,
1897        request: tonic::Request<CheckpointRequestV2>,
1898    ) -> WrappedServiceResponse<CheckpointResponseV2> {
1899        let request = request.into_inner();
1900        let response = self.state.handle_checkpoint_request_v2(&request)?;
1901        Ok((tonic::Response::new(response), Weight::one()))
1902    }
1903
1904    async fn get_system_state_object_impl(
1905        &self,
1906        _request: tonic::Request<SystemStateRequest>,
1907    ) -> WrappedServiceResponse<SuiSystemState> {
1908        let response = self
1909            .state
1910            .get_object_cache_reader()
1911            .get_sui_system_state_object_unsafe()?;
1912        Ok((tonic::Response::new(response), Weight::one()))
1913    }
1914
1915    async fn validator_health_impl(
1916        &self,
1917        _request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
1918    ) -> WrappedServiceResponse<sui_types::messages_grpc::RawValidatorHealthResponse> {
1919        let state = &self.state;
1920
1921        // Get epoch store once for both metrics
1922        let epoch_store = state.load_epoch_store_one_call_per_task();
1923
1924        // Get in-flight execution transactions from execution scheduler
1925        let num_inflight_execution_transactions =
1926            state.execution_scheduler().num_pending_certificates() as u64;
1927
1928        // Get in-flight consensus transactions from consensus adapter
1929        let num_inflight_consensus_transactions =
1930            self.consensus_adapter.num_inflight_transactions();
1931
1932        // Get last committed leader round from epoch store
1933        let last_committed_leader_round = epoch_store
1934            .consensus_tx_status_cache
1935            .get_last_committed_leader_round()
1936            .unwrap_or(0);
1937
1938        // Get last locally built checkpoint sequence
1939        let last_locally_built_checkpoint = epoch_store
1940            .last_built_checkpoint_summary()
1941            .ok()
1942            .flatten()
1943            .map(|(_, summary)| summary.sequence_number)
1944            .unwrap_or(0);
1945
1946        let typed_response = sui_types::messages_grpc::ValidatorHealthResponse {
1947            num_inflight_consensus_transactions,
1948            num_inflight_execution_transactions,
1949            last_locally_built_checkpoint,
1950            last_committed_leader_round,
1951        };
1952
1953        let raw_response = typed_response
1954            .try_into()
1955            .map_err(|e: sui_types::error::SuiError| {
1956                tonic::Status::internal(format!("Failed to serialize health response: {}", e))
1957            })?;
1958
1959        Ok((tonic::Response::new(raw_response), Weight::one()))
1960    }
1961
1962    fn get_client_ip_addr<T>(
1963        &self,
1964        request: &tonic::Request<T>,
1965        source: &ClientIdSource,
1966    ) -> Option<IpAddr> {
1967        let forwarded_header = request.metadata().get_all("x-forwarded-for").iter().next();
1968
1969        if let Some(header) = forwarded_header {
1970            let num_hops = header
1971                .to_str()
1972                .map(|h| h.split(',').count().saturating_sub(1))
1973                .unwrap_or(0);
1974
1975            self.metrics.x_forwarded_for_num_hops.set(num_hops as f64);
1976        }
1977
1978        match source {
1979            ClientIdSource::SocketAddr => {
1980                let socket_addr: Option<SocketAddr> = request.remote_addr();
1981
1982                // We will hit this case if the IO type used does not
1983                // implement Connected or when using a unix domain socket.
1984                // TODO: once we have confirmed that no legitimate traffic
1985                // is hitting this case, we should reject such requests that
1986                // hit this case.
1987                if let Some(socket_addr) = socket_addr {
1988                    Some(socket_addr.ip())
1989                } else {
1990                    if cfg!(msim) {
1991                        // Ignore the error from simtests.
1992                    } else if cfg!(test) {
1993                        panic!("Failed to get remote address from request");
1994                    } else {
1995                        self.metrics.connection_ip_not_found.inc();
1996                        error!("Failed to get remote address from request");
1997                    }
1998                    None
1999                }
2000            }
2001            ClientIdSource::XForwardedFor(num_hops) => {
2002                let do_header_parse = |op: &MetadataValue<Ascii>| {
2003                    match op.to_str() {
2004                        Ok(header_val) => {
2005                            let header_contents =
2006                                header_val.split(',').map(str::trim).collect::<Vec<_>>();
2007                            if *num_hops == 0 {
2008                                error!(
2009                                    "x-forwarded-for: 0 specified. x-forwarded-for contents: {:?}. Please assign nonzero value for \
2010                                    number of hops here, or use `socket-addr` client-id-source type if requests are not being proxied \
2011                                    to this node. Skipping traffic controller request handling.",
2012                                    header_contents,
2013                                );
2014                                return None;
2015                            }
2016                            let contents_len = header_contents.len();
2017                            if contents_len < *num_hops {
2018                                error!(
2019                                    "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2020                                    Expected at least {} values. Please correctly set the `x-forwarded-for` value under \
2021                                    `client-id-source` in the node config.",
2022                                    header_contents, contents_len, num_hops, contents_len,
2023                                );
2024                                self.metrics.client_id_source_config_mismatch.inc();
2025                                return None;
2026                            }
2027                            let Some(client_ip) = header_contents.get(contents_len - num_hops)
2028                            else {
2029                                error!(
2030                                    "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2031                                    Expected at least {} values. Skipping traffic controller request handling.",
2032                                    header_contents, contents_len, num_hops, contents_len,
2033                                );
2034                                return None;
2035                            };
2036                            parse_ip(client_ip).or_else(|| {
2037                                self.metrics.forwarded_header_parse_error.inc();
2038                                None
2039                            })
2040                        }
2041                        Err(e) => {
2042                            // TODO: once we have confirmed that no legitimate traffic
2043                            // is hitting this case, we should reject such requests that
2044                            // hit this case.
2045                            self.metrics.forwarded_header_invalid.inc();
2046                            error!("Invalid UTF-8 in x-forwarded-for header: {:?}", e);
2047                            None
2048                        }
2049                    }
2050                };
2051                if let Some(op) = request.metadata().get("x-forwarded-for") {
2052                    do_header_parse(op)
2053                } else if let Some(op) = request.metadata().get("X-Forwarded-For") {
2054                    do_header_parse(op)
2055                } else {
2056                    self.metrics.forwarded_header_not_included.inc();
2057                    error!(
2058                        "x-forwarded-for header not present for request despite node configuring x-forwarded-for tracking type"
2059                    );
2060                    None
2061                }
2062            }
2063        }
2064    }
2065
2066    async fn handle_traffic_req(&self, client: Option<IpAddr>) -> Result<(), tonic::Status> {
2067        if let Some(traffic_controller) = &self.traffic_controller {
2068            if !traffic_controller.check(&client, &None).await {
2069                // Entity in blocklist
2070                Err(tonic::Status::from_error(
2071                    SuiErrorKind::TooManyRequests.into(),
2072                ))
2073            } else {
2074                Ok(())
2075            }
2076        } else {
2077            Ok(())
2078        }
2079    }
2080
2081    fn handle_traffic_resp<T>(
2082        &self,
2083        client: Option<IpAddr>,
2084        wrapped_response: WrappedServiceResponse<T>,
2085        method_name: &str,
2086    ) -> Result<tonic::Response<T>, tonic::Status> {
2087        let (error, spam_weight, unwrapped_response) = match wrapped_response {
2088            Ok((result, spam_weight)) => (None, spam_weight.clone(), Ok(result)),
2089            Err(status) => (
2090                Some(SuiError::from(status.clone())),
2091                Weight::zero(),
2092                Err(status.clone()),
2093            ),
2094        };
2095
2096        if let Some(traffic_controller) = self.traffic_controller.clone() {
2097            traffic_controller.tally(TrafficTally {
2098                direct: client,
2099                through_fullnode: None,
2100                error_info: error.map(|e| {
2101                    let error_type = String::from(e.clone().as_ref());
2102                    let error_weight = normalize(e);
2103                    (error_weight, error_type)
2104                }),
2105                spam_weight,
2106                timestamp: SystemTime::now(),
2107                method: Some(method_name.to_string()),
2108            })
2109        }
2110        unwrapped_response
2111    }
2112}
2113
2114// TODO: refine error matching here
2115fn normalize(err: SuiError) -> Weight {
2116    match err.as_inner() {
2117        SuiErrorKind::UserInputError {
2118            error: UserInputError::IncorrectUserSignature { .. },
2119        } => Weight::one(),
2120        SuiErrorKind::InvalidSignature { .. }
2121        | SuiErrorKind::SignerSignatureAbsent { .. }
2122        | SuiErrorKind::SignerSignatureNumberMismatch { .. }
2123        | SuiErrorKind::IncorrectSigner { .. }
2124        | SuiErrorKind::UnknownSigner { .. }
2125        | SuiErrorKind::WrongEpoch { .. } => Weight::one(),
2126        _ => Weight::zero(),
2127    }
2128}
2129
2130/// Implements generic pre- and post-processing. Since this is on the critical
2131/// path, any heavy lifting should be done in a separate non-blocking task
2132/// unless it is necessary to override the return value.
2133#[macro_export]
2134macro_rules! handle_with_decoration {
2135    ($self:ident, $func_name:ident, $request:ident, $method_name:expr) => {{
2136        if $self.client_id_source.is_none() {
2137            return $self.$func_name($request).await.map(|(result, _)| result);
2138        }
2139
2140        let client = $self.get_client_ip_addr(&$request, $self.client_id_source.as_ref().unwrap());
2141
2142        // check if either IP is blocked, in which case return early
2143        $self.handle_traffic_req(client.clone()).await?;
2144
2145        // handle traffic tallying
2146        let wrapped_response = $self.$func_name($request).await;
2147        $self.handle_traffic_resp(client, wrapped_response, $method_name)
2148    }};
2149}
2150
2151#[async_trait]
2152impl Validator for ValidatorService {
2153    async fn submit_transaction(
2154        &self,
2155        request: tonic::Request<RawSubmitTxRequest>,
2156    ) -> Result<tonic::Response<RawSubmitTxResponse>, tonic::Status> {
2157        let validator_service = self.clone();
2158
2159        // Spawns a task which handles the transaction. The task will unconditionally continue
2160        // processing in the event that the client connection is dropped.
2161        spawn_monitored_task!(async move {
2162            // NB: traffic tally wrapping handled within the task rather than on task exit
2163            // to prevent an attacker from subverting traffic control by severing the connection
2164            handle_with_decoration!(
2165                validator_service,
2166                handle_submit_transaction_impl,
2167                request,
2168                "submit_transaction"
2169            )
2170        })
2171        .await
2172        .unwrap()
2173    }
2174
2175    async fn wait_for_effects(
2176        &self,
2177        request: tonic::Request<RawWaitForEffectsRequest>,
2178    ) -> Result<tonic::Response<RawWaitForEffectsResponse>, tonic::Status> {
2179        handle_with_decoration!(self, wait_for_effects_impl, request, "wait_for_effects")
2180    }
2181
2182    async fn object_info(
2183        &self,
2184        request: tonic::Request<ObjectInfoRequest>,
2185    ) -> Result<tonic::Response<ObjectInfoResponse>, tonic::Status> {
2186        handle_with_decoration!(self, object_info_impl, request, "object_info")
2187    }
2188
2189    async fn transaction_info(
2190        &self,
2191        request: tonic::Request<TransactionInfoRequest>,
2192    ) -> Result<tonic::Response<TransactionInfoResponse>, tonic::Status> {
2193        handle_with_decoration!(self, transaction_info_impl, request, "transaction_info")
2194    }
2195
2196    async fn checkpoint(
2197        &self,
2198        request: tonic::Request<CheckpointRequest>,
2199    ) -> Result<tonic::Response<CheckpointResponse>, tonic::Status> {
2200        handle_with_decoration!(self, checkpoint_impl, request, "checkpoint")
2201    }
2202
2203    async fn checkpoint_v2(
2204        &self,
2205        request: tonic::Request<CheckpointRequestV2>,
2206    ) -> Result<tonic::Response<CheckpointResponseV2>, tonic::Status> {
2207        handle_with_decoration!(self, checkpoint_v2_impl, request, "checkpoint_v2")
2208    }
2209
2210    async fn get_system_state_object(
2211        &self,
2212        request: tonic::Request<SystemStateRequest>,
2213    ) -> Result<tonic::Response<SuiSystemState>, tonic::Status> {
2214        handle_with_decoration!(
2215            self,
2216            get_system_state_object_impl,
2217            request,
2218            "get_system_state_object"
2219        )
2220    }
2221
2222    async fn validator_health(
2223        &self,
2224        request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
2225    ) -> Result<tonic::Response<sui_types::messages_grpc::RawValidatorHealthResponse>, tonic::Status>
2226    {
2227        handle_with_decoration!(self, validator_health_impl, request, "validator_health")
2228    }
2229}
2230
2231#[cfg(test)]
2232mod inflight_guard_tests {
2233    use super::*;
2234    use prometheus::Registry;
2235
2236    fn make_guard(
2237        inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
2238        cache: Cache<TransactionDigest, Instant>,
2239        metrics: Arc<ValidatorServiceMetrics>,
2240    ) -> InflightTransactionsGuard {
2241        InflightTransactionsGuard {
2242            inflight,
2243            recently_submitted: cache,
2244            window: Duration::from_secs(10),
2245            metrics,
2246            acquired: HashSet::new(),
2247        }
2248    }
2249
2250    #[test]
2251    fn concurrent_acquire_rejects_other_request_and_is_idempotent_for_owner() {
2252        let inflight = Arc::new(Mutex::new(HashSet::new()));
2253        let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2254        let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2255        let digest = TransactionDigest::random();
2256
2257        let mut g1 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2258        let mut g2 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2259
2260        // First handler acquires it.
2261        assert!(matches!(g1.try_acquire(digest), AcquireOutcome::Acquired));
2262        assert_eq!(metrics.inflight_transactions.get(), 1);
2263
2264        // A concurrent handler sees another in-flight owner and is rejected.
2265        assert!(matches!(
2266            g2.try_acquire(digest),
2267            AcquireOutcome::AlreadyAcquiredByAnotherRequest
2268        ));
2269
2270        // The owning handler re-acquiring (epoch-end retry) is NOT a duplicate.
2271        assert!(matches!(
2272            g1.try_acquire(digest),
2273            AcquireOutcome::AlreadyAcquiredByThisRequest
2274        ));
2275    }
2276
2277    #[test]
2278    fn drop_demotes_into_recently_processed_outcome() {
2279        let inflight = Arc::new(Mutex::new(HashSet::new()));
2280        let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2281        let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2282        let digest = TransactionDigest::random();
2283
2284        {
2285            let mut g = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2286            assert!(matches!(g.try_acquire(digest), AcquireOutcome::Acquired));
2287            assert_eq!(inflight.lock().len(), 1);
2288        } // guard dropped here -> remove from set + demote to TTL cache
2289
2290        assert_eq!(
2291            inflight.lock().len(),
2292            0,
2293            "acquired digest must be removed from the in-flight set on drop"
2294        );
2295        assert_eq!(metrics.inflight_transactions.get(), 0);
2296
2297        // Make moka's read view deterministic before asserting the demoted entry.
2298        cache.run_pending_tasks();
2299
2300        // A fresh request now sees the digest as recently processed (tail window).
2301        let mut g_after = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2302        assert!(matches!(
2303            g_after.try_acquire(digest),
2304            AcquireOutcome::RecentlyProcessed { .. }
2305        ));
2306    }
2307}