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