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