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 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 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 let is_soft_bundle_request = submit_type == SubmitTxType::SoftBundle;
745
746 let max_num_transactions = if is_soft_bundle_request {
747 epoch_store.protocol_config().max_soft_bundle_size()
750 } else {
751 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 let mut tx_digests = Vec::with_capacity(request.transactions.len());
768 let mut consensus_transactions = Vec::with_capacity(request.transactions.len());
770 let mut transaction_indexes = Vec::with_capacity(request.transactions.len());
772 let mut results: Vec<Option<SubmitTxResult>> = vec![None; request.transactions.len()];
774 let mut total_size_bytes = 0;
776 let mut has_gasless = false;
778 let mut duplicate_at_admission = false;
782 let mut expected_soft_bundle_gas_price = None;
784 let mut expected_soft_bundle_allowed_proposers = None;
786 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 return Err(SuiErrorKind::TransactionDeserializationError {
812 error: format!("Failed to deserialize transaction at index {}: {}", idx, e),
813 }
814 .into());
815 }
816 };
817
818 let tx_size = transaction.validity_check(&epoch_store.tx_validity_check_context())?;
820 let tx_digest = *transaction.digest();
821
822 epoch_store.check_self_allowed_proposer(transaction.data().transaction_data())?;
825
826 if !request_digests.insert(tx_digest) {
828 let error: SuiError = SuiErrorKind::UserInputError {
829 error: UserInputError::RepeatedTransactions { digest: tx_digest },
830 }
831 .into();
832 if is_soft_bundle_request {
834 return Err(error);
835 }
836 results[idx] = Some(SubmitTxResult::Rejected { error });
837 continue;
838 }
839
840 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 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 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 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 let consensus_key = SequencedConsensusTransactionKey::External(
1033 ConsensusTransactionKey::Certificate(tx_digest),
1034 );
1035 if epoch_store.is_consensus_message_processed(&consensus_key)? {
1036 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 if let Err(error) =
1089 state.handle_vote_transaction(&epoch_store, verified_transaction.tx().clone())
1090 {
1091 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 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 match inflight_guard.try_acquire(tx_digest) {
1150 AcquireOutcome::Acquired | AcquireOutcome::AlreadyAcquiredByThisRequest => {
1151 }
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(_) => { }
1202 Err(e) => {
1203 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 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 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 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 assert!(
1347 !consensus_transactions.is_empty(),
1348 "A valid soft bundle must have at least one transaction"
1349 );
1350 }
1351
1352 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 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 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_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 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_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 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 Err(err) => {
1524 let SuiErrorKind::TransactionProcessing { status, .. } =
1525 err.as_inner().clone()
1526 else {
1527 return Err(err);
1528 };
1529 for (idx, tx_digest) in txns_meta {
1531 debug!(
1532 ?tx_digest,
1533 "handle_submit_transaction: transaction already processing: {err}"
1534 );
1535 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 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 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 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 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 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 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
1691struct InflightTransactionsGuard {
1696 inflight: Arc<Mutex<HashSet<TransactionDigest>>>,
1698 recently_submitted: Cache<TransactionDigest, Instant>,
1699 window: Duration,
1700 metrics: Arc<ValidatorServiceMetrics>,
1701 acquired: HashSet<TransactionDigest>,
1703}
1704
1705enum AcquireOutcome {
1706 Acquired,
1708 AlreadyAcquiredByThisRequest,
1710 AlreadyAcquiredByAnotherRequest,
1712 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 if self.acquired.contains(&digest) {
1730 return AcquireOutcome::AlreadyAcquiredByThisRequest;
1731 }
1732
1733 if let Some(outcome) = self.recently_processed_outcome(digest) {
1735 return outcome;
1736 }
1737
1738 {
1740 let mut set = self.inflight.lock();
1741 if !set.insert(digest) {
1743 return AcquireOutcome::AlreadyAcquiredByAnotherRequest;
1744 }
1745 self.metrics.inflight_transactions.set(set.len() as i64);
1746 }
1747
1748 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 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 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 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 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 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, true, true,
1973 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 let epoch_store = state.load_epoch_store_one_call_per_task();
2039
2040 let num_inflight_execution_transactions =
2042 state.execution_scheduler().num_pending_certificates() as u64;
2043
2044 let num_inflight_consensus_transactions =
2046 self.consensus_adapter.num_inflight_transactions();
2047
2048 let last_committed_leader_round = epoch_store
2050 .consensus_tx_status_cache
2051 .get_last_committed_leader_round()
2052 .unwrap_or(0);
2053
2054 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 if let Some(socket_addr) = socket_addr {
2104 Some(socket_addr.ip())
2105 } else {
2106 if cfg!(msim) {
2107 } 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 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 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
2230fn 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#[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 $self.handle_traffic_req(client.clone()).await?;
2260
2261 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 spawn_monitored_task!(async move {
2278 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 assert!(matches!(g1.try_acquire(digest), AcquireOutcome::Acquired));
2378 assert_eq!(metrics.inflight_transactions.get(), 1);
2379
2380 assert!(matches!(
2382 g2.try_acquire(digest),
2383 AcquireOutcome::AlreadyAcquiredByAnotherRequest
2384 ));
2385
2386 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 } 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 cache.run_pending_tasks();
2415
2416 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}