1use 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(®istry)
403 }
404}
405
406#[derive(Clone, Copy)]
409enum UserSubmissionMode {
410 Queue,
412 Pool,
414 Direct,
419}
420
421#[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 recently_submitted: Cache<TransactionDigest, Instant>,
443 recent_submission_window: Duration,
445 inflight_transactions: Arc<Mutex<HashSet<TransactionDigest>>>,
448}
449
450const 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 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 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 transaction.validity_check(&epoch_store.tx_validity_check_context())?;
527
528 let transaction = epoch_store
530 .verify_transaction_require_no_aliases(transaction)?
531 .into_tx();
532
533 self.state
535 .handle_vote_transaction(&epoch_store, transaction)?;
536
537 Ok(())
538 }
539
540 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 transaction.validity_check(&epoch_store.tx_validity_check_context())?;
550
551 self.state.check_system_overload(
553 transaction.data(),
554 self.state.check_system_overload_at_signing(),
555 )?;
556
557 let transaction = epoch_store
559 .verify_transaction_require_no_aliases(transaction)?
560 .into_tx();
561
562 self.state
564 .handle_vote_transaction(&epoch_store, transaction)?;
565
566 Ok(())
567 }
568
569 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 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 let objects = state.get_object_cache_reader().get_objects(&object_ids);
592
593 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 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 let submit_type = SubmitTxType::try_from(request.submit_type).map_err(|e| {
707 SuiErrorKind::GrpcMessageDeserializeError {
708 type_info: "RawSubmitTxRequest.submit_type".to_string(),
709 error: e.to_string(),
710 }
711 })?;
712
713 let is_ping_request = submit_type == SubmitTxType::Ping;
714 if is_ping_request {
715 fp_ensure!(
716 request.transactions.is_empty(),
717 SuiErrorKind::InvalidRequest(format!(
718 "Ping request cannot contain {} transactions",
719 request.transactions.len()
720 ))
721 .into()
722 );
723 } else {
724 fp_ensure!(
726 !request.transactions.is_empty(),
727 SuiErrorKind::InvalidRequest(
728 "At least one transaction needs to be submitted".to_string(),
729 )
730 .into()
731 );
732 }
733
734 let is_soft_bundle_request = submit_type == SubmitTxType::SoftBundle;
739
740 let max_num_transactions = if is_soft_bundle_request {
741 epoch_store.protocol_config().max_soft_bundle_size()
744 } else {
745 epoch_store
747 .protocol_config()
748 .max_num_transactions_in_block()
749 };
750 fp_ensure!(
751 request.transactions.len() <= max_num_transactions as usize,
752 SuiErrorKind::InvalidRequest(format!(
753 "Too many transactions in request: {} vs {}",
754 request.transactions.len(),
755 max_num_transactions
756 ))
757 .into()
758 );
759
760 let mut tx_digests = Vec::with_capacity(request.transactions.len());
762 let mut consensus_transactions = Vec::with_capacity(request.transactions.len());
764 let mut transaction_indexes = Vec::with_capacity(request.transactions.len());
766 let mut results: Vec<Option<SubmitTxResult>> = vec![None; request.transactions.len()];
768 let mut total_size_bytes = 0;
770 let mut has_gasless = false;
772 let mut duplicate_at_admission = false;
776 let mut expected_soft_bundle_gas_price = None;
778 let mut request_digests = HashSet::new();
780
781 let req_type = if is_ping_request {
782 "ping"
783 } else if request.transactions.len() == 1 {
784 "single_transaction"
785 } else if is_soft_bundle_request {
786 "soft_bundle"
787 } else {
788 "batch"
789 };
790
791 let _handle_tx_metrics_guard = metrics
792 .handle_submit_transaction_latency
793 .with_label_values(&[req_type])
794 .start_timer();
795
796 let submit_mode = self.classify_submit_mode(is_ping_request);
797
798 for (idx, tx_bytes) in request.transactions.iter().enumerate() {
799 let transaction = match bcs::from_bytes::<Transaction>(tx_bytes) {
800 Ok(txn) => txn,
801 Err(e) => {
802 return Err(SuiErrorKind::TransactionDeserializationError {
804 error: format!("Failed to deserialize transaction at index {}: {}", idx, e),
805 }
806 .into());
807 }
808 };
809
810 let tx_size = transaction.validity_check(&epoch_store.tx_validity_check_context())?;
812 let tx_digest = *transaction.digest();
813
814 epoch_store.check_self_allowed_proposer(transaction.data().transaction_data())?;
817
818 if !request_digests.insert(tx_digest) {
820 let error: SuiError = SuiErrorKind::UserInputError {
821 error: UserInputError::RepeatedTransactions { digest: tx_digest },
822 }
823 .into();
824 if is_soft_bundle_request {
826 return Err(error);
827 }
828 results[idx] = Some(SubmitTxResult::Rejected { error });
829 continue;
830 }
831
832 if is_soft_bundle_request {
834 let gas_price = transaction.data().transaction_data().gas_price();
835 if let Some(expected) = expected_soft_bundle_gas_price {
836 fp_ensure!(
837 gas_price == expected,
838 SuiErrorKind::UserInputError {
839 error: UserInputError::GasPriceMismatchError {
840 digest: tx_digest,
841 expected,
842 actual: gas_price,
843 }
844 }
845 .into()
846 );
847 } else {
848 expected_soft_bundle_gas_price = Some(gas_price);
849 }
850 }
851
852 let is_gasless = transaction
853 .data()
854 .transaction_data()
855 .is_gasless_transaction();
856
857 if is_gasless {
858 has_gasless = true;
859 metrics
860 .gasless_submission_outcomes
861 .with_label_values(&["attempted"])
862 .inc();
863 }
864
865 let overload_check_res = state.check_system_overload(
866 transaction.data(),
867 state.check_system_overload_at_signing(),
868 );
869 if let Err(error) = overload_check_res {
870 metrics
871 .num_rejected_tx_during_overload
872 .with_label_values(&[error.as_ref()])
873 .inc();
874 if is_gasless {
875 metrics
876 .gasless_submission_outcomes
877 .with_label_values(&["rejected_overload"])
878 .inc();
879 }
880 results[idx] = Some(SubmitTxResult::Rejected { error });
881 continue;
882 }
883
884 if matches!(submit_mode, UserSubmissionMode::Direct)
889 && !matches!(&self.user_submission_path, UserSubmissionPath::Pool(_))
890 && let Err(error) = self.consensus_adapter.check_consensus_overload()
891 {
892 state.update_overload_metrics("consensus");
893 metrics
894 .num_rejected_tx_during_overload
895 .with_label_values(&[error.as_ref()])
896 .inc();
897 if is_gasless {
898 metrics
899 .gasless_submission_outcomes
900 .with_label_values(&["rejected_overload"])
901 .inc();
902 }
903 results[idx] = Some(SubmitTxResult::Rejected { error });
904 continue;
905 }
906
907 if is_gasless
908 && !self
909 .gasless_limiter
910 .try_acquire(epoch_store.protocol_config())
911 {
912 metrics.gasless_rate_limited_count.inc();
913 metrics
914 .gasless_submission_outcomes
915 .with_label_values(&["rejected_rate_limited"])
916 .inc();
917 results[idx] = Some(SubmitTxResult::Rejected {
918 error: SuiErrorKind::ValidatorOverloadedRetryAfter {
919 retry_after_secs: 1,
920 }
921 .into(),
922 });
923 continue;
924 }
925
926 let verified_transaction = {
928 let _metrics_guard = metrics.tx_verification_latency.start_timer();
929 if epoch_store.protocol_config().address_aliases() {
930 match epoch_store.verify_transaction_with_current_aliases(transaction) {
931 Ok(tx) => tx,
932 Err(e) => {
933 metrics.signature_errors.inc();
934 return Err(e);
935 }
936 }
937 } else {
938 match epoch_store.verify_transaction_require_no_aliases(transaction) {
939 Ok(tx) => tx,
940 Err(e) => {
941 metrics.signature_errors.inc();
942 return Err(e);
943 }
944 }
945 }
946 };
947
948 debug!(
949 ?tx_digest,
950 "handle_submit_transaction: verified transaction"
951 );
952
953 if let Some(effects) = state
956 .get_transaction_cache_reader()
957 .get_executed_effects(&tx_digest)
958 {
959 let effects_digest = effects.digest();
960 if let Err(error) = state.check_effects_against_previously_signed(
961 &epoch_store,
962 &tx_digest,
963 &effects_digest,
964 "submit_transaction",
965 ) {
966 results[idx] = Some(SubmitTxResult::Rejected { error });
967 continue;
968 }
969 if let Ok(executed_data) = self.complete_executed_data(effects).await {
970 let executed_result = SubmitTxResult::Executed {
971 effects_digest,
972 details: Some(executed_data),
973 };
974 results[idx] = Some(executed_result);
975 debug!(?tx_digest, "handle_submit_transaction: already executed");
976 continue;
977 }
978 }
979
980 if self
981 .state
982 .get_transaction_cache_reader()
983 .transaction_executed_in_last_epoch(&tx_digest, epoch_store.epoch())
984 {
985 results[idx] = Some(SubmitTxResult::Rejected {
986 error: UserInputError::TransactionAlreadyExecuted { digest: tx_digest }.into(),
987 });
988 debug!(
989 ?tx_digest,
990 "handle_submit_transaction: transaction already executed in previous epoch"
991 );
992 continue;
993 }
994
995 let consensus_key = SequencedConsensusTransactionKey::External(
999 ConsensusTransactionKey::Certificate(tx_digest),
1000 );
1001 if epoch_store.is_consensus_message_processed(&consensus_key)? {
1002 if let Ok(input_objects) = verified_transaction
1014 .tx()
1015 .data()
1016 .transaction_data()
1017 .input_objects()
1018 {
1019 let immutable_object_ids = self
1020 .collect_immutable_object_ids(verified_transaction.tx(), state)
1021 .await?;
1022 let owned_object_refs: Vec<_> = input_objects
1023 .iter()
1024 .filter_map(|obj| match obj {
1025 InputObjectKind::ImmOrOwnedMoveObject(obj_ref)
1026 if !immutable_object_ids.contains(&obj_ref.0) =>
1027 {
1028 Some(*obj_ref)
1029 }
1030 _ => None,
1031 })
1032 .collect();
1033 let existing_locks =
1034 epoch_store.get_owned_object_locks_map(&owned_object_refs)?;
1035 if let Err(error) = epoch_store.try_acquire_owned_object_locks_post_consensus(
1036 &owned_object_refs,
1037 tx_digest,
1038 &HashMap::new(),
1039 &existing_locks,
1040 ) {
1041 debug!(
1042 ?tx_digest,
1043 "handle_submit_transaction: processed transaction rejected on lock conflict: {error}"
1044 );
1045 metrics
1046 .submission_rejected_transactions
1047 .with_label_values(&[error.to_variant_name()])
1048 .inc();
1049 results[idx] = Some(SubmitTxResult::Rejected { error });
1050 continue;
1051 }
1052 }
1053 if let Err(error) =
1056 state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone())
1057 {
1058 if let Some(effects) = state
1061 .get_transaction_cache_reader()
1062 .get_executed_effects(&tx_digest)
1063 {
1064 let effects_digest = effects.digest();
1065 if let Err(error) = state.check_effects_against_previously_signed(
1066 &epoch_store,
1067 &tx_digest,
1068 &effects_digest,
1069 "submit_transaction",
1070 ) {
1071 results[idx] = Some(SubmitTxResult::Rejected { error });
1072 continue;
1073 }
1074 if let Ok(executed_data) = self.complete_executed_data(effects).await {
1075 results[idx] = Some(SubmitTxResult::Executed {
1076 effects_digest,
1077 details: Some(executed_data),
1078 });
1079 continue;
1080 }
1081 }
1082 debug!(
1083 ?tx_digest,
1084 "handle_submit_transaction: processed transaction rejected on revalidation: {error}"
1085 );
1086 metrics
1087 .submission_rejected_transactions
1088 .with_label_values(&[error.to_variant_name()])
1089 .inc();
1090 results[idx] = Some(SubmitTxResult::Rejected { error });
1091 continue;
1092 }
1093 metrics
1097 .submission_suppressed_already_processed
1098 .with_label_values(&[req_type])
1099 .inc();
1100 results[idx] = Some(SubmitTxResult::Rejected {
1101 error: SuiErrorKind::TransactionProcessing {
1102 digest: tx_digest,
1103 status: "consensus message processed".to_string(),
1104 }
1105 .into(),
1106 });
1107 debug!(
1108 ?tx_digest,
1109 "handle_submit_transaction: consensus message already processed"
1110 );
1111 continue;
1112 }
1113
1114 match inflight_guard.try_acquire(tx_digest) {
1117 AcquireOutcome::Acquired | AcquireOutcome::AlreadyAcquiredByThisRequest => {
1118 }
1120 AcquireOutcome::AlreadyAcquiredByAnotherRequest => {
1121 metrics
1122 .submission_suppressed_inflight
1123 .with_label_values(&[req_type])
1124 .inc();
1125 results[idx] = Some(SubmitTxResult::Rejected {
1126 error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1127 });
1128 debug!(
1129 ?tx_digest,
1130 "handle_submit_transaction: concurrent submission in progress"
1131 );
1132 continue;
1133 }
1134 AcquireOutcome::RecentlyProcessed { since } => {
1135 metrics
1136 .submission_suppressed_recently_submitted
1137 .with_label_values(&[req_type])
1138 .inc();
1139 metrics
1140 .recently_submitted_resubmission_interval
1141 .observe(since.as_secs_f64());
1142 results[idx] = Some(SubmitTxResult::Rejected {
1143 error: SuiErrorKind::TransactionSubmitted { digest: tx_digest }.into(),
1144 });
1145 debug!(?tx_digest, "handle_submit_transaction: recently processed");
1146 continue;
1147 }
1148 }
1149
1150 debug!(
1151 ?tx_digest,
1152 "handle_submit_transaction: waiting for fastpath dependency objects"
1153 );
1154 if !state
1155 .wait_for_fastpath_dependency_objects(
1156 verified_transaction.tx(),
1157 epoch_store.epoch(),
1158 )
1159 .await?
1160 {
1161 debug!(
1162 ?tx_digest,
1163 "fastpath input objects are still unavailable after waiting"
1164 );
1165 }
1166
1167 match state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone()) {
1168 Ok(_) => { }
1169 Err(e) => {
1170 if let Some(effects) = state
1173 .get_transaction_cache_reader()
1174 .get_executed_effects(&tx_digest)
1175 {
1176 let effects_digest = effects.digest();
1177 if let Err(error) = state.check_effects_against_previously_signed(
1178 &epoch_store,
1179 &tx_digest,
1180 &effects_digest,
1181 "submit_transaction",
1182 ) {
1183 results[idx] = Some(SubmitTxResult::Rejected { error });
1184 continue;
1185 }
1186 if let Ok(executed_data) = self.complete_executed_data(effects).await {
1187 let executed_result = SubmitTxResult::Executed {
1188 effects_digest,
1189 details: Some(executed_data),
1190 };
1191 results[idx] = Some(executed_result);
1192 continue;
1193 }
1194 }
1195
1196 debug!(?tx_digest, "Transaction rejected during submission: {e}");
1198 metrics
1199 .submission_rejected_transactions
1200 .with_label_values(&[e.to_variant_name()])
1201 .inc();
1202 results[idx] = Some(SubmitTxResult::Rejected { error: e });
1203 continue;
1204 }
1205 }
1206
1207 let mut claims = vec![];
1209
1210 let immutable_object_ids = self
1211 .collect_immutable_object_ids(verified_transaction.tx(), state)
1212 .await?;
1213 if !immutable_object_ids.is_empty() {
1214 claims.push(TransactionClaim::ImmutableInputObjects(
1215 immutable_object_ids,
1216 ));
1217 }
1218
1219 let (tx, aliases) = verified_transaction.into_inner();
1220 if epoch_store.protocol_config().address_aliases() {
1221 if epoch_store
1222 .protocol_config()
1223 .fix_checkpoint_signature_mapping()
1224 {
1225 claims.push(TransactionClaim::AddressAliasesV2(aliases));
1226 } else {
1227 let v1_aliases: Vec<_> = tx
1228 .data()
1229 .intent_message()
1230 .value
1231 .required_signers()
1232 .into_iter()
1233 .zip_eq(aliases.into_iter().map(|(_, seq)| seq))
1234 .collect();
1235 #[allow(deprecated)]
1236 claims.push(TransactionClaim::AddressAliases(
1237 nonempty::NonEmpty::from_vec(v1_aliases)
1238 .expect("must have at least one required_signer"),
1239 ));
1240 }
1241 }
1242
1243 let tx_with_claims = TransactionWithClaims::new(tx.into(), claims);
1244
1245 consensus_transactions.push(ConsensusTransaction::new_user_transaction_v2_message(
1246 &state.name,
1247 tx_with_claims,
1248 ));
1249 if is_gasless {
1250 metrics
1251 .gasless_submission_outcomes
1252 .with_label_values(&["submitted"])
1253 .inc();
1254 }
1255
1256 transaction_indexes.push(idx);
1257 tx_digests.push(tx_digest);
1258 total_size_bytes += tx_size;
1259 }
1260
1261 if consensus_transactions.is_empty() && !is_ping_request {
1262 let spam_weight = Self::request_spam_weight(
1263 &results,
1264 has_gasless,
1265 duplicate_at_admission,
1266 is_ping_request,
1267 );
1268 let response = Self::try_from_submit_tx_response(results)?;
1269 return Ok((response, spam_weight));
1270 }
1271
1272 let max_transaction_bytes = if is_soft_bundle_request {
1276 epoch_store
1277 .protocol_config()
1278 .consensus_max_transactions_in_block_bytes()
1279 / 2
1280 } else {
1281 epoch_store
1282 .protocol_config()
1283 .consensus_max_transactions_in_block_bytes()
1284 };
1285 fp_ensure!(
1286 total_size_bytes <= max_transaction_bytes as usize,
1287 SuiErrorKind::UserInputError {
1288 error: UserInputError::TotalTransactionSizeTooLargeInBatch {
1289 size: total_size_bytes,
1290 limit: max_transaction_bytes,
1291 },
1292 }
1293 .into()
1294 );
1295
1296 metrics
1297 .handle_submit_transaction_bytes
1298 .with_label_values(&[req_type])
1299 .observe(total_size_bytes as f64);
1300 metrics
1301 .handle_submit_transaction_batch_size
1302 .with_label_values(&[req_type])
1303 .observe(consensus_transactions.len() as f64);
1304
1305 let _latency_metric_guard = metrics
1306 .handle_submit_transaction_consensus_latency
1307 .with_label_values(&[req_type])
1308 .start_timer();
1309
1310 if is_soft_bundle_request {
1311 assert!(
1314 !consensus_transactions.is_empty(),
1315 "A valid soft bundle must have at least one transaction"
1316 );
1317 }
1318
1319 let tx_groups: Vec<Vec<ConsensusTransaction>> = if is_soft_bundle_request || is_ping_request
1322 {
1323 vec![consensus_transactions]
1324 } else {
1325 consensus_transactions
1326 .into_iter()
1327 .map(|t| vec![t])
1328 .collect()
1329 };
1330
1331 let group_tx_meta = if is_soft_bundle_request {
1336 vec![
1337 transaction_indexes
1338 .into_iter()
1339 .zip_eq(tx_digests)
1340 .collect::<Vec<_>>(),
1341 ]
1342 } else {
1343 transaction_indexes
1344 .into_iter()
1345 .zip_eq(tx_digests)
1346 .map(|pair| vec![pair])
1347 .collect::<Vec<_>>()
1348 };
1349
1350 let group_results = match submit_mode {
1355 UserSubmissionMode::Direct => {
1356 let futures = tx_groups.into_iter().map(|txns| {
1357 debug!(
1358 "handle_submit_transaction: submitting consensus transactions ({}): {}",
1359 req_type,
1360 txns.iter().map(|t| t.local_display()).join(", ")
1361 );
1362 self.consensus_adapter.submit_and_get_positions(
1363 txns,
1364 &epoch_store,
1365 submitter_client_addr,
1366 )
1367 });
1368 future::join_all(futures).await
1369 }
1370 UserSubmissionMode::Queue => {
1371 let UserSubmissionPath::AdmissionQueue(context) = &self.user_submission_path else {
1372 debug_fatal!("queue mode requires an admission queue");
1373 return Err(SuiErrorKind::GenericAuthorityError {
1374 error: "queue mode requires an admission queue".to_string(),
1375 }
1376 .into());
1377 };
1378 let aq = context.load();
1379 let mut receivers = Vec::with_capacity(tx_groups.len());
1380 for txns in tx_groups {
1381 let gas_price = Self::extract_gas_price(&txns);
1382 let (rx, newly_inserted) = aq
1383 .try_insert(gas_price, txns, submitter_client_addr)
1384 .await?;
1385 if !newly_inserted {
1386 duplicate_at_admission = true;
1389 }
1390 receivers.push(rx);
1391 }
1392 future::join_all(receivers.into_iter().map(|rx| async move {
1393 match rx.await {
1394 Ok(result) => result.map_err(SuiError::from),
1395 Err(_) => Err(SuiError::from(
1396 SuiErrorKind::TooManyTransactionsPendingConsensus,
1397 )),
1398 }
1399 }))
1400 .await
1401 }
1402 UserSubmissionMode::Pool => {
1403 let UserSubmissionPath::Pool(context) = &self.user_submission_path else {
1404 debug_fatal!("pool mode requires a transaction pool");
1405 return Err(SuiErrorKind::GenericAuthorityError {
1406 error: "pool mode requires a transaction pool".to_string(),
1407 }
1408 .into());
1409 };
1410 {
1411 let reconfiguration_lock = epoch_store.get_reconfig_state_read_lock_guard();
1412 if !reconfiguration_lock.should_accept_user_certs() {
1413 context
1414 .adapter_metrics()
1415 .num_rejected_cert_in_epoch_boundary
1416 .inc();
1417 return Err(SuiErrorKind::ValidatorHaltedAtEpochEnd.into());
1418 }
1419 }
1420
1421 let mut receivers = Vec::with_capacity(tx_groups.len());
1422 for txns in tx_groups {
1423 epoch_store.record_submitted_user_transactions(&txns, submitter_client_addr);
1427 let gas_price = Self::extract_gas_price(&txns);
1428 let result = context
1429 .try_insert(epoch_store.epoch(), gas_price, txns)
1430 .await;
1431 if let Ok((_, false)) = &result {
1432 duplicate_at_admission = true;
1435 }
1436 receivers.push(result.map(|(receiver, _)| receiver));
1437 }
1438 let halted_rejections = context
1439 .adapter_metrics()
1440 .num_rejected_cert_in_epoch_boundary
1441 .clone();
1442 future::join_all(receivers.into_iter().map(|receiver| {
1443 let halted_rejections = halted_rejections.clone();
1444 async move {
1445 let result = match receiver {
1446 Ok(receiver) => receiver.await.unwrap_or_else(|_| {
1447 Err(SuiErrorKind::TooManyTransactionsPendingConsensus.into())
1448 }),
1449 Err(error) => Err(error),
1450 };
1451 if let Err(error) = &result
1452 && matches!(error.as_inner(), SuiErrorKind::ValidatorHaltedAtEpochEnd)
1453 {
1454 halted_rejections.inc();
1455 }
1456 result
1457 }
1458 }))
1459 .await
1460 }
1461 };
1462
1463 if is_ping_request {
1464 let consensus_positions = group_results
1466 .into_iter()
1467 .next()
1468 .expect("Ping request must have exactly one submission group")?;
1469 assert_eq!(consensus_positions.len(), 1);
1470 results.push(Some(SubmitTxResult::Submitted {
1471 consensus_position: consensus_positions[0],
1472 }));
1473 } else {
1474 for (group_result, txns_meta) in group_results.into_iter().zip_debug_eq(group_tx_meta) {
1475 match group_result {
1476 Ok(consensus_positions) => {
1477 for ((idx, tx_digest), consensus_position) in
1478 txns_meta.into_iter().zip_debug_eq(consensus_positions)
1479 {
1480 debug!(
1481 ?tx_digest,
1482 "handle_submit_transaction: submitted consensus transaction at {}",
1483 consensus_position,
1484 );
1485 results[idx] = Some(SubmitTxResult::Submitted { consensus_position });
1486 }
1487 }
1488 Err(err) => {
1491 let SuiErrorKind::TransactionProcessing { status, .. } =
1492 err.as_inner().clone()
1493 else {
1494 return Err(err);
1495 };
1496 for (idx, tx_digest) in txns_meta {
1498 debug!(
1499 ?tx_digest,
1500 "handle_submit_transaction: transaction already processing: {err}"
1501 );
1502 metrics
1506 .submission_suppressed_already_processed
1507 .with_label_values(&[req_type])
1508 .inc();
1509 results[idx] = Some(SubmitTxResult::Rejected {
1510 error: SuiErrorKind::TransactionProcessing {
1511 digest: tx_digest,
1512 status: status.clone(),
1513 }
1514 .into(),
1515 });
1516 }
1517 }
1518 }
1519 }
1520 }
1521
1522 let spam_weight = Self::request_spam_weight(
1523 &results,
1524 has_gasless,
1525 duplicate_at_admission,
1526 is_ping_request,
1527 );
1528 let response = Self::try_from_submit_tx_response(results)?;
1529 Ok((response, spam_weight))
1530 }
1531
1532 fn request_spam_weight(
1535 results: &[Option<SubmitTxResult>],
1536 has_gasless: bool,
1537 duplicate_at_admission: bool,
1538 is_ping: bool,
1539 ) -> Weight {
1540 if is_ping || has_gasless || duplicate_at_admission {
1541 return Weight::one();
1542 }
1543 for result in results {
1544 let Some(result) = result else {
1545 debug_fatal!("transaction outcome unset when computing spam weight");
1549 return Weight::one();
1550 };
1551 if Self::submission_spam_weight(result) == Weight::one() {
1552 return Weight::one();
1553 }
1554 }
1555 Weight::zero()
1556 }
1557
1558 fn submission_spam_weight(result: &SubmitTxResult) -> Weight {
1559 match result {
1560 SubmitTxResult::Submitted { .. } => Weight::zero(),
1561 SubmitTxResult::Executed { .. } | SubmitTxResult::Rejected { .. } => Weight::one(),
1563 }
1564 }
1565
1566 fn try_from_submit_tx_response(
1567 results: Vec<Option<SubmitTxResult>>,
1568 ) -> Result<RawSubmitTxResponse, SuiError> {
1569 let mut raw_results = Vec::new();
1570 for (i, result) in results.into_iter().enumerate() {
1571 let result = result.ok_or_else(|| SuiErrorKind::GenericAuthorityError {
1572 error: format!("Missing transaction result at {}", i),
1573 })?;
1574 let raw_result = result.try_into()?;
1575 raw_results.push(raw_result);
1576 }
1577 Ok(RawSubmitTxResponse {
1578 results: raw_results,
1579 })
1580 }
1581
1582 fn extract_gas_price(transactions: &[ConsensusTransaction]) -> u64 {
1585 use sui_types::messages_consensus::ConsensusTransactionKind;
1586 transactions
1587 .iter()
1588 .filter_map(|tx| match &tx.kind {
1589 ConsensusTransactionKind::CertifiedTransaction(cert) => Some(cert.gas_price()),
1590 ConsensusTransactionKind::UserTransaction(t) => {
1591 Some(t.data().transaction_data().gas_price())
1592 }
1593 ConsensusTransactionKind::UserTransactionV2(t) => {
1594 Some(t.tx().data().transaction_data().gas_price())
1595 }
1596 _ => None,
1597 })
1598 .min()
1599 .unwrap_or(0)
1600 }
1601
1602 fn classify_submit_mode(&self, is_ping_request: bool) -> UserSubmissionMode {
1603 if is_ping_request {
1606 return UserSubmissionMode::Direct;
1607 }
1608
1609 match &self.user_submission_path {
1610 UserSubmissionPath::Direct => UserSubmissionMode::Direct,
1611 UserSubmissionPath::Pool(_) => UserSubmissionMode::Pool,
1612 UserSubmissionPath::AdmissionQueue(context) => {
1613 if context.load().failover_tripped() {
1616 UserSubmissionMode::Direct
1617 } else {
1618 UserSubmissionMode::Queue
1619 }
1620 }
1621 }
1622 }
1623
1624 async fn collect_effects_data(
1625 &self,
1626 effects: &TransactionEffects,
1627 include_events: bool,
1628 include_input_objects: bool,
1629 include_output_objects: bool,
1630 ) -> SuiResult<(Option<TransactionEvents>, Vec<Object>, Vec<Object>)> {
1631 let events = if include_events && effects.events_digest().is_some() {
1632 Some(
1633 self.state
1634 .get_transaction_events(effects.transaction_digest())?,
1635 )
1636 } else {
1637 None
1638 };
1639
1640 let input_objects = if include_input_objects {
1641 self.state.get_transaction_input_objects(effects)?
1642 } else {
1643 vec![]
1644 };
1645
1646 let output_objects = if include_output_objects {
1647 self.state.get_transaction_output_objects(effects)?
1648 } else {
1649 vec![]
1650 };
1651
1652 Ok((events, input_objects, output_objects))
1653 }
1654}
1655
1656type WrappedServiceResponse<T> = Result<(tonic::Response<T>, Weight), tonic::Status>;
1657
1658struct InflightTransactionsGuard {
1663 inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
1665 recently_submitted: Cache<TransactionDigest, Instant>,
1666 window: Duration,
1667 metrics: Arc<ValidatorServiceMetrics>,
1668 acquired: HashSet<TransactionDigest>,
1670}
1671
1672enum AcquireOutcome {
1673 Acquired,
1675 AlreadyAcquiredByThisRequest,
1677 AlreadyAcquiredByAnotherRequest,
1679 RecentlyProcessed { since: Duration },
1681}
1682
1683impl InflightTransactionsGuard {
1684 fn new(service: &ValidatorService) -> Self {
1685 Self {
1686 inflight: service.inflight_transactions.clone(),
1687 recently_submitted: service.recently_submitted.clone(),
1688 window: service.recent_submission_window,
1689 metrics: service.metrics.clone(),
1690 acquired: HashSet::new(),
1691 }
1692 }
1693
1694 fn try_acquire(&mut self, digest: TransactionDigest) -> AcquireOutcome {
1695 if self.acquired.contains(&digest) {
1697 return AcquireOutcome::AlreadyAcquiredByThisRequest;
1698 }
1699
1700 if let Some(outcome) = self.recently_processed_outcome(digest) {
1702 return outcome;
1703 }
1704
1705 {
1707 let mut set = self.inflight.lock();
1708 if !set.insert(digest) {
1710 return AcquireOutcome::AlreadyAcquiredByAnotherRequest;
1711 }
1712 self.metrics.inflight_transactions.set(set.len() as i64);
1713 }
1714
1715 if let Some(outcome) = self.recently_processed_outcome(digest) {
1718 let mut set = self.inflight.lock();
1719 set.remove(&digest);
1720 self.metrics.inflight_transactions.set(set.len() as i64);
1721 return outcome;
1722 }
1723
1724 self.acquired.insert(digest);
1725 AcquireOutcome::Acquired
1726 }
1727
1728 fn recently_processed_outcome(&self, digest: TransactionDigest) -> Option<AcquireOutcome> {
1729 let recorded_at = self.recently_submitted.get(&digest)?;
1730 let since = recorded_at.elapsed();
1731 (since < self.window).then_some(AcquireOutcome::RecentlyProcessed { since })
1732 }
1733}
1734
1735impl Drop for InflightTransactionsGuard {
1736 fn drop(&mut self) {
1737 if self.acquired.is_empty() {
1738 return;
1739 }
1740 let now = Instant::now();
1743 for digest in &self.acquired {
1744 self.recently_submitted.insert(*digest, now);
1745 }
1746 {
1747 let mut set = self.inflight.lock();
1748 for digest in &self.acquired {
1749 set.remove(digest);
1750 }
1751 self.metrics.inflight_transactions.set(set.len() as i64);
1752 }
1753 self.metrics
1754 .recently_submitted_cache_size
1755 .set(self.recently_submitted.entry_count() as i64);
1756 }
1757}
1758
1759impl ValidatorService {
1760 async fn handle_submit_transaction_impl(
1761 &self,
1762 request: tonic::Request<RawSubmitTxRequest>,
1763 ) -> WrappedServiceResponse<RawSubmitTxResponse> {
1764 self.handle_submit_transaction(request).await
1765 }
1766
1767 async fn wait_for_effects_impl(
1768 &self,
1769 request: tonic::Request<RawWaitForEffectsRequest>,
1770 ) -> WrappedServiceResponse<RawWaitForEffectsResponse> {
1771 let request: WaitForEffectsRequest = request.into_inner().try_into()?;
1772 let epoch_store = self.state.load_epoch_store_one_call_per_task();
1773 let response = timeout(
1774 Duration::from_secs(20),
1776 epoch_store
1777 .within_alive_epoch(self.wait_for_effects_response(request, &epoch_store))
1778 .map_err(|_| SuiErrorKind::EpochEnded(epoch_store.epoch())),
1779 )
1780 .await
1781 .map_err(|_| tonic::Status::internal("Timeout waiting for effects"))???
1782 .try_into()?;
1783 Ok((tonic::Response::new(response), Weight::zero()))
1784 }
1785
1786 #[instrument(name= "ValidatorService::wait_for_effects_response", level = "debug", skip_all, fields(consensus_position = ?request.consensus_position))]
1787 async fn wait_for_effects_response(
1788 &self,
1789 request: WaitForEffectsRequest,
1790 epoch_store: &Arc<AuthorityPerEpochStore>,
1791 ) -> SuiResult<WaitForEffectsResponse> {
1792 if request.ping_type.is_some() {
1793 return timeout(
1794 Duration::from_secs(10),
1795 self.ping_response(request, epoch_store),
1796 )
1797 .await
1798 .map_err(|_| SuiErrorKind::TimeoutError)?;
1799 }
1800
1801 let Some(tx_digest) = request.transaction_digest else {
1802 return Err(SuiErrorKind::InvalidRequest(
1803 "Transaction digest is required for wait for effects requests".to_string(),
1804 )
1805 .into());
1806 };
1807 let tx_digests = [tx_digest];
1808
1809 let consensus_status_future = async {
1813 let consensus_position = match request.consensus_position {
1814 Some(pos) => pos,
1815 None => return futures::future::pending().await,
1816 };
1817 let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1818 consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1819 match consensus_tx_status_cache
1820 .notify_read_transaction_status(consensus_position)
1821 .await
1822 {
1823 NotifyReadConsensusTxStatusResult::Status(
1824 ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped,
1825 ) => Ok(WaitForEffectsResponse::Rejected {
1826 error: epoch_store.get_rejection_vote_reason(consensus_position),
1827 }),
1828 NotifyReadConsensusTxStatusResult::Status(ConsensusTxStatus::Finalized) => {
1829 futures::future::pending().await
1831 }
1832 NotifyReadConsensusTxStatusResult::Expired(round) => {
1833 Ok(WaitForEffectsResponse::Expired {
1834 epoch: epoch_store.epoch(),
1835 round: Some(round),
1836 })
1837 }
1838 }
1839 };
1840
1841 tokio::select! {
1842 effects_result = self.state
1843 .get_transaction_cache_reader()
1844 .notify_read_executed_effects_may_fail(
1845 "AuthorityServer::wait_for_effects::notify_read_executed_effects_finalized",
1846 &tx_digests,
1847 ) => {
1848 let effects = effects_result?.pop().unwrap();
1849 let effects_digest = effects.digest();
1850 self.state.check_effects_against_previously_signed(
1851 epoch_store,
1852 &tx_digest,
1853 &effects_digest,
1854 "wait_for_effects",
1855 )?;
1856 let details = if request.include_details {
1857 Some(self.complete_executed_data(effects).await?)
1858 } else {
1859 None
1860 };
1861 Ok(WaitForEffectsResponse::Executed {
1862 effects_digest,
1863 details,
1864 })
1865 }
1866 status_response = consensus_status_future => {
1867 status_response
1868 }
1869 }
1870 }
1871
1872 #[instrument(level = "error", skip_all, err(level = "debug"))]
1873 async fn ping_response(
1874 &self,
1875 request: WaitForEffectsRequest,
1876 epoch_store: &Arc<AuthorityPerEpochStore>,
1877 ) -> SuiResult<WaitForEffectsResponse> {
1878 let consensus_tx_status_cache = &epoch_store.consensus_tx_status_cache;
1879
1880 let Some(consensus_position) = request.consensus_position else {
1881 return Err(SuiErrorKind::InvalidRequest(
1882 "Consensus position is required for Ping requests".to_string(),
1883 )
1884 .into());
1885 };
1886
1887 let Some(ping) = request.ping_type else {
1889 return Err(SuiErrorKind::InvalidRequest(
1890 "Ping type is required for ping requests".to_string(),
1891 )
1892 .into());
1893 };
1894
1895 let _metrics_guard = self
1896 .metrics
1897 .handle_wait_for_effects_ping_latency
1898 .with_label_values(&[ping.as_str()])
1899 .start_timer();
1900
1901 consensus_tx_status_cache.check_position_too_ahead(&consensus_position)?;
1902
1903 let details = if request.include_details {
1904 Some(Box::new(ExecutedData::default()))
1905 } else {
1906 None
1907 };
1908
1909 let status = consensus_tx_status_cache
1910 .notify_read_transaction_status(consensus_position)
1911 .await;
1912 match status {
1913 NotifyReadConsensusTxStatusResult::Status(status) => match status {
1914 ConsensusTxStatus::Rejected | ConsensusTxStatus::Dropped => {
1915 Ok(WaitForEffectsResponse::Rejected {
1916 error: epoch_store.get_rejection_vote_reason(consensus_position),
1917 })
1918 }
1919 ConsensusTxStatus::Finalized => Ok(WaitForEffectsResponse::Executed {
1920 effects_digest: TransactionEffectsDigest::ZERO,
1921 details,
1922 }),
1923 },
1924 NotifyReadConsensusTxStatusResult::Expired(round) => {
1925 Ok(WaitForEffectsResponse::Expired {
1926 epoch: epoch_store.epoch(),
1927 round: Some(round),
1928 })
1929 }
1930 }
1931 }
1932
1933 async fn complete_executed_data(
1934 &self,
1935 effects: TransactionEffects,
1936 ) -> SuiResult<Box<ExecutedData>> {
1937 let (events, input_objects, output_objects) = self
1938 .collect_effects_data(
1939 &effects, true, true,
1940 true,
1941 )
1942 .await?;
1943 Ok(Box::new(ExecutedData {
1944 effects,
1945 events,
1946 input_objects,
1947 output_objects,
1948 }))
1949 }
1950
1951 async fn object_info_impl(
1952 &self,
1953 request: tonic::Request<ObjectInfoRequest>,
1954 ) -> WrappedServiceResponse<ObjectInfoResponse> {
1955 let request = request.into_inner();
1956 let response = self.state.handle_object_info_request(request).await?;
1957 Ok((tonic::Response::new(response), Weight::one()))
1958 }
1959
1960 async fn transaction_info_impl(
1961 &self,
1962 request: tonic::Request<TransactionInfoRequest>,
1963 ) -> WrappedServiceResponse<TransactionInfoResponse> {
1964 let request = request.into_inner();
1965 let response = self.state.handle_transaction_info_request(request).await?;
1966 Ok((tonic::Response::new(response), Weight::one()))
1967 }
1968
1969 async fn checkpoint_impl(
1970 &self,
1971 request: tonic::Request<CheckpointRequest>,
1972 ) -> WrappedServiceResponse<CheckpointResponse> {
1973 let request = request.into_inner();
1974 let response = self.state.handle_checkpoint_request(&request)?;
1975 Ok((tonic::Response::new(response), Weight::one()))
1976 }
1977
1978 async fn checkpoint_v2_impl(
1979 &self,
1980 request: tonic::Request<CheckpointRequestV2>,
1981 ) -> WrappedServiceResponse<CheckpointResponseV2> {
1982 let request = request.into_inner();
1983 let response = self.state.handle_checkpoint_request_v2(&request)?;
1984 Ok((tonic::Response::new(response), Weight::one()))
1985 }
1986
1987 async fn get_system_state_object_impl(
1988 &self,
1989 _request: tonic::Request<SystemStateRequest>,
1990 ) -> WrappedServiceResponse<SuiSystemState> {
1991 let response = self
1992 .state
1993 .get_object_cache_reader()
1994 .get_sui_system_state_object_unsafe()?;
1995 Ok((tonic::Response::new(response), Weight::one()))
1996 }
1997
1998 async fn validator_health_impl(
1999 &self,
2000 _request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
2001 ) -> WrappedServiceResponse<sui_types::messages_grpc::RawValidatorHealthResponse> {
2002 let state = &self.state;
2003
2004 let epoch_store = state.load_epoch_store_one_call_per_task();
2006
2007 let num_inflight_execution_transactions =
2009 state.execution_scheduler().num_pending_certificates() as u64;
2010
2011 let num_inflight_consensus_transactions =
2013 self.consensus_adapter.num_inflight_transactions();
2014
2015 let last_committed_leader_round = epoch_store
2017 .consensus_tx_status_cache
2018 .get_last_committed_leader_round()
2019 .unwrap_or(0);
2020
2021 let last_locally_built_checkpoint = epoch_store
2023 .last_built_checkpoint_summary()
2024 .ok()
2025 .flatten()
2026 .map(|(_, summary)| summary.sequence_number)
2027 .unwrap_or(0);
2028
2029 let typed_response = sui_types::messages_grpc::ValidatorHealthResponse {
2030 num_inflight_consensus_transactions,
2031 num_inflight_execution_transactions,
2032 last_locally_built_checkpoint,
2033 last_committed_leader_round,
2034 };
2035
2036 let raw_response = typed_response
2037 .try_into()
2038 .map_err(|e: sui_types::error::SuiError| {
2039 tonic::Status::internal(format!("Failed to serialize health response: {}", e))
2040 })?;
2041
2042 Ok((tonic::Response::new(raw_response), Weight::one()))
2043 }
2044
2045 fn get_client_ip_addr<T>(
2046 &self,
2047 request: &tonic::Request<T>,
2048 source: &ClientIdSource,
2049 ) -> Option<IpAddr> {
2050 let forwarded_header = request.metadata().get_all("x-forwarded-for").iter().next();
2051
2052 if let Some(header) = forwarded_header {
2053 let num_hops = header
2054 .to_str()
2055 .map(|h| h.split(',').count().saturating_sub(1))
2056 .unwrap_or(0);
2057
2058 self.metrics.x_forwarded_for_num_hops.set(num_hops as f64);
2059 }
2060
2061 match source {
2062 ClientIdSource::SocketAddr => {
2063 let socket_addr: Option<SocketAddr> = request.remote_addr();
2064
2065 if let Some(socket_addr) = socket_addr {
2071 Some(socket_addr.ip())
2072 } else {
2073 if cfg!(msim) {
2074 } else if cfg!(test) {
2076 panic!("Failed to get remote address from request");
2077 } else {
2078 self.metrics.connection_ip_not_found.inc();
2079 error!("Failed to get remote address from request");
2080 }
2081 None
2082 }
2083 }
2084 ClientIdSource::XForwardedFor(num_hops) => {
2085 let do_header_parse = |op: &MetadataValue<Ascii>| {
2086 match op.to_str() {
2087 Ok(header_val) => {
2088 let header_contents =
2089 header_val.split(',').map(str::trim).collect::<Vec<_>>();
2090 if *num_hops == 0 {
2091 error!(
2092 "x-forwarded-for: 0 specified. x-forwarded-for contents: {:?}. Please assign nonzero value for \
2093 number of hops here, or use `socket-addr` client-id-source type if requests are not being proxied \
2094 to this node. Skipping traffic controller request handling.",
2095 header_contents,
2096 );
2097 return None;
2098 }
2099 let contents_len = header_contents.len();
2100 if contents_len < *num_hops {
2101 error!(
2102 "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2103 Expected at least {} values. Please correctly set the `x-forwarded-for` value under \
2104 `client-id-source` in the node config.",
2105 header_contents, contents_len, num_hops, contents_len,
2106 );
2107 self.metrics.client_id_source_config_mismatch.inc();
2108 return None;
2109 }
2110 let Some(client_ip) = header_contents.get(contents_len - num_hops)
2111 else {
2112 error!(
2113 "x-forwarded-for header value of {:?} contains {} values, but {} hops were specified. \
2114 Expected at least {} values. Skipping traffic controller request handling.",
2115 header_contents, contents_len, num_hops, contents_len,
2116 );
2117 return None;
2118 };
2119 parse_ip(client_ip).or_else(|| {
2120 self.metrics.forwarded_header_parse_error.inc();
2121 None
2122 })
2123 }
2124 Err(e) => {
2125 self.metrics.forwarded_header_invalid.inc();
2129 error!("Invalid UTF-8 in x-forwarded-for header: {:?}", e);
2130 None
2131 }
2132 }
2133 };
2134 if let Some(op) = request.metadata().get("x-forwarded-for") {
2135 do_header_parse(op)
2136 } else if let Some(op) = request.metadata().get("X-Forwarded-For") {
2137 do_header_parse(op)
2138 } else {
2139 self.metrics.forwarded_header_not_included.inc();
2140 error!(
2141 "x-forwarded-for header not present for request despite node configuring x-forwarded-for tracking type"
2142 );
2143 None
2144 }
2145 }
2146 }
2147 }
2148
2149 async fn handle_traffic_req(&self, client: Option<IpAddr>) -> Result<(), tonic::Status> {
2150 if let Some(traffic_controller) = &self.traffic_controller {
2151 if !traffic_controller.check(&client, &None).await {
2152 Err(tonic::Status::from_error(
2154 SuiErrorKind::TooManyRequests.into(),
2155 ))
2156 } else {
2157 Ok(())
2158 }
2159 } else {
2160 Ok(())
2161 }
2162 }
2163
2164 fn handle_traffic_resp<T>(
2165 &self,
2166 client: Option<IpAddr>,
2167 wrapped_response: WrappedServiceResponse<T>,
2168 method_name: &str,
2169 ) -> Result<tonic::Response<T>, tonic::Status> {
2170 let (error, spam_weight, unwrapped_response) = match wrapped_response {
2171 Ok((result, spam_weight)) => (None, spam_weight.clone(), Ok(result)),
2172 Err(status) => (
2173 Some(SuiError::from(status.clone())),
2174 Weight::zero(),
2175 Err(status.clone()),
2176 ),
2177 };
2178
2179 if let Some(traffic_controller) = self.traffic_controller.clone() {
2180 traffic_controller.tally(TrafficTally {
2181 direct: client,
2182 through_fullnode: None,
2183 error_info: error.map(|e| {
2184 let error_type = String::from(e.clone().as_ref());
2185 let error_weight = normalize(e);
2186 (error_weight, error_type)
2187 }),
2188 spam_weight,
2189 timestamp: SystemTime::now(),
2190 method: Some(method_name.to_string()),
2191 })
2192 }
2193 unwrapped_response
2194 }
2195}
2196
2197fn normalize(err: SuiError) -> Weight {
2199 match err.as_inner() {
2200 SuiErrorKind::UserInputError {
2201 error: UserInputError::IncorrectUserSignature { .. },
2202 } => Weight::one(),
2203 SuiErrorKind::InvalidSignature { .. }
2204 | SuiErrorKind::SignerSignatureAbsent { .. }
2205 | SuiErrorKind::SignerSignatureNumberMismatch { .. }
2206 | SuiErrorKind::IncorrectSigner { .. }
2207 | SuiErrorKind::UnknownSigner { .. }
2208 | SuiErrorKind::WrongEpoch { .. } => Weight::one(),
2209 _ => Weight::zero(),
2210 }
2211}
2212
2213#[macro_export]
2217macro_rules! handle_with_decoration {
2218 ($self:ident, $func_name:ident, $request:ident, $method_name:expr) => {{
2219 if $self.client_id_source.is_none() {
2220 return $self.$func_name($request).await.map(|(result, _)| result);
2221 }
2222
2223 let client = $self.get_client_ip_addr(&$request, $self.client_id_source.as_ref().unwrap());
2224
2225 $self.handle_traffic_req(client.clone()).await?;
2227
2228 let wrapped_response = $self.$func_name($request).await;
2230 $self.handle_traffic_resp(client, wrapped_response, $method_name)
2231 }};
2232}
2233
2234#[async_trait]
2235impl Validator for ValidatorService {
2236 async fn submit_transaction(
2237 &self,
2238 request: tonic::Request<RawSubmitTxRequest>,
2239 ) -> Result<tonic::Response<RawSubmitTxResponse>, tonic::Status> {
2240 let validator_service = self.clone();
2241
2242 spawn_monitored_task!(async move {
2245 handle_with_decoration!(
2248 validator_service,
2249 handle_submit_transaction_impl,
2250 request,
2251 "submit_transaction"
2252 )
2253 })
2254 .await
2255 .unwrap()
2256 }
2257
2258 async fn wait_for_effects(
2259 &self,
2260 request: tonic::Request<RawWaitForEffectsRequest>,
2261 ) -> Result<tonic::Response<RawWaitForEffectsResponse>, tonic::Status> {
2262 handle_with_decoration!(self, wait_for_effects_impl, request, "wait_for_effects")
2263 }
2264
2265 async fn object_info(
2266 &self,
2267 request: tonic::Request<ObjectInfoRequest>,
2268 ) -> Result<tonic::Response<ObjectInfoResponse>, tonic::Status> {
2269 handle_with_decoration!(self, object_info_impl, request, "object_info")
2270 }
2271
2272 async fn transaction_info(
2273 &self,
2274 request: tonic::Request<TransactionInfoRequest>,
2275 ) -> Result<tonic::Response<TransactionInfoResponse>, tonic::Status> {
2276 handle_with_decoration!(self, transaction_info_impl, request, "transaction_info")
2277 }
2278
2279 async fn checkpoint(
2280 &self,
2281 request: tonic::Request<CheckpointRequest>,
2282 ) -> Result<tonic::Response<CheckpointResponse>, tonic::Status> {
2283 handle_with_decoration!(self, checkpoint_impl, request, "checkpoint")
2284 }
2285
2286 async fn checkpoint_v2(
2287 &self,
2288 request: tonic::Request<CheckpointRequestV2>,
2289 ) -> Result<tonic::Response<CheckpointResponseV2>, tonic::Status> {
2290 handle_with_decoration!(self, checkpoint_v2_impl, request, "checkpoint_v2")
2291 }
2292
2293 async fn get_system_state_object(
2294 &self,
2295 request: tonic::Request<SystemStateRequest>,
2296 ) -> Result<tonic::Response<SuiSystemState>, tonic::Status> {
2297 handle_with_decoration!(
2298 self,
2299 get_system_state_object_impl,
2300 request,
2301 "get_system_state_object"
2302 )
2303 }
2304
2305 async fn validator_health(
2306 &self,
2307 request: tonic::Request<sui_types::messages_grpc::RawValidatorHealthRequest>,
2308 ) -> Result<tonic::Response<sui_types::messages_grpc::RawValidatorHealthResponse>, tonic::Status>
2309 {
2310 handle_with_decoration!(self, validator_health_impl, request, "validator_health")
2311 }
2312}
2313
2314#[cfg(test)]
2315mod inflight_guard_tests {
2316 use super::*;
2317 use prometheus::Registry;
2318
2319 fn make_guard(
2320 inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
2321 cache: Cache<TransactionDigest, Instant>,
2322 metrics: Arc<ValidatorServiceMetrics>,
2323 ) -> InflightTransactionsGuard {
2324 InflightTransactionsGuard {
2325 inflight,
2326 recently_submitted: cache,
2327 window: Duration::from_secs(10),
2328 metrics,
2329 acquired: HashSet::new(),
2330 }
2331 }
2332
2333 #[test]
2334 fn concurrent_acquire_rejects_other_request_and_is_idempotent_for_owner() {
2335 let inflight = Arc::new(Mutex::new(HashSet::new()));
2336 let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2337 let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2338 let digest = TransactionDigest::random();
2339
2340 let mut g1 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2341 let mut g2 = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2342
2343 assert!(matches!(g1.try_acquire(digest), AcquireOutcome::Acquired));
2345 assert_eq!(metrics.inflight_transactions.get(), 1);
2346
2347 assert!(matches!(
2349 g2.try_acquire(digest),
2350 AcquireOutcome::AlreadyAcquiredByAnotherRequest
2351 ));
2352
2353 assert!(matches!(
2355 g1.try_acquire(digest),
2356 AcquireOutcome::AlreadyAcquiredByThisRequest
2357 ));
2358 }
2359
2360 #[test]
2361 fn drop_demotes_into_recently_processed_outcome() {
2362 let inflight = Arc::new(Mutex::new(HashSet::new()));
2363 let cache = ValidatorService::new_recently_submitted_cache(Duration::from_secs(10));
2364 let metrics = Arc::new(ValidatorServiceMetrics::new(&Registry::new()));
2365 let digest = TransactionDigest::random();
2366
2367 {
2368 let mut g = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2369 assert!(matches!(g.try_acquire(digest), AcquireOutcome::Acquired));
2370 assert_eq!(inflight.lock().len(), 1);
2371 } assert_eq!(
2374 inflight.lock().len(),
2375 0,
2376 "acquired digest must be removed from the in-flight set on drop"
2377 );
2378 assert_eq!(metrics.inflight_transactions.get(), 0);
2379
2380 cache.run_pending_tasks();
2382
2383 let mut g_after = make_guard(inflight.clone(), cache.clone(), metrics.clone());
2385 assert!(matches!(
2386 g_after.try_acquire(digest),
2387 AcquireOutcome::RecentlyProcessed { .. }
2388 ));
2389 }
2390}