1use std::{
11 collections::{BTreeMap, HashMap, hash_map::Entry},
12 net::SocketAddr,
13 ops::Deref,
14 path::Path,
15 sync::Arc,
16 time::Duration,
17};
18
19use futures::{
20 FutureExt,
21 future::{Either, Future, select},
22};
23use iota_common::{debug_fatal, sync::notify_read::NotifyRead};
24use iota_config::NodeConfig;
25use iota_metrics::{
26 TX_TYPE_SHARED_OBJ_TX, TX_TYPE_SINGLE_WRITER_TX, add_server_timing,
27 spawn_logged_monitored_task, spawn_monitored_task,
28};
29use iota_sdk_types::{Transaction, TransactionDigest};
30use iota_storage::write_path_pending_tx_log::WritePathPendingTransactionLog;
31use iota_types::{
32 effects::TransactionEffectsAPI,
33 error::{IotaError, IotaResult},
34 iota_system_state::IotaSystemState,
35 messages_checkpoint::CheckpointSequenceNumber,
36 quorum_driver_types::{
37 EffectsFinalityInfo, ExecuteTransactionRequestType, ExecuteTransactionRequestV1,
38 ExecuteTransactionResponseV1, FinalizedEffects, GroupedErrors,
39 IsTransactionExecutedLocally, QuorumDriverEffectsQueueResult, QuorumDriverError,
40 QuorumDriverResponse, QuorumDriverResult,
41 },
42 transaction::{SenderSignedTransactionAPI, VerifiedTransaction},
43 transaction_driver_types::{
44 EffectsFinalityInfo as TdEffectsFinalityInfo, FinalizedEffects as TdFinalizedEffects,
45 },
46 transaction_executor::{SimulateTransactionResult, VmChecks},
47};
48use parking_lot::Mutex;
49use prometheus_filtered::{
50 Histogram, MetricLevel, Registry,
51 core::{AtomicI64, AtomicU64, GenericCounter, GenericGauge},
52 register_histogram_vec_with_registry, register_int_counter_vec_with_registry,
53 register_int_counter_with_registry, register_int_gauge_vec_with_registry,
54 register_int_gauge_with_registry,
55};
56use tokio::{
57 sync::{
58 broadcast::{Receiver, error::RecvError},
59 watch,
60 },
61 task::JoinHandle,
62 time::timeout,
63};
64use tracing::{Instrument, debug, error, info, instrument, trace_span, warn};
65
66use crate::{
67 authority::{AuthorityState, authority_per_epoch_store::AuthorityPerEpochStore},
68 authority_aggregator::AuthorityAggregator,
69 authority_client::{AuthorityAPI, NetworkAuthorityClient},
70 quorum_driver::{
71 QuorumDriverHandler, QuorumDriverHandlerBuilder, QuorumDriverMetrics,
72 reconfig_observer::{OnsiteReconfigObserver, ReconfigObserver},
73 },
74 transaction_driver::{
75 AggregatedRequestErrors, QuorumTransactionResponse, SubmitTransactionOptions,
76 TransactionDriver, TransactionDriverError, TransactionDriverMetrics,
77 reconfig_observer::OnsiteReconfigObserver as TdOnsiteReconfigObserver,
78 },
79 validator_client_monitor::ValidatorClientMetrics,
80};
81
82const WAIT_FOR_FINALITY_TIMEOUT: Duration = Duration::from_secs(30);
83
84enum Driver<A: Clone> {
87 Quorum(Arc<QuorumDriverHandler<A>>),
89 Transaction(Arc<TransactionDriver<A>>),
91}
92
93pub struct TransactionOrchestrator<A: Clone> {
104 quorum_driver: Option<Arc<QuorumDriverHandler<A>>>,
105 transaction_driver: Arc<TransactionDriver<A>>,
106 validator_state: Arc<AuthorityState>,
107 _local_executor_handle: Option<JoinHandle<()>>,
110 pending_tx_log: Arc<WritePathPendingTransactionLog>,
111 in_flight_transactions: InFlightTransactions,
119 notifier: Arc<NotifyRead<TransactionDigest, QuorumDriverResult>>,
120 metrics: Arc<TransactionOrchestratorMetrics>,
121}
122
123impl TransactionOrchestrator<NetworkAuthorityClient> {
124 pub fn new_with_auth_aggregator(
125 validators: Arc<AuthorityAggregator<NetworkAuthorityClient>>,
126 validator_state: Arc<AuthorityState>,
127 reconfig_channel: Receiver<IotaSystemState>,
128 parent_path: &Path,
129 prometheus_registry: &Registry,
130 node_config: Option<&NodeConfig>,
131 ) -> Self {
132 let td_reconfig_observer = TdOnsiteReconfigObserver::new(
133 reconfig_channel.resubscribe(),
134 validator_state.get_object_cache_reader().clone(),
135 validator_state.clone_committee_store(),
136 validators.safe_client_metrics_base.clone(),
137 );
138
139 let qd_reconfig_observer = OnsiteReconfigObserver::new(
140 reconfig_channel.resubscribe(),
141 validator_state.get_object_cache_reader().clone(),
142 validator_state.clone_committee_store(),
143 validators.safe_client_metrics_base.clone(),
144 validators.metrics.deref().clone(),
145 );
146
147 TransactionOrchestrator::new(
148 validators,
149 validator_state,
150 parent_path,
151 prometheus_registry,
152 qd_reconfig_observer,
153 td_reconfig_observer,
154 node_config,
155 )
156 }
157}
158
159impl<A> TransactionOrchestrator<A>
160where
161 A: AuthorityAPI + Send + Sync + 'static + Clone,
162 OnsiteReconfigObserver: ReconfigObserver<A>,
163 TdOnsiteReconfigObserver: crate::transaction_driver::reconfig_observer::ReconfigObserver<A>,
164{
165 pub fn new(
166 validators: Arc<AuthorityAggregator<A>>,
167 validator_state: Arc<AuthorityState>,
168 parent_path: &Path,
169 prometheus_registry: &Registry,
170 reconfig_observer: OnsiteReconfigObserver,
171 td_reconfig_observer: TdOnsiteReconfigObserver,
172 node_config: Option<&NodeConfig>,
173 ) -> Self {
174 let epoch_store = validator_state.load_epoch_store_one_call_per_task();
175 let use_transaction_driver = epoch_store.protocol_config().enable_pcool_flow();
176
177 let notifier = Arc::new(NotifyRead::new());
178 let metrics = Arc::new(TransactionOrchestratorMetrics::new(prometheus_registry));
179 let pending_tx_log = Arc::new(WritePathPendingTransactionLog::new(
180 parent_path.join("fullnode_pending_transactions"),
181 ));
182
183 let quorum_driver_metrics = Arc::new(QuorumDriverMetrics::new(prometheus_registry));
186 let transaction_driver_metrics =
187 Arc::new(TransactionDriverMetrics::new(prometheus_registry));
188 let client_metrics = Arc::new(ValidatorClientMetrics::new(prometheus_registry));
189
190 let (quorum_driver, _local_executor_handle) = if use_transaction_driver {
191 (None, None)
192 } else {
193 let quorum_driver = Arc::new(
194 QuorumDriverHandlerBuilder::new(validators.clone(), quorum_driver_metrics)
195 .with_notifier(notifier.clone())
196 .with_reconfig_observer(Arc::new(reconfig_observer))
197 .start(),
198 );
199 let effects_receiver = quorum_driver.subscribe_to_effects();
203 let pending_tx_log_clone = pending_tx_log.clone();
204 let local_executor_handle = spawn_monitored_task!(async move {
205 Self::loop_pending_transaction_log(effects_receiver, pending_tx_log_clone).await;
206 });
207 Self::schedule_txes_in_log(pending_tx_log.clone(), quorum_driver.clone());
208 (Some(quorum_driver), Some(local_executor_handle))
209 };
210
211 let pcool_flow_enabled: Arc<dyn Fn() -> bool + Send + Sync> = {
213 let validator_state = Arc::downgrade(&validator_state);
214 Arc::new(move || {
215 validator_state.upgrade().is_some_and(|state| {
216 state
217 .load_epoch_store_one_call_per_task()
218 .protocol_config()
219 .enable_pcool_flow()
220 })
221 })
222 };
223 let transaction_driver = TransactionDriver::new(
224 validators,
225 Arc::new(td_reconfig_observer),
226 transaction_driver_metrics,
227 node_config.and_then(|config| config.validator_client_monitor_config.clone()),
228 client_metrics,
229 pcool_flow_enabled,
230 );
231
232 Self {
233 quorum_driver,
234 transaction_driver,
235 validator_state,
236 _local_executor_handle,
237 pending_tx_log,
238 in_flight_transactions: Default::default(),
239 notifier,
240 metrics,
241 }
242 }
243}
244
245impl<A> TransactionOrchestrator<A>
246where
247 A: AuthorityAPI + Send + Sync + 'static + Clone,
248{
249 fn select_driver(
255 &self,
256 epoch_store: &AuthorityPerEpochStore,
257 ) -> Result<Driver<A>, QuorumDriverError> {
258 if epoch_store.protocol_config().enable_pcool_flow() {
259 return Ok(Driver::Transaction(self.transaction_driver.clone()));
260 }
261 self.quorum_driver
262 .clone()
263 .map(Driver::Quorum)
264 .ok_or_else(|| {
265 error!(
266 "This fullnode started while P-COOL was enabled and must be restarted to \
267 serve the certificate-based flow"
268 );
269 QuorumDriverError::QuorumDriverInternal(IotaError::UnsupportedFeature {
270 error: "this fullnode started while P-COOL was enabled and must be \
271 restarted to serve the certificate-based flow"
272 .to_string(),
273 })
274 })
275 }
276
277 #[instrument(name = "tx_orchestrator_execute_transaction_block", level = "trace", skip_all,
278 fields(
279 tx_digest = ?request.transaction.digest(),
280 tx_type = ?request_type,
281 ),
282 err)]
283 pub async fn execute_transaction_block(
284 &self,
285 request: ExecuteTransactionRequestV1,
286 request_type: ExecuteTransactionRequestType,
287 client_addr: Option<SocketAddr>,
288 ) -> Result<(ExecuteTransactionResponseV1, IsTransactionExecutedLocally), QuorumDriverError>
289 {
290 let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
291
292 let transaction = epoch_store
293 .verify_transaction(request.transaction.clone())
294 .map_err(QuorumDriverError::InvalidUserSignature)?;
295
296 let include_events = request.include_events;
301 let include_input_objects = request.include_input_objects;
302 let include_output_objects = request.include_output_objects;
303
304 let tx_digest = *transaction.digest();
305 let wait_for_local_execution = matches!(
306 request_type,
307 ExecuteTransactionRequestType::WaitForLocalExecution
308 );
309
310 let _object_retention = (include_input_objects || include_output_objects)
314 .then(|| self.validator_state.retain_objects());
315
316 if let Some(response) = Self::build_response_from_local_effects(
320 &self.validator_state,
321 &tx_digest,
322 include_events,
323 include_input_objects,
324 include_output_objects,
325 )? {
326 self.metrics.early_cached_response.inc();
327 debug!(
328 ?tx_digest,
329 "Returning cached results for already-executed transaction"
330 );
331 let executed_locally = !wait_for_local_execution
339 || (response.effects.effects.epoch() < epoch_store.epoch()
340 || self
341 .validator_state
342 .wait_for_checkpoint_inclusion(&[tx_digest], WAIT_FOR_FINALITY_TIMEOUT)
343 .await
344 .is_ok_and(|inclusion| self.is_indexed(&inclusion, &tx_digest)));
345 return Ok((response, executed_locally));
346 }
347
348 transaction
354 .validity_check(&epoch_store.tx_validity_check_context())
355 .map_err(QuorumDriverError::InvalidTransaction)?;
356
357 let (mut response, seq) =
358 match (self.select_driver(&epoch_store)?, wait_for_local_execution) {
359 (Driver::Transaction(td), true) => {
360 let in_flight_transactions = self.in_flight_transactions.clone();
361 let validator_state = self.validator_state.clone();
362 let metrics = self.metrics.clone();
363 join_submission_task(spawn_monitored_task!(Self::submit_with_checkpoint_race(
367 td,
368 in_flight_transactions,
369 validator_state,
370 metrics,
371 request,
372 client_addr,
373 tx_digest,
374 )))
375 .await?
376 }
377 (Driver::Transaction(td), false) => {
378 let in_flight_transactions = self.in_flight_transactions.clone();
379 let validator_state = self.validator_state.clone();
380 let result = join_submission_task(spawn_monitored_task!(
382 Self::submit_with_transaction_driver(
383 td,
384 in_flight_transactions,
385 validator_state,
386 request,
387 client_addr,
388 false,
389 )
390 ))
391 .await?;
392 (Some(result), None)
393 }
394 (Driver::Quorum(qd), _) => {
395 let qd_resp = self
396 .execute_transaction_impl(
397 &qd,
398 &epoch_store,
399 request,
400 transaction.clone(),
401 client_addr,
402 )
403 .await?;
404 (Some(quorum_driver_response_to_v1(qd_resp)), None)
405 }
406 };
407
408 let needs_cache_rebuild = matches!(
419 response.as_ref().map(|r| &r.effects.finality_info),
420 None | Some(EffectsFinalityInfo::UncertifiedSingleValidator(_)),
421 );
422
423 let executed_locally = if !wait_for_local_execution {
424 false
425 } else if needs_cache_rebuild {
426 let Some(seq) = seq else {
427 return Err(QuorumDriverError::TimeoutBeforeFinality);
433 };
434 match response.as_mut() {
435 Some(existing) => Self::reconcile_effects_from_cache(
436 &self.validator_state,
437 tx_digest,
438 seq,
439 include_events,
440 include_input_objects,
441 include_output_objects,
442 existing,
443 &self.metrics,
444 )?,
445 None => {
446 response = Some(Self::build_response_from_cache(
447 &self.validator_state,
448 tx_digest,
449 seq,
450 include_events,
451 include_input_objects,
452 include_output_objects,
453 )?);
454 }
455 }
456 self.validator_state
459 .is_checkpoint_indexed(seq)
460 .unwrap_or(false)
461 } else {
462 let deadline = tokio::time::Instant::now() + WAIT_FOR_FINALITY_TIMEOUT;
470 let executed = Self::wait_for_finalized_tx_executed_locally_with_timeout(
471 &self.validator_state,
472 &transaction,
473 &self.metrics,
474 deadline,
475 )
476 .await
477 .is_ok();
478 add_server_timing("local_execution");
479 executed
482 && self
483 .validator_state
484 .wait_for_checkpoint_inclusion(
485 &[tx_digest],
486 deadline.saturating_duration_since(tokio::time::Instant::now()),
487 )
488 .await
489 .is_ok_and(|inclusion| self.is_indexed(&inclusion, &tx_digest))
490 };
491
492 let response = response.expect("response must be populated before return");
493
494 if matches!(
503 response.effects.finality_info,
504 EffectsFinalityInfo::UncertifiedSingleValidator(_)
505 ) {
506 debug_fatal!(
507 "Uncertified effects (UncertifiedSingleValidator) about to be returned \
508 to the client for tx {:?}",
509 response.effects.effects.transaction_digest()
510 );
511 return Err(QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(
512 "internal error: transaction effects not finalized".to_string(),
513 )));
514 }
515
516 Ok((response, executed_locally))
517 }
518
519 fn reconcile_effects_from_cache(
542 validator_state: &Arc<AuthorityState>,
543 tx_digest: TransactionDigest,
544 checkpoint_seq: CheckpointSequenceNumber,
545 include_events: bool,
546 include_input_objects: bool,
547 include_output_objects: bool,
548 response: &mut ExecuteTransactionResponseV1,
549 metrics: &TransactionOrchestratorMetrics,
550 ) -> Result<(), QuorumDriverError> {
551 let rebuilt = Self::build_response_from_cache(
552 validator_state,
553 tx_digest,
554 checkpoint_seq,
555 include_events,
556 include_input_objects,
557 include_output_objects,
558 )?;
559
560 let td_digest = response.effects.effects.digest();
561 let cache_digest = rebuilt.effects.effects.digest();
562 if td_digest != cache_digest {
563 warn!(
564 ?tx_digest,
565 ?td_digest,
566 ?cache_digest,
567 "reconcile_effects_from_cache: TransactionDriver and local cache disagree \
568 on effects digest — replacing with cache (possible byzantine submitter)"
569 );
570 }
571 if include_events && response.events.is_some() && rebuilt.events.is_none() {
572 warn!(
573 ?tx_digest,
574 "reconcile_effects_from_cache: submitter claimed events but cache has \
575 none — discarding (possible byzantine submitter)"
576 );
577 metrics.skip_effect_cert_events_cache_miss.inc();
578 }
579 *response = rebuilt;
580 Ok(())
581 }
582
583 fn is_indexed(
587 &self,
588 inclusion: &BTreeMap<TransactionDigest, (CheckpointSequenceNumber, u64)>,
589 digest: &TransactionDigest,
590 ) -> bool {
591 inclusion.get(digest).is_some_and(|&(seq, _)| {
592 self.validator_state
593 .is_checkpoint_indexed(seq)
594 .unwrap_or(false)
595 })
596 }
597
598 fn build_response_from_cache(
606 validator_state: &Arc<AuthorityState>,
607 tx_digest: TransactionDigest,
608 checkpoint_seq: CheckpointSequenceNumber,
609 include_events: bool,
610 include_input_objects: bool,
611 include_output_objects: bool,
612 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
613 let cached = read_cached_transaction_data(
614 validator_state,
615 &tx_digest,
616 include_events,
617 include_input_objects,
618 include_output_objects,
619 )
620 .map_err(|e| {
621 QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(format!(
622 "failed to read cached tx data for {tx_digest:?}: {e:?}"
623 )))
624 })?
625 .ok_or_else(|| {
626 warn!(
630 ?tx_digest,
631 "effects missing from cache after checkpoint inclusion — surfacing as \
632 TimeoutBeforeFinality"
633 );
634 QuorumDriverError::TimeoutBeforeFinality
635 })?;
636 let iota_types::transaction_executor::CachedTransactionData {
637 effects,
638 events,
639 input_objects,
640 output_objects,
641 } = cached;
642
643 let epoch = effects.epoch();
644 Ok(ExecuteTransactionResponseV1 {
645 effects: FinalizedEffects {
646 effects,
647 finality_info: EffectsFinalityInfo::Checkpointed(epoch, checkpoint_seq),
648 },
649 events,
650 input_objects,
651 output_objects,
652 auxiliary_data: None,
653 })
654 }
655
656 fn build_response_from_local_effects(
663 validator_state: &Arc<AuthorityState>,
664 tx_digest: &TransactionDigest,
665 include_events: bool,
666 include_input_objects: bool,
667 include_output_objects: bool,
668 ) -> Result<Option<ExecuteTransactionResponseV1>, QuorumDriverError> {
669 let Some(cached) = read_cached_transaction_data(
670 validator_state,
671 tx_digest,
672 include_events,
673 include_input_objects,
674 include_output_objects,
675 )
676 .map_err(QuorumDriverError::QuorumDriverInternal)?
677 else {
678 return Ok(None);
679 };
680 let iota_types::transaction_executor::CachedTransactionData {
681 effects,
682 events,
683 input_objects,
684 output_objects,
685 } = cached;
686
687 let epoch = effects.epoch();
688 Ok(Some(ExecuteTransactionResponseV1 {
689 effects: FinalizedEffects {
690 effects,
691 finality_info: EffectsFinalityInfo::QuorumExecuted(epoch),
692 },
693 events,
694 input_objects,
695 output_objects,
696 auxiliary_data: None,
697 }))
698 }
699
700 #[instrument(name = "tx_orchestrator_execute_transaction_v1", level = "trace", skip_all,
703 fields(tx_digest = ?request.transaction.digest()))]
704 pub async fn execute_transaction_v1(
705 &self,
706 request: ExecuteTransactionRequestV1,
707 skip_certification: bool,
708 client_addr: Option<SocketAddr>,
709 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
710 let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
711
712 let transaction = epoch_store
713 .verify_transaction(request.transaction.clone())
714 .map_err(QuorumDriverError::InvalidUserSignature)?;
715 let tx_digest = *transaction.digest();
716
717 if let Some(response) = Self::build_response_from_local_effects(
721 &self.validator_state,
722 &tx_digest,
723 request.include_events,
724 request.include_input_objects,
725 request.include_output_objects,
726 )? {
727 self.metrics.early_cached_response.inc();
728 debug!(
729 ?tx_digest,
730 "Returning cached results for already-executed transaction"
731 );
732 return Ok(response);
733 }
734
735 transaction
741 .validity_check(&epoch_store.tx_validity_check_context())
742 .map_err(QuorumDriverError::InvalidTransaction)?;
743
744 match self.select_driver(&epoch_store)? {
745 Driver::Transaction(td) => {
746 let in_flight_transactions = self.in_flight_transactions.clone();
747 let validator_state = self.validator_state.clone();
748 join_submission_task(spawn_monitored_task!(Self::submit_with_transaction_driver(
756 td,
757 in_flight_transactions,
758 validator_state,
759 request,
760 client_addr,
761 skip_certification,
762 )))
763 .await
764 }
765 Driver::Quorum(qd) => {
766 let qd_resp = self
767 .execute_transaction_impl(&qd, &epoch_store, request, transaction, client_addr)
768 .await?;
769 Ok(quorum_driver_response_to_v1(qd_resp))
770 }
771 }
772 }
773
774 #[instrument(name = "tx_orchestrator_submit_with_checkpoint_race", level = "trace", skip_all,
792 fields(tx_digest = ?tx_digest))]
793 async fn submit_with_checkpoint_race(
794 td: Arc<TransactionDriver<A>>,
795 in_flight_transactions: InFlightTransactions,
796 validator_state: Arc<AuthorityState>,
797 metrics: Arc<TransactionOrchestratorMetrics>,
798 request: ExecuteTransactionRequestV1,
799 client_addr: Option<SocketAddr>,
800 tx_digest: TransactionDigest,
801 ) -> Result<
802 (
803 Option<ExecuteTransactionResponseV1>,
804 Option<CheckpointSequenceNumber>,
805 ),
806 QuorumDriverError,
807 > {
808 let digests = [tx_digest];
809 let checkpoint_inclusion =
810 validator_state.wait_for_checkpoint_inclusion(&digests, WAIT_FOR_FINALITY_TIMEOUT);
811 tokio::pin!(checkpoint_inclusion);
812 let driver = Self::submit_with_transaction_driver(
813 td,
814 in_flight_transactions,
815 validator_state.clone(),
816 request,
817 client_addr,
818 true,
819 );
820
821 let seq_for_tx = |inclusion_map: BTreeMap<_, (CheckpointSequenceNumber, _)>| {
822 inclusion_map.get(&tx_digest).map(|&(seq, _)| seq)
823 };
824
825 let result = tokio::select! {
826 biased;
827 driver_result = driver => {
832 let response = Some(driver_result?);
833 let seq = (&mut checkpoint_inclusion).await.ok().and_then(seq_for_tx);
834 (response, seq)
835 }
836 checkpoint_result = &mut checkpoint_inclusion => {
837 metrics.skip_effect_cert_checkpoint_overrode_driver.inc();
838 let seq = checkpoint_result.ok().and_then(seq_for_tx);
843 (None, seq)
844 }
845 };
846 add_server_timing("local_execution");
847 Ok(result)
848 }
849
850 #[instrument(name = "tx_orchestrator_submit_with_td", level = "trace", skip_all,
864 fields(tx_digest = ?request.transaction.digest()))]
865 async fn submit_with_transaction_driver(
866 td: Arc<TransactionDriver<A>>,
867 in_flight_transactions: InFlightTransactions,
868 validator_state: Arc<AuthorityState>,
869 request: ExecuteTransactionRequestV1,
870 client_addr: Option<SocketAddr>,
871 skip_certification: bool,
872 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
873 let tx_digest = *request.transaction.digest();
874
875 let guard = match TransactionSubmissionGuard::acquire(in_flight_transactions, tx_digest) {
881 TransactionSubmission::Driving(guard) => guard,
882 TransactionSubmission::AlreadyInFlight(receiver) => {
883 debug!(
884 ?tx_digest,
885 "transaction already in flight; awaiting its outcome instead of driving a \
886 duplicate submission"
887 );
888 return Self::await_in_flight_transaction(
889 receiver,
890 &td,
891 &validator_state,
892 tx_digest,
893 &request,
894 client_addr,
895 skip_certification,
896 )
897 .await;
898 }
899 };
900
901 let td_response = match td
906 .drive_transaction(
907 Some(request.transaction.clone()),
908 SubmitTransactionOptions {
909 forwarded_client_addr: client_addr,
910 cut_off_validators: validator_state.cut_off_validators(),
911 ..Default::default()
912 },
913 Some(WAIT_FOR_FINALITY_TIMEOUT),
914 skip_certification,
915 )
916 .await
917 {
918 Ok(response) => response,
919 Err(e) => {
920 warn!(?tx_digest, "TransactionDriver submission failed: {e}");
921 let error = map_td_error_to_qd(e);
922 guard.publish(Err(error.clone()));
923 return Err(error);
924 }
925 };
926
927 debug!(?tx_digest, "TransactionDriver submission succeeded");
928
929 let td_response = Arc::new(td_response);
930 guard.publish(Ok(td_response.clone()));
931 drop(guard);
936 let td_response = Arc::try_unwrap(td_response).unwrap_or_else(|shared| (*shared).clone());
937
938 Ok(Self::response_from_driver_response(td_response, &request))
939 }
940
941 fn response_from_driver_response(
944 td_response: QuorumTransactionResponse,
945 request: &ExecuteTransactionRequestV1,
946 ) -> ExecuteTransactionResponseV1 {
947 let QuorumTransactionResponse {
948 effects,
949 events,
950 input_objects,
951 output_objects,
952 auxiliary_data,
953 } = td_response;
954 ExecuteTransactionResponseV1 {
955 effects: convert_td_to_qd_effects(effects),
956 events: request.include_events.then_some(events).flatten(),
957 input_objects: request
958 .include_input_objects
959 .then_some(input_objects)
960 .flatten(),
961 output_objects: request
962 .include_output_objects
963 .then_some(output_objects)
964 .flatten(),
965 auxiliary_data: request
966 .include_auxiliary_data
967 .then_some(auxiliary_data)
968 .flatten(),
969 }
970 }
971
972 async fn await_in_flight_transaction(
983 mut receiver: watch::Receiver<Option<InFlightSubmissionResult>>,
984 td: &Arc<TransactionDriver<A>>,
985 validator_state: &Arc<AuthorityState>,
986 tx_digest: TransactionDigest,
987 request: &ExecuteTransactionRequestV1,
988 client_addr: Option<SocketAddr>,
989 skip_certification: bool,
990 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
991 let published = tokio::time::timeout(
996 WAIT_FOR_FINALITY_TIMEOUT,
997 receiver.wait_for(|outcome| outcome.is_some()),
998 )
999 .await
1000 .map_err(|_elapsed| QuorumDriverError::TimeoutBeforeFinality)?
1001 .ok()
1002 .and_then(|outcome_ref| outcome_ref.clone());
1003
1004 let Some(outcome) = published else {
1005 return Self::response_from_checkpoint_inclusion(validator_state, tx_digest, request)
1011 .await;
1012 };
1013 let td_response = outcome?;
1014
1015 let uncertified = matches!(
1016 td_response.effects.finality_info,
1017 TdEffectsFinalityInfo::UncertifiedSingleValidator(_)
1018 );
1019 if uncertified && !skip_certification {
1020 let certified = tokio::time::timeout(
1029 WAIT_FOR_FINALITY_TIMEOUT,
1030 td.certify_transaction(
1031 tx_digest,
1032 SubmitTransactionOptions {
1033 forwarded_client_addr: client_addr,
1034 ..Default::default()
1035 },
1036 ),
1037 )
1038 .await
1039 .map_err(|_elapsed| QuorumDriverError::TimeoutBeforeFinality)?
1040 .map_err(map_td_error_to_qd)?;
1041 return Ok(Self::response_from_driver_response(certified, request));
1042 }
1043
1044 Ok(Self::response_from_driver_response(
1045 (*td_response).clone(),
1046 request,
1047 ))
1048 }
1049
1050 async fn response_from_checkpoint_inclusion(
1057 validator_state: &Arc<AuthorityState>,
1058 tx_digest: TransactionDigest,
1059 request: &ExecuteTransactionRequestV1,
1060 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
1061 let digests = [tx_digest];
1062 let seq = validator_state
1071 .wait_for_checkpoint_inclusion(&digests, WAIT_FOR_FINALITY_TIMEOUT)
1072 .await
1073 .ok()
1074 .and_then(|inclusion| inclusion.get(&tx_digest).map(|&(seq, _)| seq))
1075 .ok_or(QuorumDriverError::TimeoutBeforeFinality)?;
1076 Self::build_response_from_cache(
1077 validator_state,
1078 tx_digest,
1079 seq,
1080 request.include_events,
1081 request.include_input_objects,
1082 request.include_output_objects,
1083 )
1084 }
1085
1086 #[instrument(level = "trace", skip_all, fields(tx_digest = ?request.transaction.digest()))]
1090 async fn execute_transaction_impl(
1091 &self,
1092 quorum_driver: &Arc<QuorumDriverHandler<A>>,
1093 epoch_store: &Arc<AuthorityPerEpochStore>,
1094 request: ExecuteTransactionRequestV1,
1095 transaction: VerifiedTransaction,
1096 client_addr: Option<SocketAddr>,
1097 ) -> Result<QuorumDriverResponse, QuorumDriverError> {
1098 let (_in_flight_metrics_guards, good_response_metrics) = self.update_metrics(&transaction);
1099 let tx_digest = *transaction.digest();
1100 debug!(?tx_digest, "TO Received transaction execution request.");
1101
1102 let (_e2e_latency_timer, _txn_finality_timer) = if transaction.contains_shared_object() {
1103 (
1104 self.metrics.request_latency_shared_obj.start_timer(),
1105 self.metrics
1106 .wait_for_finality_latency_shared_obj
1107 .start_timer(),
1108 )
1109 } else {
1110 (
1111 self.metrics.request_latency_single_writer.start_timer(),
1112 self.metrics
1113 .wait_for_finality_latency_single_writer
1114 .start_timer(),
1115 )
1116 };
1117
1118 let wait_for_finality_gauge = self.metrics.wait_for_finality_in_flight.clone();
1120 wait_for_finality_gauge.inc();
1121 let _wait_for_finality_gauge = scopeguard::guard(wait_for_finality_gauge, |in_flight| {
1122 in_flight.dec();
1123 });
1124
1125 let ticket = self
1126 .submit(
1127 quorum_driver,
1128 epoch_store.clone(),
1129 transaction.clone(),
1130 request,
1131 client_addr,
1132 )
1133 .await
1134 .map_err(|e| {
1135 warn!(?tx_digest, "QuorumDriverInternalError: {e:?}");
1136 QuorumDriverError::QuorumDriverInternal(e)
1137 })?;
1138
1139 let Ok(result) = timeout(WAIT_FOR_FINALITY_TIMEOUT, ticket).await else {
1140 debug!(?tx_digest, "Timeout waiting for transaction finality.");
1141 self.metrics.wait_for_finality_timeout.inc();
1142 return Err(QuorumDriverError::TimeoutBeforeFinality);
1143 };
1144 add_server_timing("wait_for_finality");
1145
1146 drop(_txn_finality_timer);
1147 drop(_wait_for_finality_gauge);
1148 self.metrics.wait_for_finality_finished.inc();
1149
1150 match result {
1151 Err(err) => {
1152 warn!(?tx_digest, "QuorumDriverInternalError: {err:?}");
1153 Err(QuorumDriverError::QuorumDriverInternal(err))
1154 }
1155 Ok(Err(err)) => Err(err),
1156 Ok(Ok(response)) => {
1157 good_response_metrics.inc();
1158 Ok(response)
1159 }
1160 }
1161 }
1162
1163 #[instrument(name = "tx_orchestrator_submit", level = "trace", skip_all)]
1166 async fn submit(
1167 &self,
1168 quorum_driver: &Arc<QuorumDriverHandler<A>>,
1169 epoch_store: Arc<AuthorityPerEpochStore>,
1170 transaction: VerifiedTransaction,
1171 request: ExecuteTransactionRequestV1,
1172 client_addr: Option<SocketAddr>,
1173 ) -> IotaResult<impl Future<Output = IotaResult<QuorumDriverResult>> + '_> {
1174 let tx_digest = *transaction.digest();
1175 let ticket = self.notifier.register_one(&tx_digest);
1176 if self
1179 .pending_tx_log
1180 .write_pending_transaction_maybe(&transaction)
1181 .await?
1182 {
1183 debug!(?tx_digest, "no pending request in flight, submitting.");
1184 quorum_driver
1185 .submit_transaction_no_ticket(request.clone(), client_addr)
1186 .await?;
1187 }
1188 let cache_reader = self.validator_state.get_transaction_cache_reader().clone();
1194 let qd = quorum_driver.clone();
1195 Ok(async move {
1196 let digests = [tx_digest];
1197 let effects_await =
1198 epoch_store.within_alive_epoch(cache_reader.try_notify_read_executed_effects(
1199 "TransactionOrchestrator::notify_read_submit_with_qd",
1200 &digests,
1201 ));
1202 let res = match select(ticket, effects_await.boxed()).await {
1204 Either::Left((quorum_driver_response, _)) => Ok(quorum_driver_response),
1205 Either::Right((_, unfinished_quorum_driver_task)) => {
1206 debug!(
1207 ?tx_digest,
1208 "Effects are available in DB, use quorum driver to get a certificate"
1209 );
1210 qd.submit_transaction_no_ticket(request, client_addr)
1211 .await?;
1212 Ok(unfinished_quorum_driver_task.await)
1213 }
1214 };
1215 res
1216 })
1217 }
1218
1219 #[instrument(
1220 name = "tx_orchestrator_wait_for_finalized_tx_executed_locally_with_timeout",
1221 level = "debug",
1222 skip_all,
1223 fields(tx_digest = ?transaction.digest()),
1224 err
1225 )]
1226 async fn wait_for_finalized_tx_executed_locally_with_timeout(
1227 validator_state: &Arc<AuthorityState>,
1228 transaction: &VerifiedTransaction,
1229 metrics: &TransactionOrchestratorMetrics,
1230 deadline: tokio::time::Instant,
1231 ) -> IotaResult {
1232 let tx_digest = *transaction.digest();
1233 metrics.local_execution_in_flight.inc();
1234 let _metrics_guard =
1235 scopeguard::guard(metrics.local_execution_in_flight.clone(), |in_flight| {
1236 in_flight.dec();
1237 });
1238
1239 let _guard = if transaction.contains_shared_object() {
1240 metrics.local_execution_latency_shared_obj.start_timer()
1241 } else {
1242 metrics.local_execution_latency_single_writer.start_timer()
1243 };
1244 debug!(
1245 ?tx_digest,
1246 "Waiting for finalized tx to be executed locally."
1247 );
1248 match tokio::time::timeout_at(
1249 deadline,
1250 validator_state
1251 .get_transaction_cache_reader()
1252 .try_notify_read_executed_effects_digests(
1253 "TransactionOrchestrator::notify_read_wait_for_local_execution",
1254 &[tx_digest],
1255 ),
1256 )
1257 .instrument(trace_span!("local_execution"))
1258 .await
1259 {
1260 Err(_elapsed) => {
1261 debug!(
1262 ?tx_digest,
1263 "Waiting for finalized tx to be executed locally timed out."
1264 );
1265 metrics.local_execution_timeout.inc();
1266 Err(IotaError::Timeout)
1267 }
1268 Ok(Err(err)) => {
1269 debug!(
1270 ?tx_digest,
1271 "Waiting for finalized tx to be executed locally failed with error: {:?}", err
1272 );
1273 metrics.local_execution_failure.inc();
1274 Err(IotaError::TransactionOrchestratorLocalExecution {
1275 error: err.to_string(),
1276 })
1277 }
1278 Ok(Ok(_)) => {
1279 metrics.local_execution_success.inc();
1280 Ok(())
1281 }
1282 }
1283 }
1284
1285 async fn loop_pending_transaction_log(
1287 mut effects_receiver: Receiver<QuorumDriverEffectsQueueResult>,
1288 pending_transaction_log: Arc<WritePathPendingTransactionLog>,
1289 ) {
1290 loop {
1291 match effects_receiver.recv().await {
1292 Ok(Ok((transaction, ..))) => {
1293 let tx_digest = transaction.digest();
1294 if let Err(err) = pending_transaction_log.finish_transaction(tx_digest) {
1295 error!(
1296 ?tx_digest,
1297 "Failed to finish transaction in pending transaction log: {err}"
1298 );
1299 }
1300 }
1301 Ok(Err((tx_digest, _err))) => {
1302 if let Err(err) = pending_transaction_log.finish_transaction(&tx_digest) {
1303 error!(
1304 ?tx_digest,
1305 "Failed to finish transaction in pending transaction log: {err}"
1306 );
1307 }
1308 }
1309 Err(RecvError::Closed) => {
1310 error!("Sender of effects subscriber queue has been dropped!");
1311 return;
1312 }
1313 Err(RecvError::Lagged(skipped_count)) => {
1314 warn!("Skipped {skipped_count} transasctions in effects subscriber queue.");
1315 }
1316 }
1317 }
1318 }
1319
1320 #[cfg(any(test, feature = "test-utils"))]
1324 pub fn quorum_driver(&self) -> Option<&Arc<QuorumDriverHandler<A>>> {
1325 self.quorum_driver.as_ref()
1326 }
1327
1328 #[cfg(any(test, feature = "test-utils"))]
1330 pub fn clone_quorum_driver(&self) -> Option<Arc<QuorumDriverHandler<A>>> {
1331 self.quorum_driver.clone()
1332 }
1333
1334 pub fn clone_authority_aggregator(&self) -> Arc<AuthorityAggregator<A>> {
1337 self.transaction_driver.authority_aggregator().load_full()
1338 }
1339
1340 pub fn subscribe_to_effects_queue(&self) -> Option<Receiver<QuorumDriverEffectsQueueResult>> {
1344 let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
1345 if epoch_store.protocol_config().enable_pcool_flow() {
1346 return None;
1347 }
1348 self.quorum_driver
1349 .as_ref()
1350 .map(|quorum_driver| quorum_driver.subscribe_to_effects())
1351 }
1352
1353 #[cfg(any(test, feature = "test-utils"))]
1355 pub fn select_driver_for_testing(
1356 &self,
1357 epoch_store: &AuthorityPerEpochStore,
1358 ) -> Result<(), QuorumDriverError> {
1359 self.select_driver(epoch_store).map(|_| ())
1360 }
1361
1362 fn update_metrics(
1363 &'_ self,
1364 transaction: &VerifiedTransaction,
1365 ) -> (impl Drop, &'_ GenericCounter<AtomicU64>) {
1366 let (in_flight, good_response) = if transaction.contains_shared_object() {
1367 self.metrics.total_req_received_shared_object.inc();
1368 (
1369 self.metrics.req_in_flight_shared_object.clone(),
1370 &self.metrics.good_response_shared_object,
1371 )
1372 } else {
1373 self.metrics.total_req_received_single_writer.inc();
1374 (
1375 self.metrics.req_in_flight_single_writer.clone(),
1376 &self.metrics.good_response_single_writer,
1377 )
1378 };
1379 in_flight.inc();
1380 (
1381 scopeguard::guard(in_flight, |in_flight| {
1382 in_flight.dec();
1383 }),
1384 good_response,
1385 )
1386 }
1387
1388 fn schedule_txes_in_log(
1389 pending_tx_log: Arc<WritePathPendingTransactionLog>,
1390 quorum_driver: Arc<QuorumDriverHandler<A>>,
1391 ) {
1392 if std::env::var("SKIP_LOADING_FROM_PENDING_TX_LOG").is_ok() {
1393 info!("Skipping loading pending transactions from pending_tx_log.");
1394 return;
1395 }
1396 spawn_logged_monitored_task!(async move {
1397 let pending_txes = pending_tx_log
1398 .load_all_pending_transactions()
1399 .expect("failed to load all pending transactions");
1400 info!(
1401 "Recovering {} pending transactions from pending_tx_log.",
1402 pending_txes.len()
1403 );
1404 for (i, tx) in pending_txes.into_iter().enumerate() {
1405 let tx = tx.into_inner();
1408 let tx_digest = *tx.digest();
1409 if let Err(err) = quorum_driver
1412 .submit_transaction_no_ticket(
1413 ExecuteTransactionRequestV1 {
1414 transaction: tx,
1415 include_events: true,
1416 include_input_objects: false,
1417 include_output_objects: false,
1418 include_auxiliary_data: false,
1419 },
1420 None,
1421 )
1422 .await
1423 {
1424 warn!(
1425 ?tx_digest,
1426 "Failed to enqueue transaction from pending_tx_log, err: {err:?}"
1427 );
1428 } else {
1429 debug!(?tx_digest, "Enqueued transaction from pending_tx_log");
1430 if (i + 1) % 1000 == 0 {
1431 info!("Enqueued {} transactions from pending_tx_log.", i + 1);
1432 }
1433 }
1434 }
1435 });
1439 }
1440
1441 pub fn load_all_pending_transactions(&self) -> IotaResult<Vec<VerifiedTransaction>> {
1442 self.pending_tx_log.load_all_pending_transactions()
1443 }
1444
1445 #[cfg(any(test, feature = "test-utils"))]
1448 pub fn in_flight_duplicates_for_testing(&self, tx_digest: &TransactionDigest) -> Option<usize> {
1449 self.in_flight_transactions
1450 .lock()
1451 .get(tx_digest)
1452 .map(|sender| sender.receiver_count())
1453 }
1454}
1455
1456fn quorum_driver_response_to_v1(response: QuorumDriverResponse) -> ExecuteTransactionResponseV1 {
1460 let QuorumDriverResponse {
1461 effects_cert,
1462 events,
1463 input_objects,
1464 output_objects,
1465 auxiliary_data,
1466 } = response;
1467 ExecuteTransactionResponseV1 {
1468 effects: FinalizedEffects::new_from_effects_cert(effects_cert.into()),
1469 events,
1470 input_objects,
1471 output_objects,
1472 auxiliary_data,
1473 }
1474}
1475
1476fn convert_td_to_qd_effects(td: TdFinalizedEffects) -> FinalizedEffects {
1479 let finality_info = match td.finality_info {
1480 TdEffectsFinalityInfo::Certified(sig) => EffectsFinalityInfo::Certified(sig),
1481 TdEffectsFinalityInfo::Checkpointed(epoch, seq) => {
1482 EffectsFinalityInfo::Checkpointed(epoch, seq)
1483 }
1484 TdEffectsFinalityInfo::QuorumExecuted(epoch) => EffectsFinalityInfo::QuorumExecuted(epoch),
1485 TdEffectsFinalityInfo::UncertifiedSingleValidator(epoch) => {
1486 EffectsFinalityInfo::UncertifiedSingleValidator(epoch)
1487 }
1488 };
1489 FinalizedEffects {
1490 effects: td.effects,
1491 finality_info,
1492 }
1493}
1494
1495fn map_td_error_to_qd(e: TransactionDriverError) -> QuorumDriverError {
1506 use TransactionDriverError::*;
1507 match e {
1508 ValidationFailed { error } => {
1509 QuorumDriverError::InvalidUserSignature(IotaError::InvalidSignature { error })
1510 }
1511 TimeoutWithLastRetriableError { last_error, .. } => match last_error {
1512 Some(last_error) => map_overload_to_qd(&last_error).unwrap_or_else(|| {
1513 QuorumDriverError::TimeoutBeforeFinalityWithErrors {
1514 last_error: last_error.to_string(),
1515 }
1516 }),
1517 None => QuorumDriverError::TimeoutBeforeFinality,
1518 },
1519 RejectedByValidators {
1520 submission_non_retriable_errors,
1521 ..
1522 } => {
1523 QuorumDriverError::RejectedByValidators {
1531 errors: submission_non_retriable_errors
1532 .errors
1533 .into_iter()
1534 .map(|(msg, _, _, _)| msg)
1535 .collect(),
1536 }
1537 }
1538 Aborted {
1539 submission_retriable_errors,
1540 submission_non_retriable_errors,
1541 ..
1542 } => {
1543 let attempts = count_validator_attempts(&submission_retriable_errors)
1548 + count_validator_attempts(&submission_non_retriable_errors);
1549 QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts {
1550 total_attempts: attempts,
1551 }
1552 }
1553 other @ ForkedExecution { .. } => {
1554 let msg = other.to_string();
1558 error!("TransactionDriver observed forked execution: {msg}");
1559 QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1560 }
1561 other @ ClientInternal { .. } => {
1562 let msg = other.to_string();
1563 warn!("TransactionDriver client-internal error: {msg}");
1564 QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1565 }
1566 other @ SubmittedButFetchFailed { .. } => {
1567 let msg = other.to_string();
1568 warn!("TransactionDriver submitted transaction but failed to fetch effects: {msg}");
1569 QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1570 }
1571 }
1572}
1573
1574fn map_overload_to_qd(e: &TransactionDriverError) -> Option<QuorumDriverError> {
1577 use iota_types::{base_types::ConciseableName, error::ErrorCategory};
1578
1579 if !e.is_overload_dominated() {
1580 return None;
1581 }
1582 let TransactionDriverError::Aborted {
1583 submission_retriable_errors,
1584 submission_non_retriable_errors,
1585 ..
1586 } = e
1587 else {
1588 return None;
1589 };
1590 let mut overloaded_stake = 0;
1591 let mut errors: GroupedErrors = Vec::new();
1592 for (msg, names, stake, category) in submission_retriable_errors
1593 .errors
1594 .iter()
1595 .chain(submission_non_retriable_errors.errors.iter())
1596 {
1597 if *category != ErrorCategory::ValidatorOverloaded {
1598 continue;
1599 }
1600 overloaded_stake += *stake;
1601 errors.push((
1602 IotaError::Unknown(msg.clone()),
1603 *stake,
1604 names.iter().map(|n| n.concise_owned()).collect(),
1605 ));
1606 }
1607 Some(
1608 match submission_retriable_errors.median_retry_after_secs() {
1609 Some(retry_after_secs) => QuorumDriverError::SystemOverloadRetryAfter {
1610 overload_stake: overloaded_stake,
1611 errors,
1612 retry_after_secs,
1613 },
1614 None => QuorumDriverError::SystemOverload {
1615 overloaded_stake,
1616 errors,
1617 },
1618 },
1619 )
1620}
1621
1622fn count_validator_attempts(errors: &AggregatedRequestErrors) -> u32 {
1623 errors
1624 .errors
1625 .iter()
1626 .map(|(_, authorities, _, _)| authorities.len() as u32)
1627 .sum()
1628}
1629
1630async fn join_submission_task<T>(
1633 handle: tokio::task::JoinHandle<Result<T, QuorumDriverError>>,
1634) -> Result<T, QuorumDriverError> {
1635 handle.await.unwrap_or_else(|e| {
1636 Err(QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(
1637 format!("transaction submission task panicked: {e}"),
1638 )))
1639 })
1640}
1641
1642#[derive(Clone)]
1644pub struct TransactionOrchestratorMetrics {
1645 total_req_received_single_writer: GenericCounter<AtomicU64>,
1646 total_req_received_shared_object: GenericCounter<AtomicU64>,
1647
1648 good_response_single_writer: GenericCounter<AtomicU64>,
1649 good_response_shared_object: GenericCounter<AtomicU64>,
1650
1651 req_in_flight_single_writer: GenericGauge<AtomicI64>,
1652 req_in_flight_shared_object: GenericGauge<AtomicI64>,
1653
1654 wait_for_finality_in_flight: GenericGauge<AtomicI64>,
1655 wait_for_finality_finished: GenericCounter<AtomicU64>,
1656 wait_for_finality_timeout: GenericCounter<AtomicU64>,
1657
1658 local_execution_in_flight: GenericGauge<AtomicI64>,
1659 local_execution_success: GenericCounter<AtomicU64>,
1660 local_execution_timeout: GenericCounter<AtomicU64>,
1661 local_execution_failure: GenericCounter<AtomicU64>,
1662
1663 early_cached_response: GenericCounter<AtomicU64>,
1664
1665 skip_effect_cert_events_cache_miss: GenericCounter<AtomicU64>,
1670
1671 skip_effect_cert_checkpoint_overrode_driver: GenericCounter<AtomicU64>,
1677
1678 request_latency_single_writer: Histogram,
1679 request_latency_shared_obj: Histogram,
1680 wait_for_finality_latency_single_writer: Histogram,
1681 wait_for_finality_latency_shared_obj: Histogram,
1682 local_execution_latency_single_writer: Histogram,
1683 local_execution_latency_shared_obj: Histogram,
1684}
1685
1686impl TransactionOrchestratorMetrics {
1690 pub fn new(registry: &Registry) -> Self {
1691 let total_req_received = register_int_counter_vec_with_registry!(
1692 "tx_orchestrator_total_req_received",
1693 "Total number of executions request Transaction Orchestrator receives, group by tx type",
1694 &["tx_type"],
1695 registry;
1696 MetricLevel::Warn,
1697 )
1698 .unwrap();
1699
1700 let total_req_received_single_writer =
1701 total_req_received.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1702 let total_req_received_shared_object =
1703 total_req_received.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1704
1705 let good_response = register_int_counter_vec_with_registry!(
1706 "tx_orchestrator_good_response",
1707 "Total number of good responses Transaction Orchestrator generates, group by tx type",
1708 &["tx_type"],
1709 registry;
1710 MetricLevel::Warn,
1711 )
1712 .unwrap();
1713
1714 let good_response_single_writer =
1715 good_response.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1716 let good_response_shared_object = good_response.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1717
1718 let req_in_flight = register_int_gauge_vec_with_registry!(
1719 "tx_orchestrator_req_in_flight",
1720 "Number of requests in flights Transaction Orchestrator processes, group by tx type",
1721 &["tx_type"],
1722 registry;
1723 MetricLevel::Warn,
1724 )
1725 .unwrap();
1726
1727 let req_in_flight_single_writer =
1728 req_in_flight.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1729 let req_in_flight_shared_object = req_in_flight.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1730
1731 let request_latency = register_histogram_vec_with_registry!(
1732 "tx_orchestrator_request_latency",
1733 "Time spent in processing one Transaction Orchestrator request",
1734 &["tx_type"],
1735 iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1736 registry;
1737 MetricLevel::Warn,
1738 )
1739 .unwrap();
1740 let wait_for_finality_latency = register_histogram_vec_with_registry!(
1741 "tx_orchestrator_wait_for_finality_latency",
1742 "Time spent in waiting for one Transaction Orchestrator request gets finalized",
1743 &["tx_type"],
1744 iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1745 registry;
1746 MetricLevel::Warn,
1747 )
1748 .unwrap();
1749 let local_execution_latency = register_histogram_vec_with_registry!(
1750 "tx_orchestrator_local_execution_latency",
1751 "Time spent in waiting for one Transaction Orchestrator gets locally executed",
1752 &["tx_type"],
1753 iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1754 registry;
1755 MetricLevel::Warn,
1756 )
1757 .unwrap();
1758
1759 Self {
1760 total_req_received_single_writer,
1761 total_req_received_shared_object,
1762 good_response_single_writer,
1763 good_response_shared_object,
1764 req_in_flight_single_writer,
1765 req_in_flight_shared_object,
1766 wait_for_finality_in_flight: register_int_gauge_with_registry!(
1767 "tx_orchestrator_wait_for_finality_in_flight",
1768 "Number of in flight txns Transaction Orchestrator are waiting for finality for",
1769 registry;
1770 MetricLevel::Warn,
1771 )
1772 .unwrap(),
1773 wait_for_finality_finished: register_int_counter_with_registry!(
1774 "tx_orchestrator_wait_for_finality_finished",
1775 "Total number of txns Transaction Orchestrator gets responses from Quorum Driver before timeout, either success or failure",
1776 registry;
1777 MetricLevel::Warn,
1778 )
1779 .unwrap(),
1780 wait_for_finality_timeout: register_int_counter_with_registry!(
1781 "tx_orchestrator_wait_for_finality_timeout",
1782 "Total number of txns timing out in waiting for finality Transaction Orchestrator handles",
1783 registry;
1784 MetricLevel::Warn,
1785 )
1786 .unwrap(),
1787 local_execution_in_flight: register_int_gauge_with_registry!(
1788 "tx_orchestrator_local_execution_in_flight",
1789 "Number of local execution txns in flights Transaction Orchestrator handles",
1790 registry;
1791 MetricLevel::Warn,
1792 )
1793 .unwrap(),
1794 local_execution_success: register_int_counter_with_registry!(
1795 "tx_orchestrator_local_execution_success",
1796 "Total number of successful local execution txns Transaction Orchestrator handles",
1797 registry;
1798 MetricLevel::Warn,
1799 )
1800 .unwrap(),
1801 local_execution_timeout: register_int_counter_with_registry!(
1802 "tx_orchestrator_local_execution_timeout",
1803 "Total number of timed-out local execution txns Transaction Orchestrator handles",
1804 registry;
1805 MetricLevel::Warn,
1806 )
1807 .unwrap(),
1808 local_execution_failure: register_int_counter_with_registry!(
1809 "tx_orchestrator_local_execution_failure",
1810 "Total number of failed local execution txns Transaction Orchestrator handles",
1811 registry;
1812 MetricLevel::Warn,
1813 )
1814 .unwrap(),
1815 early_cached_response: register_int_counter_with_registry!(
1816 "tx_orchestrator_early_cached_response",
1817 "Total number of requests returning cached results for already-executed transactions",
1818 registry,
1819 )
1820 .unwrap(),
1821 skip_effect_cert_events_cache_miss: register_int_counter_with_registry!(
1822 "tx_orchestrator_skip_effect_cert_events_cache_miss",
1823 "Number of skip-effect-certification responses rejected because the \
1824 single submitter claimed to have events but the local cache did not \
1825 corroborate them",
1826 registry,
1827 )
1828 .unwrap(),
1829 skip_effect_cert_checkpoint_overrode_driver: register_int_counter_with_registry!(
1830 "tx_orchestrator_skip_effect_cert_checkpoint_overrode_driver",
1831 "Number of skip-effect-certification requests where local checkpoint \
1832 inclusion completed before the TransactionDriver call returned; the \
1833 driver future was cancelled and the response was rebuilt from cache",
1834 registry,
1835 )
1836 .unwrap(),
1837 request_latency_single_writer: request_latency
1838 .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1839 request_latency_shared_obj: request_latency.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1840 wait_for_finality_latency_single_writer: wait_for_finality_latency
1841 .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1842 wait_for_finality_latency_shared_obj: wait_for_finality_latency
1843 .with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1844 local_execution_latency_single_writer: local_execution_latency
1845 .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1846 local_execution_latency_shared_obj: local_execution_latency
1847 .with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1848 }
1849 }
1850
1851 pub fn new_for_tests() -> Self {
1852 let registry = Registry::new();
1853 Self::new(®istry)
1854 }
1855}
1856
1857#[async_trait::async_trait]
1858impl<A> iota_types::transaction_executor::TransactionExecutor for TransactionOrchestrator<A>
1859where
1860 A: AuthorityAPI + Send + Sync + 'static + Clone,
1861{
1862 async fn execute_transaction(
1863 &self,
1864 request: ExecuteTransactionRequestV1,
1865 skip_certification: bool,
1866 client_addr: Option<std::net::SocketAddr>,
1867 ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
1868 self.execute_transaction_v1(request, skip_certification, client_addr)
1869 .await
1870 }
1871
1872 fn simulate_transaction(
1873 &self,
1874 transaction: Transaction,
1875 checks: VmChecks,
1876 ) -> Result<SimulateTransactionResult, IotaError> {
1877 self.validator_state
1878 .simulate_transaction(transaction, checks)
1879 }
1880
1881 async fn wait_for_checkpoint_inclusion(
1888 &self,
1889 digests: &[TransactionDigest],
1890 timeout: Duration,
1891 ) -> Result<BTreeMap<TransactionDigest, (CheckpointSequenceNumber, u64)>, IotaError> {
1892 self.validator_state
1893 .wait_for_checkpoint_inclusion(digests, timeout)
1894 .await
1895 }
1896
1897 fn retain_cached_objects(&self) -> Box<dyn Send + Sync> {
1898 Box::new(self.validator_state.retain_objects())
1899 }
1900
1901 fn read_transaction_from_cache(
1902 &self,
1903 digest: &TransactionDigest,
1904 include_events: bool,
1905 include_input_objects: bool,
1906 include_output_objects: bool,
1907 ) -> Result<Option<iota_types::transaction_executor::CachedTransactionData>, IotaError> {
1908 read_cached_transaction_data(
1909 &self.validator_state,
1910 digest,
1911 include_events,
1912 include_input_objects,
1913 include_output_objects,
1914 )
1915 }
1916}
1917
1918fn read_cached_transaction_data(
1923 validator_state: &Arc<AuthorityState>,
1924 digest: &TransactionDigest,
1925 include_events: bool,
1926 include_input_objects: bool,
1927 include_output_objects: bool,
1928) -> Result<Option<iota_types::transaction_executor::CachedTransactionData>, IotaError> {
1929 let cache = validator_state.get_transaction_cache_reader();
1930 let Some(effects) = cache.try_get_executed_effects(digest)? else {
1931 return Ok(None);
1932 };
1933
1934 let events = if include_events && effects.events_digest().is_some() {
1935 Some(validator_state.get_transaction_events(digest)?)
1936 } else {
1937 None
1938 };
1939
1940 let input_objects = if include_input_objects {
1941 Some(
1942 validator_state
1943 .get_transaction_input_objects(&effects)
1944 .map_err(|e| IotaError::Unknown(format!("input objects: {e:?}")))?,
1945 )
1946 } else {
1947 None
1948 };
1949 let output_objects = if include_output_objects {
1950 Some(
1951 validator_state
1952 .get_transaction_output_objects(&effects)
1953 .map_err(|e| IotaError::Unknown(format!("output objects: {e:?}")))?,
1954 )
1955 } else {
1956 None
1957 };
1958
1959 Ok(Some(
1960 iota_types::transaction_executor::CachedTransactionData {
1961 effects,
1962 events,
1963 input_objects,
1964 output_objects,
1965 },
1966 ))
1967}
1968
1969type InFlightSubmissionResult = Result<Arc<QuorumTransactionResponse>, QuorumDriverError>;
1976
1977type InFlightTransactions =
1981 Arc<Mutex<HashMap<TransactionDigest, watch::Sender<Option<InFlightSubmissionResult>>>>>;
1982
1983enum TransactionSubmission {
1988 Driving(TransactionSubmissionGuard),
1989 AlreadyInFlight(watch::Receiver<Option<InFlightSubmissionResult>>),
1990}
1991
1992struct TransactionSubmissionGuard {
2003 in_flight_transactions: InFlightTransactions,
2004 tx_digest: TransactionDigest,
2005}
2006
2007impl TransactionSubmissionGuard {
2008 fn acquire(
2009 in_flight_transactions: InFlightTransactions,
2010 tx_digest: TransactionDigest,
2011 ) -> TransactionSubmission {
2012 {
2013 let mut in_flight = in_flight_transactions.lock();
2014 match in_flight.entry(tx_digest) {
2015 Entry::Occupied(entry) => {
2016 return TransactionSubmission::AlreadyInFlight(entry.get().subscribe());
2017 }
2018 Entry::Vacant(entry) => {
2019 let (sender, _initial_receiver) = watch::channel(None);
2020 entry.insert(sender);
2021 debug!(?tx_digest, "added transaction to in-flight map");
2022 }
2023 }
2024 }
2025 TransactionSubmission::Driving(Self {
2026 in_flight_transactions,
2027 tx_digest,
2028 })
2029 }
2030
2031 fn publish(&self, result: InFlightSubmissionResult) {
2036 if let Some(sender) = self.in_flight_transactions.lock().get(&self.tx_digest) {
2037 sender.send_replace(Some(result));
2038 }
2039 }
2040}
2041
2042impl Drop for TransactionSubmissionGuard {
2043 fn drop(&mut self) {
2044 self.in_flight_transactions.lock().remove(&self.tx_digest);
2045 }
2046}
2047
2048#[cfg(test)]
2049mod tests {
2050 use super::*;
2051
2052 fn acquire_driving(
2053 in_flight: &InFlightTransactions,
2054 tx_digest: TransactionDigest,
2055 ) -> TransactionSubmissionGuard {
2056 match TransactionSubmissionGuard::acquire(in_flight.clone(), tx_digest) {
2057 TransactionSubmission::Driving(guard) => guard,
2058 TransactionSubmission::AlreadyInFlight(_) => {
2059 panic!("expected to acquire the driving submission")
2060 }
2061 }
2062 }
2063
2064 fn acquire_duplicate(
2065 in_flight: &InFlightTransactions,
2066 tx_digest: TransactionDigest,
2067 ) -> watch::Receiver<Option<InFlightSubmissionResult>> {
2068 match TransactionSubmissionGuard::acquire(in_flight.clone(), tx_digest) {
2069 TransactionSubmission::Driving(_) => {
2070 panic!("expected the digest to already be in flight")
2071 }
2072 TransactionSubmission::AlreadyInFlight(receiver) => receiver,
2073 }
2074 }
2075
2076 #[tokio::test]
2077 async fn duplicate_submission_receives_published_outcome() {
2078 let in_flight = InFlightTransactions::default();
2079 let tx_digest = TransactionDigest::random();
2080
2081 let guard = acquire_driving(&in_flight, tx_digest);
2082 let mut receiver = acquire_duplicate(&in_flight, tx_digest);
2083
2084 guard.publish(Err(QuorumDriverError::TimeoutBeforeFinality));
2085 drop(guard);
2086
2087 let outcome = receiver
2090 .wait_for(|outcome| outcome.is_some())
2091 .await
2092 .expect("outcome was published before the sender dropped")
2093 .clone()
2094 .expect("wait_for only returns once the outcome is Some");
2095 assert!(matches!(
2096 outcome,
2097 Err(QuorumDriverError::TimeoutBeforeFinality)
2098 ));
2099 assert!(
2100 in_flight.lock().is_empty(),
2101 "guard drop must remove the in-flight entry"
2102 );
2103 }
2104
2105 #[tokio::test]
2106 async fn duplicate_subscribing_after_publish_receives_outcome() {
2107 let in_flight = InFlightTransactions::default();
2108 let tx_digest = TransactionDigest::random();
2109
2110 let guard = acquire_driving(&in_flight, tx_digest);
2111 guard.publish(Err(QuorumDriverError::TimeoutBeforeFinality));
2112
2113 let mut receiver = acquire_duplicate(&in_flight, tx_digest);
2117 drop(guard);
2118
2119 let outcome = receiver
2120 .wait_for(|outcome| outcome.is_some())
2121 .await
2122 .expect("the outcome is stored in the channel regardless of subscribers")
2123 .clone()
2124 .expect("wait_for only returns once the outcome is Some");
2125 assert!(matches!(
2126 outcome,
2127 Err(QuorumDriverError::TimeoutBeforeFinality)
2128 ));
2129 }
2130
2131 #[tokio::test]
2132 async fn dropped_guard_without_outcome_closes_channel() {
2133 let in_flight = InFlightTransactions::default();
2134 let tx_digest = TransactionDigest::random();
2135
2136 let guard = acquire_driving(&in_flight, tx_digest);
2137 let mut receiver = acquire_duplicate(&in_flight, tx_digest);
2138 drop(guard);
2139
2140 receiver
2141 .wait_for(|outcome| outcome.is_some())
2142 .await
2143 .expect_err("dropping the guard without publishing must close the channel");
2144 assert!(in_flight.lock().is_empty());
2145
2146 let _guard = acquire_driving(&in_flight, tx_digest);
2148 }
2149
2150 async fn build_orchestrator_with_pcool(
2151 enable_pcool: bool,
2152 ) -> (
2153 Arc<AuthorityState>,
2154 TransactionOrchestrator<NetworkAuthorityClient>,
2155 tempfile::TempDir,
2156 tokio::sync::broadcast::Sender<IotaSystemState>,
2157 ) {
2158 use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
2159
2160 use crate::{
2161 authority::test_authority_builder::TestAuthorityBuilder,
2162 authority_aggregator::AuthorityAggregatorBuilder,
2163 };
2164
2165 telemetry_subscribers::init_for_testing();
2166 let network_config =
2167 iota_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
2168
2169 let mut protocol_config =
2170 ProtocolConfig::get_for_version(ProtocolVersion::MAX, Chain::Unknown);
2171 protocol_config.set_enable_pcool_flow_for_testing(enable_pcool);
2172 let state = TestAuthorityBuilder::new()
2173 .with_network_config(&network_config, 0)
2174 .with_protocol_config(protocol_config)
2175 .build()
2176 .await;
2177
2178 let (aggregator, _clients) =
2179 AuthorityAggregatorBuilder::from_genesis(&network_config.genesis)
2180 .build_network_clients();
2181 let (reconfig_tx, reconfig_rx) = tokio::sync::broadcast::channel(16);
2182 let tempdir = tempfile::tempdir().unwrap();
2183 let orchestrator = TransactionOrchestrator::new_with_auth_aggregator(
2184 Arc::new(aggregator),
2185 state.clone(),
2186 reconfig_rx,
2187 tempdir.path(),
2188 &Registry::new(),
2189 None,
2190 );
2191 (state, orchestrator, tempdir, reconfig_tx)
2192 }
2193
2194 #[tokio::test(flavor = "multi_thread")]
2197 async fn qd_recovery_eager_on_flag_off_boot() {
2198 let (state, orchestrator, _tempdir, _reconfig_tx) =
2199 build_orchestrator_with_pcool(false).await;
2200 assert!(orchestrator.quorum_driver().is_some());
2201 assert!(
2202 orchestrator
2203 .select_driver_for_testing(&state.epoch_store_for_testing())
2204 .is_ok()
2205 );
2206 }
2207
2208 #[tokio::test(flavor = "multi_thread")]
2211 async fn flag_on_boot_rejects_selection_after_rollback() {
2212 use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
2213
2214 let (state, orchestrator, _tempdir, _reconfig_tx) =
2215 build_orchestrator_with_pcool(true).await;
2216 assert!(orchestrator.quorum_driver().is_none());
2217
2218 assert!(
2220 orchestrator
2221 .select_driver_for_testing(&state.epoch_store_for_testing())
2222 .is_ok()
2223 );
2224
2225 let mut protocol_config =
2228 ProtocolConfig::get_for_version(ProtocolVersion::MAX, Chain::Unknown);
2229 protocol_config.set_enable_pcool_flow_for_testing(false);
2230 state
2231 .reconfigure_for_testing_with_protocol_config(protocol_config)
2232 .await;
2233 let epoch_store = state.epoch_store_for_testing();
2234 assert_eq!(epoch_store.epoch(), 1);
2235
2236 assert!(matches!(
2237 orchestrator.select_driver_for_testing(&epoch_store),
2238 Err(QuorumDriverError::QuorumDriverInternal(_))
2239 ));
2240 assert!(orchestrator.subscribe_to_effects_queue().is_none());
2241 }
2242
2243 #[test]
2249 fn map_validator_rejections_to_distinct_variant() {
2250 use iota_types::error::ErrorCategory;
2251
2252 use crate::transaction_driver::AggregatedRequestErrors;
2253
2254 let td_error = TransactionDriverError::RejectedByValidators {
2255 submission_non_retriable_errors: AggregatedRequestErrors {
2256 errors: vec![
2257 (
2258 "Object lock conflict".to_string(),
2259 vec![],
2260 2500,
2261 ErrorCategory::LockConflict,
2262 ),
2263 (
2264 "Gas budget: 10 is lower than min: 1000000".to_string(),
2265 vec![],
2266 834,
2267 ErrorCategory::InvalidTransaction,
2268 ),
2269 ],
2270 total_stake: 3334,
2271 stake_requested_retry_after: Default::default(),
2272 },
2273 submission_retriable_errors: AggregatedRequestErrors::default(),
2274 };
2275
2276 let qd_error = map_td_error_to_qd(td_error);
2277 let QuorumDriverError::RejectedByValidators { errors } = &qd_error else {
2278 panic!("expected RejectedByValidators, got {qd_error:?}");
2279 };
2280 assert_eq!(
2281 errors,
2282 &[
2283 "Object lock conflict".to_string(),
2284 "Gas budget: 10 is lower than min: 1000000".to_string(),
2285 ]
2286 );
2287 assert_eq!(qd_error.reason(), "rejected_by_validators");
2288 }
2289
2290 fn timeout_with_last_error(last_error: Option<TransactionDriverError>) -> QuorumDriverError {
2291 map_td_error_to_qd(TransactionDriverError::TimeoutWithLastRetriableError {
2292 last_error: last_error.map(Box::new),
2293 attempts: 3,
2294 timeout: std::time::Duration::from_secs(60),
2295 })
2296 }
2297
2298 fn aborted_with_retriable(retriable_errors: AggregatedRequestErrors) -> TransactionDriverError {
2299 TransactionDriverError::Aborted {
2300 submission_non_retriable_errors: AggregatedRequestErrors::default(),
2301 submission_retriable_errors: retriable_errors,
2302 observed_effects_digests: crate::transaction_driver::AggregatedEffectsDigests {
2303 digests: vec![],
2304 },
2305 }
2306 }
2307
2308 fn bucket(
2309 msg: &str,
2310 stake: u64,
2311 category: iota_types::error::ErrorCategory,
2312 ) -> (
2313 String,
2314 Vec<iota_types::base_types::AuthorityName>,
2315 u64,
2316 iota_types::error::ErrorCategory,
2317 ) {
2318 (msg.to_string(), vec![], stake, category)
2319 }
2320
2321 #[test]
2324 fn map_overloaded_timeout_to_retry_after() {
2325 use std::collections::BTreeMap;
2326
2327 use iota_types::error::ErrorCategory;
2328
2329 let qd_error =
2330 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2331 errors: vec![
2332 bucket("validator unavailable", 4000, ErrorCategory::Unavailable),
2333 bucket("overloaded 10s", 2500, ErrorCategory::ValidatorOverloaded),
2334 bucket("overloaded 30s", 2500, ErrorCategory::ValidatorOverloaded),
2335 ],
2336 total_stake: 9000,
2337 stake_requested_retry_after: BTreeMap::from([(10, 2500), (30, 2500)]),
2338 })));
2339 let QuorumDriverError::SystemOverloadRetryAfter {
2340 overload_stake,
2341 retry_after_secs,
2342 ..
2343 } = qd_error
2344 else {
2345 panic!("expected SystemOverloadRetryAfter, got {qd_error:?}");
2346 };
2347 assert_eq!(overload_stake, 5000);
2348 assert_eq!(retry_after_secs, 30);
2349 }
2350
2351 #[test]
2353 fn map_overloaded_timeout_ignores_low_stake_outlier() {
2354 use std::collections::BTreeMap;
2355
2356 use iota_types::error::ErrorCategory;
2357
2358 let qd_error =
2359 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2360 errors: vec![
2361 bucket("overloaded 10s", 7000, ErrorCategory::ValidatorOverloaded),
2362 bucket("overloaded 3600s", 1000, ErrorCategory::ValidatorOverloaded),
2363 ],
2364 total_stake: 8000,
2365 stake_requested_retry_after: BTreeMap::from([(10, 7000), (3600, 1000)]),
2366 })));
2367 assert!(
2368 matches!(
2369 qd_error,
2370 QuorumDriverError::SystemOverloadRetryAfter {
2371 retry_after_secs: 10,
2372 ..
2373 }
2374 ),
2375 "expected the 10s majority hint, got {qd_error:?}"
2376 );
2377 }
2378
2379 #[test]
2382 fn map_overloaded_timeout_without_hint_and_non_overload() {
2383 use iota_types::error::ErrorCategory;
2384
2385 let qd_error =
2386 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2387 errors: vec![bucket(
2388 "too many transactions pending",
2389 5000,
2390 ErrorCategory::ValidatorOverloaded,
2391 )],
2392 total_stake: 5000,
2393 stake_requested_retry_after: Default::default(),
2394 })));
2395 assert!(
2396 matches!(qd_error, QuorumDriverError::SystemOverload { overloaded_stake, .. } if overloaded_stake == 5000),
2397 "expected SystemOverload, got {qd_error:?}"
2398 );
2399
2400 let qd_error =
2402 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2403 errors: vec![
2404 bucket(
2405 "too many transactions pending",
2406 6000,
2407 ErrorCategory::ValidatorOverloaded,
2408 ),
2409 bucket("overloaded 3600s", 100, ErrorCategory::ValidatorOverloaded),
2410 ],
2411 total_stake: 6100,
2412 stake_requested_retry_after: std::collections::BTreeMap::from([(3600, 100)]),
2413 })));
2414 assert!(
2415 matches!(qd_error, QuorumDriverError::SystemOverload { overloaded_stake, .. } if overloaded_stake == 6100),
2416 "expected SystemOverload without a hint, got {qd_error:?}"
2417 );
2418
2419 let qd_error =
2420 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2421 errors: vec![
2422 bucket("overloaded 10s", 3500, ErrorCategory::ValidatorOverloaded),
2423 bucket("timed out submitting", 3000, ErrorCategory::Unavailable),
2424 bucket(
2425 "timed out getting effects",
2426 2500,
2427 ErrorCategory::Unavailable,
2428 ),
2429 ],
2430 total_stake: 9000,
2431 stake_requested_retry_after: Default::default(),
2432 })));
2433 assert!(matches!(
2434 qd_error,
2435 QuorumDriverError::TimeoutBeforeFinalityWithErrors { .. }
2436 ));
2437
2438 let qd_error =
2439 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2440 errors: vec![bucket(
2441 "validator unavailable",
2442 5000,
2443 ErrorCategory::Unavailable,
2444 )],
2445 total_stake: 5000,
2446 stake_requested_retry_after: Default::default(),
2447 })));
2448 assert!(matches!(
2449 qd_error,
2450 QuorumDriverError::TimeoutBeforeFinalityWithErrors { .. }
2451 ));
2452
2453 let qd_error = timeout_with_last_error(None);
2454 assert!(matches!(qd_error, QuorumDriverError::TimeoutBeforeFinality));
2455 }
2456
2457 #[test]
2460 fn map_timeout_to_last_attempt_errors() {
2461 use iota_types::error::ErrorCategory;
2462
2463 let qd_error =
2464 timeout_with_last_error(Some(aborted_with_retriable(AggregatedRequestErrors {
2465 errors: vec![
2466 bucket(
2467 "Could not find the referenced object 0x1 at version 5",
2468 5000,
2469 ErrorCategory::Aborted,
2470 ),
2471 bucket("validator unavailable", 5000, ErrorCategory::Unavailable),
2472 ],
2473 total_stake: 10000,
2474 stake_requested_retry_after: Default::default(),
2475 })));
2476 let QuorumDriverError::TimeoutBeforeFinalityWithErrors { last_error } = &qd_error else {
2477 panic!("expected TimeoutBeforeFinalityWithErrors, got {qd_error:?}");
2478 };
2479 assert!(
2480 last_error.contains("Could not find the referenced object 0x1 at version 5"),
2481 "{last_error}"
2482 );
2483 assert_eq!(qd_error.reason(), "timeout_before_finality");
2484 }
2485}