Skip to main content

iota_core/
transaction_orchestrator.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5// Transaction Orchestrator is a Node component that utilizes Quorum Driver or
6// TransactionDriver (selected per request by the P-COOL protocol flag) to
7// submit transactions to validators for finality, and proactively executes
8// finalized transactions locally.
9
10use 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
84/// The submission flow for a transaction, selected per request by the
85/// epoch's P-COOL flag.
86enum Driver<A: Clone> {
87    /// Certificate-based flow (P-COOL disabled).
88    Quorum(Arc<QuorumDriverHandler<A>>),
89    /// Direct-to-consensus P-COOL flow.
90    Transaction(Arc<TransactionDriver<A>>),
91}
92
93/// Transaction Orchestrator is a Node component that supports both QuorumDriver
94/// and TransactionDriver for submitting transactions to validators for
95/// finality. It adds inflight deduplication, waiting for local execution,
96/// recovery, and epoch change handling.
97///
98/// The epoch's P-COOL flag selects the flow serving a request. The
99/// TransactionDriver always exists, so it tracks epochs before the flag
100/// enables it. The QuorumDriver exists only when the node booted with
101/// P-COOL disabled and ran WAL recovery then; after a rollback a node
102/// booted under P-COOL must be restarted.
103pub struct TransactionOrchestrator<A: Clone> {
104    quorum_driver: Option<Arc<QuorumDriverHandler<A>>>,
105    transaction_driver: Arc<TransactionDriver<A>>,
106    validator_state: Arc<AuthorityState>,
107    /// Handle to the pending-tx-log cleanup loop; present only with the
108    /// quorum driver.
109    _local_executor_handle: Option<JoinHandle<()>>,
110    pending_tx_log: Arc<WritePathPendingTransactionLog>,
111    /// Digests currently being driven to finality by the TransactionDriver;
112    /// used to deduplicate concurrent submissions of the same transaction,
113    /// with a channel per digest through which the driving submission
114    /// publishes its outcome to concurrent duplicates. Kept in memory only:
115    /// the driver path is best-effort, so there is nothing to recover after
116    /// a restart. The QuorumDriver path tracks its submissions in
117    /// `pending_tx_log` instead.
118    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        // Registered for both flows even when the quorum driver is not
184        // built, so metric presence does not depend on the boot mode.
185        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            // The cleanup loop must exist before WAL recovery runs, so a
200            // recovered transaction cannot complete before its receiver
201            // exists.
202            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        // `Weak` so detached driver tasks cannot pin the authority state.
212        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    /// Returns the flow selected by `epoch_store`'s P-COOL flag. Call with
250    /// the request's own epoch store snapshot so the flag and the submission
251    /// see the same epoch. Errors when the quorum driver is selected on a
252    /// node that booted under P-COOL: such a node never ran WAL recovery and
253    /// must be restarted.
254    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        // Captured before `request` moves so the skip-cert reconcile reads
297        // caller intent, not whatever the submitter happened to return — a
298        // Byzantine submitter could otherwise censor a field by returning
299        // `None`.
300        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        // Responses built from the local store read the object versions the
311        // transaction superseded, which the expiry of historic object buckets
312        // drops once their epoch falls out of the object retention.
313        let _object_retention = (include_input_objects || include_output_objects)
314            .then(|| self.validator_state.retain_objects());
315
316        // A resubmission of an already-executed transaction is answered from
317        // the local cache instead of being driven through the validators
318        // again.
319        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            // Executed here. A caller waiting for local execution also gets
332            // the read-after-write guarantee, so for them it is only confirmed
333            // once its checkpoint is indexed too, as below. A transaction
334            // executed in an earlier epoch has its checkpoint indexed already:
335            // an epoch ends only once all its checkpoints are executed, each
336            // after its index commit. The current epoch's table knows only the
337            // current epoch's transactions, so it is not asked for those.
338            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        // Reject malformed transactions before either driver inspects shared
349        // inputs or `MoveAuthenticator`. Runs after the cache lookup so that,
350        // as on the upstream flow, a resubmission of an executed transaction
351        // gets its cached results even if it no longer passes the current
352        // epoch's checks (e.g. its expiration epoch has passed).
353        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                    // Detached so a client disconnect (this future dropped) does
364                    // not cancel a submission that may already be in consensus;
365                    // the task drives the transaction to finality on its own.
366                    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                    // Detached for the same reason as above.
381                    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        // `needs_cache_rebuild` is derived from finality, not caller intent:
409        // the QD fallback path returns `Certified` and a duplicate
410        // submission inheriting the outcome of an in-flight certifying
411        // submission returns `QuorumExecuted` — neither needs a rebuild —
412        // even when the caller asked for `WaitForLocalExecution`, while only
413        // the TD skip-cert engine produces `UncertifiedSingleValidator`. The
414        // checkpoint sequence comes from `submit_with_checkpoint_race`, which
415        // relies on `executed_transactions_to_checkpoint` being written
416        // strictly after every tx's effects — so a `Some(seq)` here implies
417        // the cache has authoritative effects.
418        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                // Timed out waiting for the tx to land in a local checkpoint.
428                // In this branch `response` is either `None` (recovery) or
429                // `UncertifiedSingleValidator` (TD skip-cert) — both must
430                // surface as `TimeoutBeforeFinality` rather than leaking
431                // uncorroborated single-validator effects to the client.
432                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            // The checkpoint is final, but a read right after the response
457            // sees the transaction only once the indexes include it.
458            self.validator_state
459                .is_checkpoint_indexed(seq)
460                .unwrap_or(false)
461        } else {
462            // The response is already 2f+1 certified — from the QuorumDriver,
463            // or inherited by a duplicate submission from an in-flight
464            // certifying submission — so just confirm local execution
465            // finished. Both waits share one deadline: a fullnode often
466            // executes the transaction only with its checkpoint, so a shorter
467            // limit on the first wait would give up before the second could
468            // succeed.
469            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            // Only the checkpoint indexes the transaction, so wait for it too
480            // for an index-backed read after the response to see it.
481            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        // Safety guard: `UncertifiedSingleValidator` finality carries effects
495        // from the single submitting validator only — they MUST NOT reach the
496        // client without first being corroborated against the local cache. The
497        // reachable paths today all either upgrade finality via
498        // `reconcile_effects_from_cache` / `build_response_from_cache`, or
499        // branch to `TimeoutBeforeFinality`; this guard is the last-chance
500        // fallback for a future refactor that forgets to reconcile. Do not
501        // remove as dead code.
502        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    /// Replace the response's effects, events, and input/output objects with
520    /// the authoritative copies derived from the local cache — the local
521    /// checkpoint executor has processed the tx, so the cache has the real
522    /// data and the TD-returned (single-validator) copies can be discarded.
523    ///
524    /// `tx_digest` must be the digest of the caller's original transaction,
525    /// not the digest carried in `response.effects.effects` — a byzantine
526    /// submitter could set the latter to an unrelated (already-executed) tx
527    /// so we'd read unrelated effects from the cache.
528    ///
529    /// The caller must have obtained `checkpoint_seq` from
530    /// `wait_for_checkpoint_inclusion` (not just `get_transaction_checkpoint`),
531    /// because that function guarantees both the effects write and the
532    /// checkpoint-mapping write have landed — it's the only way to avoid the
533    /// race between `notify_read_executed_effects_digests` (fires per-tx) and
534    /// `insert_finalized_transactions` (fires per-checkpoint, after the
535    /// `CheckpointExecutor` has awaited every tx in that checkpoint).
536    ///
537    /// Upgrades the finality info to `Checkpointed(epoch, checkpoint_seq)`. A
538    /// warning is logged if the TD-returned effects digest diverges from the
539    /// cache digest, or the submitter claimed events the cache doesn't have
540    /// (byzantine submitter or bug).
541    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    /// Whether `inclusion`, from `wait_for_checkpoint_inclusion`, puts
584    /// `digest` in a checkpoint the RPC indexes include, so that a read sent
585    /// right after sees it.
586    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    /// Build a skip-effect-certification response entirely from the local
599    /// cache. The caller must have already obtained `checkpoint_seq` via
600    /// `wait_for_checkpoint_inclusion`, which is supposed to guarantee that
601    /// the effects write and the checkpoint-mapping write have both landed. A
602    /// missing cache entry here would mean a transient races we observed in
603    /// practice; mapped to `TimeoutBeforeFinality` so the client retries
604    /// rather than seeing a misleading `QuorumDriverInternal`.
605    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            // Checkpoint inclusion is supposed to guarantee the cache has
627            // effects, but we've seen transient misses; surface as a retriable
628            // timeout rather than an internal error.
629            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    /// Build a response from the local cache for a transaction that has
657    /// already been executed on this node. Returns `Ok(None)` when the
658    /// transaction has not been executed locally. Unlike
659    /// `build_response_from_cache`, no checkpoint sequence is required: local
660    /// effects only exist for finalized transactions, so the response is
661    /// tagged `QuorumExecuted`.
662    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    // Utilize the handle_certificate_v1 validator api to request input/output
701    // objects
702    #[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        // A resubmission of an already-executed transaction is answered from
718        // the local cache instead of being driven through the validators
719        // again.
720        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        // Reject malformed transactions before either driver inspects shared
736        // inputs or `MoveAuthenticator`. Runs after the cache lookup so that,
737        // as on the upstream flow, a resubmission of an executed transaction
738        // gets its cached results even if it no longer passes the current
739        // epoch's checks (e.g. its expiration epoch has passed).
740        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                // v1 does not do an internal wait; callers (e.g. the gRPC
749                // execution service) are responsible for their own
750                // `wait_for_checkpoint_inclusion` when they need it, and will
751                // reconcile the response from the cache there.
752                //
753                // Detached so a client disconnect does not cancel a submission
754                // that may already be in consensus.
755                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    /// Submit on the skip-effect-certification path while concurrently
775    /// waiting for local checkpoint inclusion. The race is asymmetric:
776    ///
777    /// - If the **checkpoint** future resolves first (slow driver, e.g. stuck corroborating a
778    ///   Byzantine validator's rejection), the driver future is dropped and the caller rebuilds the
779    ///   response from the local cache.
780    /// - If the **driver** returns first, its result is taken and the checkpoint future is awaited
781    ///   to completion (up to the shared `WAIT_FOR_FINALITY_TIMEOUT`) before returning, so the
782    ///   caller has a checkpoint sequence to reconcile against.
783    ///
784    /// Returns `(response, seq)` where `response` is `Some` when the driver
785    /// returned a result (which may carry `UncertifiedSingleValidator`
786    /// finality requiring rebuild) and `seq` is the checkpoint sequence if
787    /// either future yielded it.
788    ///
789    /// Run inside a detached task so a client disconnect cannot cancel the
790    /// race before the checkpoint-sequence bookkeeping completes.
791    #[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            // `SubmittedButFetchFailed` is retriable (`ErrorCategory::Unavailable`)
828            // so the driver's outer loop reissues submission internally and
829            // only returns here as `Ok`, `TimeoutWithLastRetriableError`, or
830            // a non-retriable error like `RejectedByValidators`.
831            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                // Dropping the cancelled driver closes the in-flight outcome
839                // channel; duplicate submissions fall back to waiting for
840                // checkpoint inclusion, which this race winning guarantees
841                // resolves immediately.
842                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    /// Submit a transaction via the TransactionDriver (P-COOL flow).
851    ///
852    /// With `skip_certification = true` the driver may return
853    /// `UncertifiedSingleValidator` effects without a 2f+1 broadcast. The
854    /// caller (gRPC `execute_transactions` or `execute_transaction_block`)
855    /// is then responsible for `wait_for_checkpoint_inclusion` and the
856    /// cache-rebuild gate that replaces those single-validator effects with
857    /// authoritative data — uncertified data must never reach the client.
858    /// See `corroborate_single_validator_error` for the per-submission
859    /// fetch-failure recovery flow inside the driver.
860    ///
861    /// Run inside a detached task so a client disconnect cannot cancel a
862    /// `drive_transaction` call that may already be in consensus.
863    #[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        // Deduplicate concurrent submissions of the same digest: only the
876        // first caller drives the committee-wide submission and publishes its
877        // outcome; the rest await that outcome. The guard removes the digest
878        // from the in-flight map on every exit path (success, error, timeout,
879        // or cancellation) when it is dropped.
880        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        // This call runs inside a task detached from the caller, so the
902        // outcome is logged here rather than left to the caller — a
903        // disconnected client's continuation never runs and would
904        // otherwise never observe it.
905        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        // Dropping the guard closes the channel, releasing its copy of the
932        // response unless a duplicate submission still holds a receiver — in
933        // the common no-duplicate case the response is then moved into the
934        // reply instead of cloned.
935        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    /// Build a caller-specific response from a driver response, honoring the
942    /// caller's include flags.
943    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    /// Await the outcome of an already in-flight submission of `tx_digest`
973    /// instead of starting a second committee-wide submission for the same
974    /// transaction. Resolves to that submission's outcome — running the
975    /// effects-certification step first if the outcome does not satisfy this
976    /// caller — falls back to waiting for checkpoint inclusion if the
977    /// driving submission went away without publishing one (checkpoint-race
978    /// cancellation, panic, or shutdown), and returns
979    /// `TimeoutBeforeFinality` if nothing is published within
980    /// `WAIT_FOR_FINALITY_TIMEOUT` or the follow-up effects certification
981    /// does not complete within another `WAIT_FOR_FINALITY_TIMEOUT`.
982    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        // The `Ref` returned by `wait_for` is a read guard and must not be
992        // held across an await, so the outcome is cloned out before
993        // branching. `wait_for` only returns a value matching its predicate,
994        // so `Some` is guaranteed on success; a closed channel yields `None`.
995        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            // Channel closed without an outcome: the driving submission went
1006            // away without publishing — routinely because its checkpoint
1007            // race observed the transaction in a local checkpoint and
1008            // cancelled it, exceptionally on panic or shutdown. Checkpoint
1009            // inclusion is the remaining signal of the outcome.
1010            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            // The in-flight submission already drove the transaction into
1021            // consensus; only the 2f+1 effects certification is missing for
1022            // this caller. Certify the effects directly instead of starting
1023            // a second committee-wide submission or waiting for a checkpoint
1024            // inclusion the caller never asked for. `certify_transaction` is
1025            // internally bounded only by committee size times its per-request
1026            // timeout, so cap it to the same client-facing budget the driving
1027            // submission gets for its whole `drive_transaction` call.
1028            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    /// Wait for `tx_digest` to reach a local checkpoint and build the
1051    /// response from the authoritative cache. The fallback outcome signal
1052    /// for a duplicate whose driving submission went away without
1053    /// publishing; the result carries `Checkpointed` finality, so it
1054    /// satisfies every caller. Times out with `TimeoutBeforeFinality` if
1055    /// the transaction does not get checkpointed in time.
1056    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        // The caller has typically already waited a full timeout on the
1063        // outcome channel, but this wait is still required: it is what
1064        // yields the checkpoint sequence and guarantees the checkpoint
1065        // mapping write has landed (see `reconcile_effects_from_cache`).
1066        // When the transaction is already checkpointed — the routine reason
1067        // the fallback fires — it resolves immediately; only after a
1068        // driving-task death does it actually wait, as the last remaining
1069        // signal of the outcome.
1070        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    /// Submit a transaction via the QuorumDriver. `transaction` must be the
1087    /// signature-verified form of `request.transaction`, and the caller must
1088    /// have run `validity_check` on it beforehand.
1089    #[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        // TODO: refactor all the gauge and timer metrics with `monitored_scope`
1119        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    /// Submits the transaction to Quorum Driver for execution.
1164    /// Returns an awaitable Future.
1165    #[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        // TODO(william) need to also write client adr to pending tx log below
1177        // so that we can re-execute with this client addr if we restart
1178        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        // It's possible that the transaction effects is already stored in DB at this
1189        // point. So we also subscribe to that. If we hear from `effects_await`
1190        // first, it means the ticket misses the previous notification, and we
1191        // want to ask quorum driver to form a certificate for us again, to
1192        // serve this request.
1193        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-and-return necessary to satisfy borrow checker.
1203            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    // TODO: Potentially cleanup this function and pending transaction log.
1286    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    /// Returns the quorum driver, or `None` when the node booted under
1321    /// P-COOL. Test-only: submissions must go through the per-request
1322    /// driver selection.
1323    #[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    /// Owned variant of [`Self::quorum_driver`].
1329    #[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    /// Returns the transaction driver's aggregator; it always exists and its
1335    /// reconfig observer keeps it current.
1336    pub fn clone_authority_aggregator(&self) -> Arc<AuthorityAggregator<A>> {
1337        self.transaction_driver.authority_aggregator().load_full()
1338    }
1339
1340    /// Returns an effects receiver only while the quorum driver is the
1341    /// currently selected flow and this node can serve it; the P-COOL flow
1342    /// has no effects broadcast.
1343    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    /// Runs driver selection for `epoch_store` and reports its outcome.
1354    #[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                // TODO: ideally pending_tx_log would not contain VerifiedTransaction, but that
1406                // requires a migration.
1407                let tx = tx.into_inner();
1408                let tx_digest = *tx.digest();
1409                // It's not impossible we fail to enqueue a task but that's not the end of
1410                // world. TODO(william) correctly extract client_addr from logs
1411                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            // Transactions will be cleaned up in
1436            // loop_execute_finalized_tx_locally() after they
1437            // produce effects.
1438        });
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    /// Reports whether a driver submission of `tx_digest` is in flight, and
1446    /// if so how many duplicate submissions are awaiting its outcome.
1447    #[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
1456/// Convert a `QuorumDriverResponse` (contains
1457/// `VerifiedCertifiedTransactionEffects`) to the V1 response format that uses
1458/// `FinalizedEffects`.
1459fn 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
1476/// Convert a `transaction_driver_types::FinalizedEffects` into a
1477/// `quorum_driver_types::FinalizedEffects`.
1478fn 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
1495/// Map a `TransactionDriverError` to a `QuorumDriverError` for client
1496/// reporting. The variant choice signals retriability: clients retry on
1497/// `QuorumDriverInternal`, `FailedWithTransientErrorAfterMaximumAttempts`,
1498/// and `TimeoutBeforeFinality`, but treat `InvalidTransaction`,
1499/// `InvalidUserSignature`, and `RejectedByValidators` as terminal.
1500/// An overload-dominated timeout maps to the retriable `SystemOverload` or
1501/// `SystemOverloadRetryAfter` instead of `TimeoutBeforeFinality`. Any other
1502/// timeout after a failed attempt carries that attempt's error.
1503/// Submission-time rejections that cannot succeed on resubmission must
1504/// therefore not be reported as internal.
1505fn 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            // f+1 stake of validators returned non-retriable errors during
1524            // submission (bad signature, malformed tx, lock conflict, ...),
1525            // or a quorum rejected it for an input object they do not have.
1526            // Some verdicts depend on validator-local state, so this does not
1527            // prove the transaction can never execute; the variant is kept
1528            // distinct from `InvalidTransaction` so clients can tell the two
1529            // apart. The driver sorts the messages by stake.
1530            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            // Driver exhausted the validator list without reaching the f+1
1544            // non-retriable threshold — most failures were transient
1545            // (validator down, network, overload). Surface as retriable so
1546            // the client can resubmit.
1547            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            // Validators disagree on effects digests — a protocol-level
1555            // invariant violation, never a client retry case. Log loud so
1556            // on-call sees it; surface as internal.
1557            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
1574/// Maps an overload-dominated aborted attempt to an overload error, so the
1575/// validators' retry hints reach the client instead of a generic timeout.
1576fn 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
1630/// Await a detached submission task, surfacing a task panic as an internal
1631/// error.
1632async 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/// Prometheus metrics which can be displayed in Grafana, queried and alerted on
1643#[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    // Bumped when the skip-effect-certification path reconciles against the
1666    // local cache but the cache has no events for a tx the single submitter
1667    // claimed had events. Uncertified events are rejected and the request
1668    // fails via the safety guard.
1669    skip_effect_cert_events_cache_miss: GenericCounter<AtomicU64>,
1670
1671    // Bumped when local checkpoint inclusion completes before the TD
1672    // skip-effect-certification call returns. Indicates the driver was slow
1673    // (e.g., corroborating a single-validator rejection) and the checkpoint
1674    // race cancelled the in-flight driver work in favor of rebuilding from
1675    // the local cache.
1676    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
1686// Note that labeled-metrics are stored upfront individually
1687// to mitigate the perf hit by MetricsVec.
1688// See https://github.com/tikv/rust-prometheus/tree/master/static-metric
1689impl 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(&registry)
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    /// Wait for the given transactions to be included in a checkpoint.
1882    ///
1883    /// Returns a mapping from transaction digest to
1884    /// `(checkpoint_sequence_number, checkpoint_timestamp_ms)`.
1885    /// On timeout, returns partial results for any transactions that were
1886    /// already checkpointed.
1887    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
1918/// Read a transaction's authoritative data from the local cache. Returns
1919/// `Ok(None)` if the tx hasn't been executed locally yet. Shared by the
1920/// orchestrator's skip-cert response builder and the `TransactionExecutor`
1921/// trait method consumed by the gRPC handler.
1922fn 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
1969/// Successful outcome of an in-flight driver submission: the unfiltered
1970/// driver response, or the local checkpoint the transaction was observed in
1971/// when the checkpoint race cancelled the driver (the cache is then the
1972/// authoritative source of the effects).
1973/// Outcome of an in-flight driver submission, shared with concurrent
1974/// submissions of the same digest.
1975type InFlightSubmissionResult = Result<Arc<QuorumTransactionResponse>, QuorumDriverError>;
1976
1977/// Digests currently being driven to finality by the TransactionDriver,
1978/// each with a channel through which the driving submission publishes its
1979/// outcome to concurrent duplicates.
1980type InFlightTransactions =
1981    Arc<Mutex<HashMap<TransactionDigest, watch::Sender<Option<InFlightSubmissionResult>>>>>;
1982
1983/// Result of trying to register a submission of a digest in the in-flight
1984/// map: either this caller drives the committee-wide submission, or another
1985/// submission of the same digest is already in flight and this caller should
1986/// await its published outcome instead.
1987enum TransactionSubmission {
1988    Driving(TransactionSubmissionGuard),
1989    AlreadyInFlight(watch::Receiver<Option<InFlightSubmissionResult>>),
1990}
1991
1992/// Tracks a transaction that is being submitted to finality so that
1993/// concurrent submissions of the same digest deduplicate.
1994///
1995/// Held only by the driving submission, which must `publish` its outcome so
1996/// concurrent duplicates can return it. Dropping the guard removes the
1997/// digest from the in-flight map on every exit path (success, error,
1998/// timeout, and cancellation); receivers subscribed before removal still
1999/// observe a published outcome, and if the entry is removed without any
2000/// outcome (checkpoint-race cancellation, panic, or shutdown) the closed
2001/// channel tells duplicates to fall back to checkpoint inclusion.
2002struct 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    /// Publish the submission outcome to concurrent duplicate submissions.
2032    /// The outcome is stored in the channel even when nobody is subscribed
2033    /// yet, so a duplicate that subscribes after this call but before the
2034    /// entry is removed still reads it instead of a closed channel.
2035    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        // The published outcome must survive the guard drop for receivers
2088        // subscribed before the entry was removed.
2089        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        // Subscribing between the publish and the entry removal must still
2114        // resolve to the outcome; falling back to checkpoint inclusion here
2115        // would cost the duplicate a full finality timeout.
2116        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        // The digest can be driven again once the entry is gone.
2147        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    /// A flag-off boot builds the quorum driver and runs WAL recovery at
2195    /// construction.
2196    #[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    /// A node booted under P-COOL has no quorum driver. After a rollback it
2209    /// must reject quorum-driver selection until restarted.
2210    #[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        // Selection under the flag serves the P-COOL flow.
2219        assert!(
2220            orchestrator
2221                .select_driver_for_testing(&state.epoch_store_for_testing())
2222                .is_ok()
2223        );
2224
2225        // Epoch 1 with P-COOL off: selection and the effects queue must both
2226        // report the missing quorum driver.
2227        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    /// Validator rejections must map to `RejectedByValidators`, not
2244    /// `InvalidTransaction`: the verdicts can be validator-local, so clients
2245    /// must be able to tell them apart from a locally proven invalid
2246    /// transaction. Every grouped reason reaches the client in the driver's
2247    /// stake order.
2248    #[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    /// Overload split across delays still beats a larger single bucket. The
2322    /// 50/50 hint split resolves to the longer delay.
2323    #[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    /// A low-stake validator must not dictate the retry hint.
2352    #[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    /// Overload without hints maps to `SystemOverload`. Non-dominant or
2380    /// absent overload keeps mapping to `TimeoutBeforeFinality`.
2381    #[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        // A hint backed by a minority of the overloaded stake is not exposed.
2401        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    /// A timeout after a failed attempt reports that attempt's errors, so
2458    /// the client learns what kept the transaction from finalizing.
2459    #[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}