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, IsTransactionExecutedLocally,
39        QuorumDriverEffectsQueueResult, QuorumDriverError, QuorumDriverResponse,
40        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
82// How long to wait for local execution (including parents) before a timeout
83// is returned to client.
84const LOCAL_EXECUTION_TIMEOUT: Duration = Duration::from_secs(10);
85
86const WAIT_FOR_FINALITY_TIMEOUT: Duration = Duration::from_secs(30);
87
88/// The submission flow for a transaction, selected per request by the
89/// epoch's P-COOL flag.
90enum Driver<A: Clone> {
91    /// Certificate-based flow (P-COOL disabled).
92    Quorum(Arc<QuorumDriverHandler<A>>),
93    /// Direct-to-consensus P-COOL flow.
94    Transaction(Arc<TransactionDriver<A>>),
95}
96
97/// Transaction Orchestrator is a Node component that supports both QuorumDriver
98/// and TransactionDriver for submitting transactions to validators for
99/// finality. It adds inflight deduplication, waiting for local execution,
100/// recovery, and epoch change handling.
101///
102/// The epoch's P-COOL flag selects the flow serving a request. The
103/// TransactionDriver always exists, so it tracks epochs before the flag
104/// enables it. The QuorumDriver exists only when the node booted with
105/// P-COOL disabled and ran WAL recovery then; after a rollback a node
106/// booted under P-COOL must be restarted.
107pub struct TransactionOrchestrator<A: Clone> {
108    quorum_driver: Option<Arc<QuorumDriverHandler<A>>>,
109    transaction_driver: Arc<TransactionDriver<A>>,
110    validator_state: Arc<AuthorityState>,
111    /// Handle to the pending-tx-log cleanup loop; present only with the
112    /// quorum driver.
113    _local_executor_handle: Option<JoinHandle<()>>,
114    pending_tx_log: Arc<WritePathPendingTransactionLog>,
115    /// Digests currently being driven to finality by the TransactionDriver;
116    /// used to deduplicate concurrent submissions of the same transaction,
117    /// with a channel per digest through which the driving submission
118    /// publishes its outcome to concurrent duplicates. Kept in memory only:
119    /// the driver path is best-effort, so there is nothing to recover after
120    /// a restart. The QuorumDriver path tracks its submissions in
121    /// `pending_tx_log` instead.
122    in_flight_transactions: InFlightTransactions,
123    notifier: Arc<NotifyRead<TransactionDigest, QuorumDriverResult>>,
124    metrics: Arc<TransactionOrchestratorMetrics>,
125}
126
127impl TransactionOrchestrator<NetworkAuthorityClient> {
128    pub fn new_with_auth_aggregator(
129        validators: Arc<AuthorityAggregator<NetworkAuthorityClient>>,
130        validator_state: Arc<AuthorityState>,
131        reconfig_channel: Receiver<IotaSystemState>,
132        parent_path: &Path,
133        prometheus_registry: &Registry,
134        node_config: Option<&NodeConfig>,
135    ) -> Self {
136        let td_reconfig_observer = TdOnsiteReconfigObserver::new(
137            reconfig_channel.resubscribe(),
138            validator_state.get_object_cache_reader().clone(),
139            validator_state.clone_committee_store(),
140            validators.safe_client_metrics_base.clone(),
141        );
142
143        let qd_reconfig_observer = OnsiteReconfigObserver::new(
144            reconfig_channel.resubscribe(),
145            validator_state.get_object_cache_reader().clone(),
146            validator_state.clone_committee_store(),
147            validators.safe_client_metrics_base.clone(),
148            validators.metrics.deref().clone(),
149        );
150
151        TransactionOrchestrator::new(
152            validators,
153            validator_state,
154            parent_path,
155            prometheus_registry,
156            qd_reconfig_observer,
157            td_reconfig_observer,
158            node_config,
159        )
160    }
161}
162
163impl<A> TransactionOrchestrator<A>
164where
165    A: AuthorityAPI + Send + Sync + 'static + Clone,
166    OnsiteReconfigObserver: ReconfigObserver<A>,
167    TdOnsiteReconfigObserver: crate::transaction_driver::reconfig_observer::ReconfigObserver<A>,
168{
169    pub fn new(
170        validators: Arc<AuthorityAggregator<A>>,
171        validator_state: Arc<AuthorityState>,
172        parent_path: &Path,
173        prometheus_registry: &Registry,
174        reconfig_observer: OnsiteReconfigObserver,
175        td_reconfig_observer: TdOnsiteReconfigObserver,
176        node_config: Option<&NodeConfig>,
177    ) -> Self {
178        let epoch_store = validator_state.load_epoch_store_one_call_per_task();
179        let use_transaction_driver = epoch_store.protocol_config().enable_pcool_flow();
180
181        let notifier = Arc::new(NotifyRead::new());
182        let metrics = Arc::new(TransactionOrchestratorMetrics::new(prometheus_registry));
183        let pending_tx_log = Arc::new(WritePathPendingTransactionLog::new(
184            parent_path.join("fullnode_pending_transactions"),
185        ));
186
187        // Registered for both flows even when the quorum driver is not
188        // built, so metric presence does not depend on the boot mode.
189        let quorum_driver_metrics = Arc::new(QuorumDriverMetrics::new(prometheus_registry));
190        let transaction_driver_metrics =
191            Arc::new(TransactionDriverMetrics::new(prometheus_registry));
192        let client_metrics = Arc::new(ValidatorClientMetrics::new(prometheus_registry));
193
194        let (quorum_driver, _local_executor_handle) = if use_transaction_driver {
195            (None, None)
196        } else {
197            let quorum_driver = Arc::new(
198                QuorumDriverHandlerBuilder::new(validators.clone(), quorum_driver_metrics)
199                    .with_notifier(notifier.clone())
200                    .with_reconfig_observer(Arc::new(reconfig_observer))
201                    .start(),
202            );
203            // The cleanup loop must exist before WAL recovery runs, so a
204            // recovered transaction cannot complete before its receiver
205            // exists.
206            let effects_receiver = quorum_driver.subscribe_to_effects();
207            let pending_tx_log_clone = pending_tx_log.clone();
208            let local_executor_handle = spawn_monitored_task!(async move {
209                Self::loop_pending_transaction_log(effects_receiver, pending_tx_log_clone).await;
210            });
211            Self::schedule_txes_in_log(pending_tx_log.clone(), quorum_driver.clone());
212            (Some(quorum_driver), Some(local_executor_handle))
213        };
214
215        // `Weak` so detached driver tasks cannot pin the authority state.
216        let pcool_flow_enabled: Arc<dyn Fn() -> bool + Send + Sync> = {
217            let validator_state = Arc::downgrade(&validator_state);
218            Arc::new(move || {
219                validator_state.upgrade().is_some_and(|state| {
220                    state
221                        .load_epoch_store_one_call_per_task()
222                        .protocol_config()
223                        .enable_pcool_flow()
224                })
225            })
226        };
227        let transaction_driver = TransactionDriver::new(
228            validators,
229            Arc::new(td_reconfig_observer),
230            transaction_driver_metrics,
231            node_config.and_then(|config| config.validator_client_monitor_config.clone()),
232            client_metrics,
233            pcool_flow_enabled,
234        );
235
236        Self {
237            quorum_driver,
238            transaction_driver,
239            validator_state,
240            _local_executor_handle,
241            pending_tx_log,
242            in_flight_transactions: Default::default(),
243            notifier,
244            metrics,
245        }
246    }
247}
248
249impl<A> TransactionOrchestrator<A>
250where
251    A: AuthorityAPI + Send + Sync + 'static + Clone,
252{
253    /// Returns the flow selected by `epoch_store`'s P-COOL flag. Call with
254    /// the request's own epoch store snapshot so the flag and the submission
255    /// see the same epoch. Errors when the quorum driver is selected on a
256    /// node that booted under P-COOL: such a node never ran WAL recovery and
257    /// must be restarted.
258    fn select_driver(
259        &self,
260        epoch_store: &AuthorityPerEpochStore,
261    ) -> Result<Driver<A>, QuorumDriverError> {
262        if epoch_store.protocol_config().enable_pcool_flow() {
263            return Ok(Driver::Transaction(self.transaction_driver.clone()));
264        }
265        self.quorum_driver
266            .clone()
267            .map(Driver::Quorum)
268            .ok_or_else(|| {
269                error!(
270                    "This fullnode started while P-COOL was enabled and must be restarted to \
271                     serve the certificate-based flow"
272                );
273                QuorumDriverError::QuorumDriverInternal(IotaError::UnsupportedFeature {
274                    error: "this fullnode started while P-COOL was enabled and must be \
275                            restarted to serve the certificate-based flow"
276                        .to_string(),
277                })
278            })
279    }
280
281    #[instrument(name = "tx_orchestrator_execute_transaction_block", level = "trace", skip_all,
282        fields(
283        tx_digest = ?request.transaction.digest(),
284        tx_type = ?request_type,
285        ),
286        err)]
287    pub async fn execute_transaction_block(
288        &self,
289        request: ExecuteTransactionRequestV1,
290        request_type: ExecuteTransactionRequestType,
291        client_addr: Option<SocketAddr>,
292    ) -> Result<(ExecuteTransactionResponseV1, IsTransactionExecutedLocally), QuorumDriverError>
293    {
294        let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
295
296        let transaction = epoch_store
297            .verify_transaction(request.transaction.clone())
298            .map_err(QuorumDriverError::InvalidUserSignature)?;
299
300        // Captured before `request` moves so the skip-cert reconcile reads
301        // caller intent, not whatever the submitter happened to return — a
302        // Byzantine submitter could otherwise censor a field by returning
303        // `None`.
304        let include_events = request.include_events;
305        let include_input_objects = request.include_input_objects;
306        let include_output_objects = request.include_output_objects;
307
308        let tx_digest = *transaction.digest();
309
310        // A resubmission of an already-executed transaction is answered from
311        // the local cache instead of being driven through the validators
312        // again.
313        if let Some(response) = Self::build_response_from_local_effects(
314            &self.validator_state,
315            &tx_digest,
316            include_events,
317            include_input_objects,
318            include_output_objects,
319        )? {
320            self.metrics.early_cached_response.inc();
321            debug!(
322                ?tx_digest,
323                "Returning cached results for already-executed transaction"
324            );
325            return Ok((response, true));
326        }
327
328        // Reject malformed transactions before either driver inspects shared
329        // inputs or `MoveAuthenticator`. Runs after the cache lookup so that,
330        // as on the upstream flow, a resubmission of an executed transaction
331        // gets its cached results even if it no longer passes the current
332        // epoch's checks (e.g. its expiration epoch has passed).
333        transaction
334            .validity_check(&epoch_store.tx_validity_check_context())
335            .map_err(QuorumDriverError::InvalidTransaction)?;
336
337        let wait_for_local_execution = matches!(
338            request_type,
339            ExecuteTransactionRequestType::WaitForLocalExecution
340        );
341        let (mut response, seq) =
342            match (self.select_driver(&epoch_store)?, wait_for_local_execution) {
343                (Driver::Transaction(td), true) => {
344                    let in_flight_transactions = self.in_flight_transactions.clone();
345                    let validator_state = self.validator_state.clone();
346                    let metrics = self.metrics.clone();
347                    // Detached so a client disconnect (this future dropped) does
348                    // not cancel a submission that may already be in consensus;
349                    // the task drives the transaction to finality on its own.
350                    join_submission_task(spawn_monitored_task!(Self::submit_with_checkpoint_race(
351                        td,
352                        in_flight_transactions,
353                        validator_state,
354                        metrics,
355                        request,
356                        client_addr,
357                        tx_digest,
358                    )))
359                    .await?
360                }
361                (Driver::Transaction(td), false) => {
362                    let in_flight_transactions = self.in_flight_transactions.clone();
363                    let validator_state = self.validator_state.clone();
364                    // Detached for the same reason as above.
365                    let result = join_submission_task(spawn_monitored_task!(
366                        Self::submit_with_transaction_driver(
367                            td,
368                            in_flight_transactions,
369                            validator_state,
370                            request,
371                            client_addr,
372                            false,
373                        )
374                    ))
375                    .await?;
376                    (Some(result), None)
377                }
378                (Driver::Quorum(qd), _) => {
379                    let qd_resp = self
380                        .execute_transaction_impl(
381                            &qd,
382                            &epoch_store,
383                            request,
384                            transaction.clone(),
385                            client_addr,
386                        )
387                        .await?;
388                    (Some(quorum_driver_response_to_v1(qd_resp)), None)
389                }
390            };
391
392        // `needs_cache_rebuild` is derived from finality, not caller intent:
393        // the QD fallback path returns `Certified` and a duplicate
394        // submission inheriting the outcome of an in-flight certifying
395        // submission returns `QuorumExecuted` — neither needs a rebuild —
396        // even when the caller asked for `WaitForLocalExecution`, while only
397        // the TD skip-cert engine produces `UncertifiedSingleValidator`. The
398        // checkpoint sequence comes from `submit_with_checkpoint_race`, which
399        // relies on `executed_transactions_to_checkpoint` being written
400        // strictly after every tx's effects — so a `Some(seq)` here implies
401        // the cache has authoritative effects.
402        let needs_cache_rebuild = matches!(
403            response.as_ref().map(|r| &r.effects.finality_info),
404            None | Some(EffectsFinalityInfo::UncertifiedSingleValidator(_)),
405        );
406
407        let executed_locally = if !wait_for_local_execution {
408            false
409        } else if needs_cache_rebuild {
410            let Some(seq) = seq else {
411                // Timed out waiting for the tx to land in a local checkpoint.
412                // In this branch `response` is either `None` (recovery) or
413                // `UncertifiedSingleValidator` (TD skip-cert) — both must
414                // surface as `TimeoutBeforeFinality` rather than leaking
415                // uncorroborated single-validator effects to the client.
416                return Err(QuorumDriverError::TimeoutBeforeFinality);
417            };
418            match response.as_mut() {
419                Some(existing) => Self::reconcile_effects_from_cache(
420                    &self.validator_state,
421                    tx_digest,
422                    seq,
423                    include_events,
424                    include_input_objects,
425                    include_output_objects,
426                    existing,
427                    &self.metrics,
428                )?,
429                None => {
430                    response = Some(Self::build_response_from_cache(
431                        &self.validator_state,
432                        tx_digest,
433                        seq,
434                        include_events,
435                        include_input_objects,
436                        include_output_objects,
437                    )?);
438                }
439            }
440            true
441        } else {
442            // The response is already 2f+1 certified — from the QuorumDriver,
443            // or inherited by a duplicate submission from an in-flight
444            // certifying submission — so just confirm local execution
445            // finished.
446            let ok = Self::wait_for_finalized_tx_executed_locally_with_timeout(
447                &self.validator_state,
448                &transaction,
449                &self.metrics,
450            )
451            .await
452            .is_ok();
453            add_server_timing("local_execution");
454            ok
455        };
456
457        let response = response.expect("response must be populated before return");
458
459        // Safety guard: `UncertifiedSingleValidator` finality carries effects
460        // from the single submitting validator only — they MUST NOT reach the
461        // client without first being corroborated against the local cache. The
462        // reachable paths today all either upgrade finality via
463        // `reconcile_effects_from_cache` / `build_response_from_cache`, or
464        // branch to `TimeoutBeforeFinality`; this guard is the last-chance
465        // fallback for a future refactor that forgets to reconcile. Do not
466        // remove as dead code.
467        if matches!(
468            response.effects.finality_info,
469            EffectsFinalityInfo::UncertifiedSingleValidator(_)
470        ) {
471            debug_fatal!(
472                "Uncertified effects (UncertifiedSingleValidator) about to be returned \
473                 to the client for tx {:?}",
474                response.effects.effects.transaction_digest()
475            );
476            return Err(QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(
477                "internal error: transaction effects not finalized".to_string(),
478            )));
479        }
480
481        Ok((response, executed_locally))
482    }
483
484    /// Replace the response's effects, events, and input/output objects with
485    /// the authoritative copies derived from the local cache — the local
486    /// checkpoint executor has processed the tx, so the cache has the real
487    /// data and the TD-returned (single-validator) copies can be discarded.
488    ///
489    /// `tx_digest` must be the digest of the caller's original transaction,
490    /// not the digest carried in `response.effects.effects` — a byzantine
491    /// submitter could set the latter to an unrelated (already-executed) tx
492    /// so we'd read unrelated effects from the cache.
493    ///
494    /// The caller must have obtained `checkpoint_seq` from
495    /// `wait_for_checkpoint_inclusion` (not just `get_transaction_checkpoint`),
496    /// because that function guarantees both the effects write and the
497    /// checkpoint-mapping write have landed — it's the only way to avoid the
498    /// race between `notify_read_executed_effects_digests` (fires per-tx) and
499    /// `insert_finalized_transactions` (fires per-checkpoint, after the
500    /// `CheckpointExecutor` has awaited every tx in that checkpoint).
501    ///
502    /// Upgrades the finality info to `Checkpointed(epoch, checkpoint_seq)`. A
503    /// warning is logged if the TD-returned effects digest diverges from the
504    /// cache digest, or the submitter claimed events the cache doesn't have
505    /// (byzantine submitter or bug).
506    fn reconcile_effects_from_cache(
507        validator_state: &Arc<AuthorityState>,
508        tx_digest: TransactionDigest,
509        checkpoint_seq: CheckpointSequenceNumber,
510        include_events: bool,
511        include_input_objects: bool,
512        include_output_objects: bool,
513        response: &mut ExecuteTransactionResponseV1,
514        metrics: &TransactionOrchestratorMetrics,
515    ) -> Result<(), QuorumDriverError> {
516        let rebuilt = Self::build_response_from_cache(
517            validator_state,
518            tx_digest,
519            checkpoint_seq,
520            include_events,
521            include_input_objects,
522            include_output_objects,
523        )?;
524
525        let td_digest = response.effects.effects.digest();
526        let cache_digest = rebuilt.effects.effects.digest();
527        if td_digest != cache_digest {
528            warn!(
529                ?tx_digest,
530                ?td_digest,
531                ?cache_digest,
532                "reconcile_effects_from_cache: TransactionDriver and local cache disagree \
533                 on effects digest — replacing with cache (possible byzantine submitter)"
534            );
535        }
536        if include_events && response.events.is_some() && rebuilt.events.is_none() {
537            warn!(
538                ?tx_digest,
539                "reconcile_effects_from_cache: submitter claimed events but cache has \
540                 none — discarding (possible byzantine submitter)"
541            );
542            metrics.skip_effect_cert_events_cache_miss.inc();
543        }
544        *response = rebuilt;
545        Ok(())
546    }
547
548    /// Build a skip-effect-certification response entirely from the local
549    /// cache. The caller must have already obtained `checkpoint_seq` via
550    /// `wait_for_checkpoint_inclusion`, which is supposed to guarantee both
551    /// the effects write and the checkpoint-mapping write have landed. A
552    /// missing cache entry here would mean a transient races we observed in
553    /// practice; mapped to `TimeoutBeforeFinality` so the client retries
554    /// rather than seeing a misleading `QuorumDriverInternal`.
555    fn build_response_from_cache(
556        validator_state: &Arc<AuthorityState>,
557        tx_digest: TransactionDigest,
558        checkpoint_seq: CheckpointSequenceNumber,
559        include_events: bool,
560        include_input_objects: bool,
561        include_output_objects: bool,
562    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
563        let cached = read_cached_transaction_data(
564            validator_state,
565            &tx_digest,
566            include_events,
567            include_input_objects,
568            include_output_objects,
569        )
570        .map_err(|e| {
571            QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(format!(
572                "failed to read cached tx data for {tx_digest:?}: {e:?}"
573            )))
574        })?
575        .ok_or_else(|| {
576            // Checkpoint inclusion is supposed to guarantee the cache has
577            // effects, but we've seen transient misses; surface as a retriable
578            // timeout rather than an internal error.
579            warn!(
580                ?tx_digest,
581                "effects missing from cache after checkpoint inclusion — surfacing as \
582                 TimeoutBeforeFinality"
583            );
584            QuorumDriverError::TimeoutBeforeFinality
585        })?;
586        let iota_types::transaction_executor::CachedTransactionData {
587            effects,
588            events,
589            input_objects,
590            output_objects,
591        } = cached;
592
593        let epoch = effects.epoch();
594        Ok(ExecuteTransactionResponseV1 {
595            effects: FinalizedEffects {
596                effects,
597                finality_info: EffectsFinalityInfo::Checkpointed(epoch, checkpoint_seq),
598            },
599            events,
600            input_objects,
601            output_objects,
602            auxiliary_data: None,
603        })
604    }
605
606    /// Build a response from the local cache for a transaction that has
607    /// already been executed on this node. Returns `Ok(None)` when the
608    /// transaction has not been executed locally. Unlike
609    /// `build_response_from_cache`, no checkpoint sequence is required: local
610    /// effects only exist for finalized transactions, so the response is
611    /// tagged `QuorumExecuted`.
612    fn build_response_from_local_effects(
613        validator_state: &Arc<AuthorityState>,
614        tx_digest: &TransactionDigest,
615        include_events: bool,
616        include_input_objects: bool,
617        include_output_objects: bool,
618    ) -> Result<Option<ExecuteTransactionResponseV1>, QuorumDriverError> {
619        let Some(cached) = read_cached_transaction_data(
620            validator_state,
621            tx_digest,
622            include_events,
623            include_input_objects,
624            include_output_objects,
625        )
626        .map_err(QuorumDriverError::QuorumDriverInternal)?
627        else {
628            return Ok(None);
629        };
630        let iota_types::transaction_executor::CachedTransactionData {
631            effects,
632            events,
633            input_objects,
634            output_objects,
635        } = cached;
636
637        let epoch = effects.epoch();
638        Ok(Some(ExecuteTransactionResponseV1 {
639            effects: FinalizedEffects {
640                effects,
641                finality_info: EffectsFinalityInfo::QuorumExecuted(epoch),
642            },
643            events,
644            input_objects,
645            output_objects,
646            auxiliary_data: None,
647        }))
648    }
649
650    // Utilize the handle_certificate_v1 validator api to request input/output
651    // objects
652    #[instrument(name = "tx_orchestrator_execute_transaction_v1", level = "trace", skip_all,
653        fields(tx_digest = ?request.transaction.digest()))]
654    pub async fn execute_transaction_v1(
655        &self,
656        request: ExecuteTransactionRequestV1,
657        skip_certification: bool,
658        client_addr: Option<SocketAddr>,
659    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
660        let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
661
662        let transaction = epoch_store
663            .verify_transaction(request.transaction.clone())
664            .map_err(QuorumDriverError::InvalidUserSignature)?;
665        let tx_digest = *transaction.digest();
666
667        // A resubmission of an already-executed transaction is answered from
668        // the local cache instead of being driven through the validators
669        // again.
670        if let Some(response) = Self::build_response_from_local_effects(
671            &self.validator_state,
672            &tx_digest,
673            request.include_events,
674            request.include_input_objects,
675            request.include_output_objects,
676        )? {
677            self.metrics.early_cached_response.inc();
678            debug!(
679                ?tx_digest,
680                "Returning cached results for already-executed transaction"
681            );
682            return Ok(response);
683        }
684
685        // Reject malformed transactions before either driver inspects shared
686        // inputs or `MoveAuthenticator`. Runs after the cache lookup so that,
687        // as on the upstream flow, a resubmission of an executed transaction
688        // gets its cached results even if it no longer passes the current
689        // epoch's checks (e.g. its expiration epoch has passed).
690        transaction
691            .validity_check(&epoch_store.tx_validity_check_context())
692            .map_err(QuorumDriverError::InvalidTransaction)?;
693
694        match self.select_driver(&epoch_store)? {
695            Driver::Transaction(td) => {
696                let in_flight_transactions = self.in_flight_transactions.clone();
697                let validator_state = self.validator_state.clone();
698                // v1 does not do an internal wait; callers (e.g. the gRPC
699                // execution service) are responsible for their own
700                // `wait_for_checkpoint_inclusion` when they need it, and will
701                // reconcile the response from the cache there.
702                //
703                // Detached so a client disconnect does not cancel a submission
704                // that may already be in consensus.
705                join_submission_task(spawn_monitored_task!(Self::submit_with_transaction_driver(
706                    td,
707                    in_flight_transactions,
708                    validator_state,
709                    request,
710                    client_addr,
711                    skip_certification,
712                )))
713                .await
714            }
715            Driver::Quorum(qd) => {
716                let qd_resp = self
717                    .execute_transaction_impl(&qd, &epoch_store, request, transaction, client_addr)
718                    .await?;
719                Ok(quorum_driver_response_to_v1(qd_resp))
720            }
721        }
722    }
723
724    /// Submit on the skip-effect-certification path while concurrently
725    /// waiting for local checkpoint inclusion. The race is asymmetric:
726    ///
727    /// - If the **checkpoint** future resolves first (slow driver, e.g. stuck
728    ///   corroborating a Byzantine validator's rejection), the driver future is
729    ///   dropped and the caller rebuilds the response from the local cache.
730    /// - If the **driver** returns first, its result is taken and the
731    ///   checkpoint future is awaited to completion (up to the shared
732    ///   `WAIT_FOR_FINALITY_TIMEOUT`) before returning, so the caller has a
733    ///   checkpoint sequence to reconcile against.
734    ///
735    /// Returns `(response, seq)` where `response` is `Some` when the driver
736    /// returned a result (which may carry `UncertifiedSingleValidator`
737    /// finality requiring rebuild) and `seq` is the checkpoint sequence if
738    /// either future yielded it.
739    ///
740    /// Run inside a detached task so a client disconnect cannot cancel the
741    /// race before the checkpoint-sequence bookkeeping completes.
742    #[instrument(name = "tx_orchestrator_submit_with_checkpoint_race", level = "trace", skip_all,
743        fields(tx_digest = ?tx_digest))]
744    async fn submit_with_checkpoint_race(
745        td: Arc<TransactionDriver<A>>,
746        in_flight_transactions: InFlightTransactions,
747        validator_state: Arc<AuthorityState>,
748        metrics: Arc<TransactionOrchestratorMetrics>,
749        request: ExecuteTransactionRequestV1,
750        client_addr: Option<SocketAddr>,
751        tx_digest: TransactionDigest,
752    ) -> Result<
753        (
754            Option<ExecuteTransactionResponseV1>,
755            Option<CheckpointSequenceNumber>,
756        ),
757        QuorumDriverError,
758    > {
759        let digests = [tx_digest];
760        let checkpoint_inclusion =
761            validator_state.wait_for_checkpoint_inclusion(&digests, WAIT_FOR_FINALITY_TIMEOUT);
762        tokio::pin!(checkpoint_inclusion);
763        let driver = Self::submit_with_transaction_driver(
764            td,
765            in_flight_transactions,
766            validator_state.clone(),
767            request,
768            client_addr,
769            true,
770        );
771
772        let seq_for_tx = |inclusion_map: BTreeMap<_, (CheckpointSequenceNumber, _)>| {
773            inclusion_map.get(&tx_digest).map(|&(seq, _)| seq)
774        };
775
776        let result = tokio::select! {
777            biased;
778            // `SubmittedButFetchFailed` is retriable (`ErrorCategory::Unavailable`)
779            // so the driver's outer loop reissues submission internally and
780            // only returns here as `Ok`, `TimeoutWithLastRetriableError`, or
781            // a non-retriable error like `RejectedByValidators`.
782            driver_result = driver => {
783                let response = Some(driver_result?);
784                let seq = (&mut checkpoint_inclusion).await.ok().and_then(seq_for_tx);
785                (response, seq)
786            }
787            checkpoint_result = &mut checkpoint_inclusion => {
788                metrics.skip_effect_cert_checkpoint_overrode_driver.inc();
789                // Dropping the cancelled driver closes the in-flight outcome
790                // channel; duplicate submissions fall back to waiting for
791                // checkpoint inclusion, which this race winning guarantees
792                // resolves immediately.
793                let seq = checkpoint_result.ok().and_then(seq_for_tx);
794                (None, seq)
795            }
796        };
797        add_server_timing("local_execution");
798        Ok(result)
799    }
800
801    /// Submit a transaction via the TransactionDriver (P-COOL flow).
802    ///
803    /// With `skip_certification = true` the driver may return
804    /// `UncertifiedSingleValidator` effects without a 2f+1 broadcast. The
805    /// caller (gRPC `execute_transactions` or `execute_transaction_block`)
806    /// is then responsible for `wait_for_checkpoint_inclusion` and the
807    /// cache-rebuild gate that replaces those single-validator effects with
808    /// authoritative data — uncertified data must never reach the client.
809    /// See `corroborate_single_validator_error` for the per-submission
810    /// fetch-failure recovery flow inside the driver.
811    ///
812    /// Run inside a detached task so a client disconnect cannot cancel a
813    /// `drive_transaction` call that may already be in consensus.
814    #[instrument(name = "tx_orchestrator_submit_with_td", level = "trace", skip_all,
815        fields(tx_digest = ?request.transaction.digest()))]
816    async fn submit_with_transaction_driver(
817        td: Arc<TransactionDriver<A>>,
818        in_flight_transactions: InFlightTransactions,
819        validator_state: Arc<AuthorityState>,
820        request: ExecuteTransactionRequestV1,
821        client_addr: Option<SocketAddr>,
822        skip_certification: bool,
823    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
824        let tx_digest = *request.transaction.digest();
825
826        // Deduplicate concurrent submissions of the same digest: only the
827        // first caller drives the committee-wide submission and publishes its
828        // outcome; the rest await that outcome. The guard removes the digest
829        // from the in-flight map on every exit path (success, error, timeout,
830        // or cancellation) when it is dropped.
831        let guard = match TransactionSubmissionGuard::acquire(in_flight_transactions, tx_digest) {
832            TransactionSubmission::Driving(guard) => guard,
833            TransactionSubmission::AlreadyInFlight(receiver) => {
834                debug!(
835                    ?tx_digest,
836                    "transaction already in flight; awaiting its outcome instead of driving a \
837                     duplicate submission"
838                );
839                return Self::await_in_flight_transaction(
840                    receiver,
841                    &td,
842                    &validator_state,
843                    tx_digest,
844                    &request,
845                    client_addr,
846                    skip_certification,
847                )
848                .await;
849            }
850        };
851
852        // This call runs inside a task detached from the caller, so the
853        // outcome is logged here rather than left to the caller — a
854        // disconnected client's continuation never runs and would
855        // otherwise never observe it.
856        let td_response = match td
857            .drive_transaction(
858                Some(request.transaction.clone()),
859                SubmitTransactionOptions {
860                    forwarded_client_addr: client_addr,
861                    ..Default::default()
862                },
863                Some(WAIT_FOR_FINALITY_TIMEOUT),
864                skip_certification,
865            )
866            .await
867        {
868            Ok(response) => response,
869            Err(e) => {
870                warn!(?tx_digest, "TransactionDriver submission failed: {e}");
871                let error = map_td_error_to_qd(e);
872                guard.publish(Err(error.clone()));
873                return Err(error);
874            }
875        };
876
877        debug!(?tx_digest, "TransactionDriver submission succeeded");
878
879        let td_response = Arc::new(td_response);
880        guard.publish(Ok(td_response.clone()));
881        // Dropping the guard closes the channel, releasing its copy of the
882        // response unless a duplicate submission still holds a receiver — in
883        // the common no-duplicate case the response is then moved into the
884        // reply instead of cloned.
885        drop(guard);
886        let td_response = Arc::try_unwrap(td_response).unwrap_or_else(|shared| (*shared).clone());
887
888        Ok(Self::response_from_driver_response(td_response, &request))
889    }
890
891    /// Build a caller-specific response from a driver response, honoring the
892    /// caller's include flags.
893    fn response_from_driver_response(
894        td_response: QuorumTransactionResponse,
895        request: &ExecuteTransactionRequestV1,
896    ) -> ExecuteTransactionResponseV1 {
897        let QuorumTransactionResponse {
898            effects,
899            events,
900            input_objects,
901            output_objects,
902            auxiliary_data,
903        } = td_response;
904        ExecuteTransactionResponseV1 {
905            effects: convert_td_to_qd_effects(effects),
906            events: request.include_events.then_some(events).flatten(),
907            input_objects: request
908                .include_input_objects
909                .then_some(input_objects)
910                .flatten(),
911            output_objects: request
912                .include_output_objects
913                .then_some(output_objects)
914                .flatten(),
915            auxiliary_data: request
916                .include_auxiliary_data
917                .then_some(auxiliary_data)
918                .flatten(),
919        }
920    }
921
922    /// Await the outcome of an already in-flight submission of `tx_digest`
923    /// instead of starting a second committee-wide submission for the same
924    /// transaction. Resolves to that submission's outcome — running the
925    /// effects-certification step first if the outcome does not satisfy this
926    /// caller — falls back to waiting for checkpoint inclusion if the
927    /// driving submission went away without publishing one (checkpoint-race
928    /// cancellation, panic, or shutdown), and returns
929    /// `TimeoutBeforeFinality` if nothing is published within
930    /// `WAIT_FOR_FINALITY_TIMEOUT` or the follow-up effects certification
931    /// does not complete within another `WAIT_FOR_FINALITY_TIMEOUT`.
932    async fn await_in_flight_transaction(
933        mut receiver: watch::Receiver<Option<InFlightSubmissionResult>>,
934        td: &Arc<TransactionDriver<A>>,
935        validator_state: &Arc<AuthorityState>,
936        tx_digest: TransactionDigest,
937        request: &ExecuteTransactionRequestV1,
938        client_addr: Option<SocketAddr>,
939        skip_certification: bool,
940    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
941        // The `Ref` returned by `wait_for` is a read guard and must not be
942        // held across an await, so the outcome is cloned out before
943        // branching. `wait_for` only returns a value matching its predicate,
944        // so `Some` is guaranteed on success; a closed channel yields `None`.
945        let published = tokio::time::timeout(
946            WAIT_FOR_FINALITY_TIMEOUT,
947            receiver.wait_for(|outcome| outcome.is_some()),
948        )
949        .await
950        .map_err(|_elapsed| QuorumDriverError::TimeoutBeforeFinality)?
951        .ok()
952        .and_then(|outcome_ref| outcome_ref.clone());
953
954        let Some(outcome) = published else {
955            // Channel closed without an outcome: the driving submission went
956            // away without publishing — routinely because its checkpoint
957            // race observed the transaction in a local checkpoint and
958            // cancelled it, exceptionally on panic or shutdown. Checkpoint
959            // inclusion is the remaining signal of the outcome.
960            return Self::response_from_checkpoint_inclusion(validator_state, tx_digest, request)
961                .await;
962        };
963        let td_response = outcome?;
964
965        let uncertified = matches!(
966            td_response.effects.finality_info,
967            TdEffectsFinalityInfo::UncertifiedSingleValidator(_)
968        );
969        if uncertified && !skip_certification {
970            // The in-flight submission already drove the transaction into
971            // consensus; only the 2f+1 effects certification is missing for
972            // this caller. Certify the effects directly instead of starting
973            // a second committee-wide submission or waiting for a checkpoint
974            // inclusion the caller never asked for. `certify_transaction` is
975            // internally bounded only by committee size times its per-request
976            // timeout, so cap it to the same client-facing budget the driving
977            // submission gets for its whole `drive_transaction` call.
978            let certified = tokio::time::timeout(
979                WAIT_FOR_FINALITY_TIMEOUT,
980                td.certify_transaction(
981                    tx_digest,
982                    SubmitTransactionOptions {
983                        forwarded_client_addr: client_addr,
984                        ..Default::default()
985                    },
986                ),
987            )
988            .await
989            .map_err(|_elapsed| QuorumDriverError::TimeoutBeforeFinality)?
990            .map_err(map_td_error_to_qd)?;
991            return Ok(Self::response_from_driver_response(certified, request));
992        }
993
994        Ok(Self::response_from_driver_response(
995            (*td_response).clone(),
996            request,
997        ))
998    }
999
1000    /// Wait for `tx_digest` to reach a local checkpoint and build the
1001    /// response from the authoritative cache. The fallback outcome signal
1002    /// for a duplicate whose driving submission went away without
1003    /// publishing; the result carries `Checkpointed` finality, so it
1004    /// satisfies every caller. Times out with `TimeoutBeforeFinality` if
1005    /// the transaction does not get checkpointed in time.
1006    async fn response_from_checkpoint_inclusion(
1007        validator_state: &Arc<AuthorityState>,
1008        tx_digest: TransactionDigest,
1009        request: &ExecuteTransactionRequestV1,
1010    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
1011        let digests = [tx_digest];
1012        // The caller has typically already waited a full timeout on the
1013        // outcome channel, but this wait is still required: it is what
1014        // yields the checkpoint sequence and guarantees the checkpoint
1015        // mapping write has landed (see `reconcile_effects_from_cache`).
1016        // When the transaction is already checkpointed — the routine reason
1017        // the fallback fires — it resolves immediately; only after a
1018        // driving-task death does it actually wait, as the last remaining
1019        // signal of the outcome.
1020        let seq = validator_state
1021            .wait_for_checkpoint_inclusion(&digests, WAIT_FOR_FINALITY_TIMEOUT)
1022            .await
1023            .ok()
1024            .and_then(|inclusion| inclusion.get(&tx_digest).map(|&(seq, _)| seq))
1025            .ok_or(QuorumDriverError::TimeoutBeforeFinality)?;
1026        Self::build_response_from_cache(
1027            validator_state,
1028            tx_digest,
1029            seq,
1030            request.include_events,
1031            request.include_input_objects,
1032            request.include_output_objects,
1033        )
1034    }
1035
1036    /// Submit a transaction via the QuorumDriver. `transaction` must be the
1037    /// signature-verified form of `request.transaction`, and the caller must
1038    /// have run `validity_check` on it beforehand.
1039    #[instrument(level = "trace", skip_all, fields(tx_digest = ?request.transaction.digest()))]
1040    async fn execute_transaction_impl(
1041        &self,
1042        quorum_driver: &Arc<QuorumDriverHandler<A>>,
1043        epoch_store: &Arc<AuthorityPerEpochStore>,
1044        request: ExecuteTransactionRequestV1,
1045        transaction: VerifiedTransaction,
1046        client_addr: Option<SocketAddr>,
1047    ) -> Result<QuorumDriverResponse, QuorumDriverError> {
1048        let (_in_flight_metrics_guards, good_response_metrics) = self.update_metrics(&transaction);
1049        let tx_digest = *transaction.digest();
1050        debug!(?tx_digest, "TO Received transaction execution request.");
1051
1052        let (_e2e_latency_timer, _txn_finality_timer) = if transaction.contains_shared_object() {
1053            (
1054                self.metrics.request_latency_shared_obj.start_timer(),
1055                self.metrics
1056                    .wait_for_finality_latency_shared_obj
1057                    .start_timer(),
1058            )
1059        } else {
1060            (
1061                self.metrics.request_latency_single_writer.start_timer(),
1062                self.metrics
1063                    .wait_for_finality_latency_single_writer
1064                    .start_timer(),
1065            )
1066        };
1067
1068        // TODO: refactor all the gauge and timer metrics with `monitored_scope`
1069        let wait_for_finality_gauge = self.metrics.wait_for_finality_in_flight.clone();
1070        wait_for_finality_gauge.inc();
1071        let _wait_for_finality_gauge = scopeguard::guard(wait_for_finality_gauge, |in_flight| {
1072            in_flight.dec();
1073        });
1074
1075        let ticket = self
1076            .submit(
1077                quorum_driver,
1078                epoch_store.clone(),
1079                transaction.clone(),
1080                request,
1081                client_addr,
1082            )
1083            .await
1084            .map_err(|e| {
1085                warn!(?tx_digest, "QuorumDriverInternalError: {e:?}");
1086                QuorumDriverError::QuorumDriverInternal(e)
1087            })?;
1088
1089        let Ok(result) = timeout(WAIT_FOR_FINALITY_TIMEOUT, ticket).await else {
1090            debug!(?tx_digest, "Timeout waiting for transaction finality.");
1091            self.metrics.wait_for_finality_timeout.inc();
1092            return Err(QuorumDriverError::TimeoutBeforeFinality);
1093        };
1094        add_server_timing("wait_for_finality");
1095
1096        drop(_txn_finality_timer);
1097        drop(_wait_for_finality_gauge);
1098        self.metrics.wait_for_finality_finished.inc();
1099
1100        match result {
1101            Err(err) => {
1102                warn!(?tx_digest, "QuorumDriverInternalError: {err:?}");
1103                Err(QuorumDriverError::QuorumDriverInternal(err))
1104            }
1105            Ok(Err(err)) => Err(err),
1106            Ok(Ok(response)) => {
1107                good_response_metrics.inc();
1108                Ok(response)
1109            }
1110        }
1111    }
1112
1113    /// Submits the transaction to Quorum Driver for execution.
1114    /// Returns an awaitable Future.
1115    #[instrument(name = "tx_orchestrator_submit", level = "trace", skip_all)]
1116    async fn submit(
1117        &self,
1118        quorum_driver: &Arc<QuorumDriverHandler<A>>,
1119        epoch_store: Arc<AuthorityPerEpochStore>,
1120        transaction: VerifiedTransaction,
1121        request: ExecuteTransactionRequestV1,
1122        client_addr: Option<SocketAddr>,
1123    ) -> IotaResult<impl Future<Output = IotaResult<QuorumDriverResult>> + '_> {
1124        let tx_digest = *transaction.digest();
1125        let ticket = self.notifier.register_one(&tx_digest);
1126        // TODO(william) need to also write client adr to pending tx log below
1127        // so that we can re-execute with this client addr if we restart
1128        if self
1129            .pending_tx_log
1130            .write_pending_transaction_maybe(&transaction)
1131            .await?
1132        {
1133            debug!(?tx_digest, "no pending request in flight, submitting.");
1134            quorum_driver
1135                .submit_transaction_no_ticket(request.clone(), client_addr)
1136                .await?;
1137        }
1138        // It's possible that the transaction effects is already stored in DB at this
1139        // point. So we also subscribe to that. If we hear from `effects_await`
1140        // first, it means the ticket misses the previous notification, and we
1141        // want to ask quorum driver to form a certificate for us again, to
1142        // serve this request.
1143        let cache_reader = self.validator_state.get_transaction_cache_reader().clone();
1144        let qd = quorum_driver.clone();
1145        Ok(async move {
1146            let digests = [tx_digest];
1147            let effects_await =
1148                epoch_store.within_alive_epoch(cache_reader.try_notify_read_executed_effects(
1149                    "TransactionOrchestrator::notify_read_submit_with_qd",
1150                    &digests,
1151                ));
1152            // let-and-return necessary to satisfy borrow checker.
1153            let res = match select(ticket, effects_await.boxed()).await {
1154                Either::Left((quorum_driver_response, _)) => Ok(quorum_driver_response),
1155                Either::Right((_, unfinished_quorum_driver_task)) => {
1156                    debug!(
1157                        ?tx_digest,
1158                        "Effects are available in DB, use quorum driver to get a certificate"
1159                    );
1160                    qd.submit_transaction_no_ticket(request, client_addr)
1161                        .await?;
1162                    Ok(unfinished_quorum_driver_task.await)
1163                }
1164            };
1165            res
1166        })
1167    }
1168
1169    #[instrument(
1170        name = "tx_orchestrator_wait_for_finalized_tx_executed_locally_with_timeout",
1171        level = "debug",
1172        skip_all,
1173        fields(tx_digest = ?transaction.digest()),
1174        err
1175    )]
1176    async fn wait_for_finalized_tx_executed_locally_with_timeout(
1177        validator_state: &Arc<AuthorityState>,
1178        transaction: &VerifiedTransaction,
1179        metrics: &TransactionOrchestratorMetrics,
1180    ) -> IotaResult {
1181        let tx_digest = *transaction.digest();
1182        metrics.local_execution_in_flight.inc();
1183        let _metrics_guard =
1184            scopeguard::guard(metrics.local_execution_in_flight.clone(), |in_flight| {
1185                in_flight.dec();
1186            });
1187
1188        let _guard = if transaction.contains_shared_object() {
1189            metrics.local_execution_latency_shared_obj.start_timer()
1190        } else {
1191            metrics.local_execution_latency_single_writer.start_timer()
1192        };
1193        debug!(
1194            ?tx_digest,
1195            "Waiting for finalized tx to be executed locally."
1196        );
1197        match timeout(
1198            LOCAL_EXECUTION_TIMEOUT,
1199            validator_state
1200                .get_transaction_cache_reader()
1201                .try_notify_read_executed_effects_digests(
1202                    "TransactionOrchestrator::notify_read_wait_for_local_execution",
1203                    &[tx_digest],
1204                ),
1205        )
1206        .instrument(trace_span!("local_execution"))
1207        .await
1208        {
1209            Err(_elapsed) => {
1210                debug!(
1211                    ?tx_digest,
1212                    "Waiting for finalized tx to be executed locally timed out within {:?}.",
1213                    LOCAL_EXECUTION_TIMEOUT
1214                );
1215                metrics.local_execution_timeout.inc();
1216                Err(IotaError::Timeout)
1217            }
1218            Ok(Err(err)) => {
1219                debug!(
1220                    ?tx_digest,
1221                    "Waiting for finalized tx to be executed locally failed with error: {:?}", err
1222                );
1223                metrics.local_execution_failure.inc();
1224                Err(IotaError::TransactionOrchestratorLocalExecution {
1225                    error: err.to_string(),
1226                })
1227            }
1228            Ok(Ok(_)) => {
1229                metrics.local_execution_success.inc();
1230                Ok(())
1231            }
1232        }
1233    }
1234
1235    // TODO: Potentially cleanup this function and pending transaction log.
1236    async fn loop_pending_transaction_log(
1237        mut effects_receiver: Receiver<QuorumDriverEffectsQueueResult>,
1238        pending_transaction_log: Arc<WritePathPendingTransactionLog>,
1239    ) {
1240        loop {
1241            match effects_receiver.recv().await {
1242                Ok(Ok((transaction, ..))) => {
1243                    let tx_digest = transaction.digest();
1244                    if let Err(err) = pending_transaction_log.finish_transaction(tx_digest) {
1245                        error!(
1246                            ?tx_digest,
1247                            "Failed to finish transaction in pending transaction log: {err}"
1248                        );
1249                    }
1250                }
1251                Ok(Err((tx_digest, _err))) => {
1252                    if let Err(err) = pending_transaction_log.finish_transaction(&tx_digest) {
1253                        error!(
1254                            ?tx_digest,
1255                            "Failed to finish transaction in pending transaction log: {err}"
1256                        );
1257                    }
1258                }
1259                Err(RecvError::Closed) => {
1260                    error!("Sender of effects subscriber queue has been dropped!");
1261                    return;
1262                }
1263                Err(RecvError::Lagged(skipped_count)) => {
1264                    warn!("Skipped {skipped_count} transasctions in effects subscriber queue.");
1265                }
1266            }
1267        }
1268    }
1269
1270    /// Returns the quorum driver, or `None` when the node booted under
1271    /// P-COOL. Test-only: submissions must go through the per-request
1272    /// driver selection.
1273    #[cfg(any(test, feature = "test-utils"))]
1274    pub fn quorum_driver(&self) -> Option<&Arc<QuorumDriverHandler<A>>> {
1275        self.quorum_driver.as_ref()
1276    }
1277
1278    /// Owned variant of [`Self::quorum_driver`].
1279    #[cfg(any(test, feature = "test-utils"))]
1280    pub fn clone_quorum_driver(&self) -> Option<Arc<QuorumDriverHandler<A>>> {
1281        self.quorum_driver.clone()
1282    }
1283
1284    /// Returns the transaction driver's aggregator; it always exists and its
1285    /// reconfig observer keeps it current.
1286    pub fn clone_authority_aggregator(&self) -> Arc<AuthorityAggregator<A>> {
1287        self.transaction_driver.authority_aggregator().load_full()
1288    }
1289
1290    /// Returns an effects receiver only while the quorum driver is the
1291    /// currently selected flow and this node can serve it; the P-COOL flow
1292    /// has no effects broadcast.
1293    pub fn subscribe_to_effects_queue(&self) -> Option<Receiver<QuorumDriverEffectsQueueResult>> {
1294        let epoch_store = self.validator_state.load_epoch_store_one_call_per_task();
1295        if epoch_store.protocol_config().enable_pcool_flow() {
1296            return None;
1297        }
1298        self.quorum_driver
1299            .as_ref()
1300            .map(|quorum_driver| quorum_driver.subscribe_to_effects())
1301    }
1302
1303    /// Runs driver selection for `epoch_store` and reports its outcome.
1304    #[cfg(any(test, feature = "test-utils"))]
1305    pub fn select_driver_for_testing(
1306        &self,
1307        epoch_store: &AuthorityPerEpochStore,
1308    ) -> Result<(), QuorumDriverError> {
1309        self.select_driver(epoch_store).map(|_| ())
1310    }
1311
1312    fn update_metrics(
1313        &'_ self,
1314        transaction: &VerifiedTransaction,
1315    ) -> (impl Drop, &'_ GenericCounter<AtomicU64>) {
1316        let (in_flight, good_response) = if transaction.contains_shared_object() {
1317            self.metrics.total_req_received_shared_object.inc();
1318            (
1319                self.metrics.req_in_flight_shared_object.clone(),
1320                &self.metrics.good_response_shared_object,
1321            )
1322        } else {
1323            self.metrics.total_req_received_single_writer.inc();
1324            (
1325                self.metrics.req_in_flight_single_writer.clone(),
1326                &self.metrics.good_response_single_writer,
1327            )
1328        };
1329        in_flight.inc();
1330        (
1331            scopeguard::guard(in_flight, |in_flight| {
1332                in_flight.dec();
1333            }),
1334            good_response,
1335        )
1336    }
1337
1338    fn schedule_txes_in_log(
1339        pending_tx_log: Arc<WritePathPendingTransactionLog>,
1340        quorum_driver: Arc<QuorumDriverHandler<A>>,
1341    ) {
1342        if std::env::var("SKIP_LOADING_FROM_PENDING_TX_LOG").is_ok() {
1343            info!("Skipping loading pending transactions from pending_tx_log.");
1344            return;
1345        }
1346        spawn_logged_monitored_task!(async move {
1347            let pending_txes = pending_tx_log
1348                .load_all_pending_transactions()
1349                .expect("failed to load all pending transactions");
1350            info!(
1351                "Recovering {} pending transactions from pending_tx_log.",
1352                pending_txes.len()
1353            );
1354            for (i, tx) in pending_txes.into_iter().enumerate() {
1355                // TODO: ideally pending_tx_log would not contain VerifiedTransaction, but that
1356                // requires a migration.
1357                let tx = tx.into_inner();
1358                let tx_digest = *tx.digest();
1359                // It's not impossible we fail to enqueue a task but that's not the end of
1360                // world. TODO(william) correctly extract client_addr from logs
1361                if let Err(err) = quorum_driver
1362                    .submit_transaction_no_ticket(
1363                        ExecuteTransactionRequestV1 {
1364                            transaction: tx,
1365                            include_events: true,
1366                            include_input_objects: false,
1367                            include_output_objects: false,
1368                            include_auxiliary_data: false,
1369                        },
1370                        None,
1371                    )
1372                    .await
1373                {
1374                    warn!(
1375                        ?tx_digest,
1376                        "Failed to enqueue transaction from pending_tx_log, err: {err:?}"
1377                    );
1378                } else {
1379                    debug!(?tx_digest, "Enqueued transaction from pending_tx_log");
1380                    if (i + 1) % 1000 == 0 {
1381                        info!("Enqueued {} transactions from pending_tx_log.", i + 1);
1382                    }
1383                }
1384            }
1385            // Transactions will be cleaned up in
1386            // loop_execute_finalized_tx_locally() after they
1387            // produce effects.
1388        });
1389    }
1390
1391    pub fn load_all_pending_transactions(&self) -> IotaResult<Vec<VerifiedTransaction>> {
1392        self.pending_tx_log.load_all_pending_transactions()
1393    }
1394
1395    /// Reports whether a driver submission of `tx_digest` is in flight, and
1396    /// if so how many duplicate submissions are awaiting its outcome.
1397    #[cfg(any(test, feature = "test-utils"))]
1398    pub fn in_flight_duplicates_for_testing(&self, tx_digest: &TransactionDigest) -> Option<usize> {
1399        self.in_flight_transactions
1400            .lock()
1401            .get(tx_digest)
1402            .map(|sender| sender.receiver_count())
1403    }
1404}
1405
1406/// Convert a `QuorumDriverResponse` (contains
1407/// `VerifiedCertifiedTransactionEffects`) to the V1 response format that uses
1408/// `FinalizedEffects`.
1409fn quorum_driver_response_to_v1(response: QuorumDriverResponse) -> ExecuteTransactionResponseV1 {
1410    let QuorumDriverResponse {
1411        effects_cert,
1412        events,
1413        input_objects,
1414        output_objects,
1415        auxiliary_data,
1416    } = response;
1417    ExecuteTransactionResponseV1 {
1418        effects: FinalizedEffects::new_from_effects_cert(effects_cert.into()),
1419        events,
1420        input_objects,
1421        output_objects,
1422        auxiliary_data,
1423    }
1424}
1425
1426/// Convert a `transaction_driver_types::FinalizedEffects` into a
1427/// `quorum_driver_types::FinalizedEffects`.
1428fn convert_td_to_qd_effects(td: TdFinalizedEffects) -> FinalizedEffects {
1429    let finality_info = match td.finality_info {
1430        TdEffectsFinalityInfo::Certified(sig) => EffectsFinalityInfo::Certified(sig),
1431        TdEffectsFinalityInfo::Checkpointed(epoch, seq) => {
1432            EffectsFinalityInfo::Checkpointed(epoch, seq)
1433        }
1434        TdEffectsFinalityInfo::QuorumExecuted(epoch) => EffectsFinalityInfo::QuorumExecuted(epoch),
1435        TdEffectsFinalityInfo::UncertifiedSingleValidator(epoch) => {
1436            EffectsFinalityInfo::UncertifiedSingleValidator(epoch)
1437        }
1438    };
1439    FinalizedEffects {
1440        effects: td.effects,
1441        finality_info,
1442    }
1443}
1444
1445/// Map a `TransactionDriverError` to a `QuorumDriverError` for client
1446/// reporting. The variant choice signals retriability: clients retry on
1447/// `QuorumDriverInternal`, `FailedWithTransientErrorAfterMaximumAttempts`,
1448/// and `TimeoutBeforeFinality`, but treat `InvalidTransaction` /
1449/// `InvalidUserSignature` as terminal. Submission-time rejections that
1450/// cannot succeed on resubmission must therefore not be reported as
1451/// internal.
1452fn map_td_error_to_qd(e: TransactionDriverError) -> QuorumDriverError {
1453    use TransactionDriverError::*;
1454    match e {
1455        ValidationFailed { error } => {
1456            QuorumDriverError::InvalidUserSignature(IotaError::InvalidSignature { error })
1457        }
1458        TimeoutWithLastRetriableError { .. } => QuorumDriverError::TimeoutBeforeFinality,
1459        RejectedByValidators {
1460            submission_non_retriable_errors,
1461            ..
1462        } => {
1463            // f+1 stake of validators returned non-retriable errors during
1464            // submission (bad signature, malformed tx, lock conflict, ...).
1465            // f+1 means at least one honest validator considered this tx
1466            // invalid, so resubmitting the same bytes cannot succeed.
1467            let representative = submission_non_retriable_errors
1468                .errors
1469                .into_iter()
1470                .next()
1471                .map(|(msg, _, _, _)| msg)
1472                .unwrap_or_else(|| "transaction rejected as invalid during submission".to_string());
1473            QuorumDriverError::InvalidTransaction(IotaError::Unknown(format!(
1474                "Transaction was rejected as invalid by more than 1/3 of validator stake \
1475                 during submission (non-retriable): {representative}"
1476            )))
1477        }
1478        Aborted {
1479            submission_retriable_errors,
1480            submission_non_retriable_errors,
1481            ..
1482        } => {
1483            // Driver exhausted the validator list without reaching the f+1
1484            // non-retriable threshold — most failures were transient
1485            // (validator down, network, overload). Surface as retriable so
1486            // the client can resubmit.
1487            let attempts = count_validator_attempts(&submission_retriable_errors)
1488                + count_validator_attempts(&submission_non_retriable_errors);
1489            QuorumDriverError::FailedWithTransientErrorAfterMaximumAttempts {
1490                total_attempts: attempts,
1491            }
1492        }
1493        other @ ForkedExecution { .. } => {
1494            // Validators disagree on effects digests — a protocol-level
1495            // invariant violation, never a client retry case. Log loud so
1496            // on-call sees it; surface as internal.
1497            let msg = other.to_string();
1498            error!("TransactionDriver observed forked execution: {msg}");
1499            QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1500        }
1501        other @ ClientInternal { .. } => {
1502            let msg = other.to_string();
1503            warn!("TransactionDriver client-internal error: {msg}");
1504            QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1505        }
1506        other @ SubmittedButFetchFailed { .. } => {
1507            let msg = other.to_string();
1508            warn!("TransactionDriver submitted transaction but failed to fetch effects: {msg}");
1509            QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(msg))
1510        }
1511    }
1512}
1513
1514fn count_validator_attempts(errors: &AggregatedRequestErrors) -> u32 {
1515    errors
1516        .errors
1517        .iter()
1518        .map(|(_, authorities, _, _)| authorities.len() as u32)
1519        .sum()
1520}
1521
1522/// Await a detached submission task, surfacing a task panic as an internal
1523/// error.
1524async fn join_submission_task<T>(
1525    handle: tokio::task::JoinHandle<Result<T, QuorumDriverError>>,
1526) -> Result<T, QuorumDriverError> {
1527    handle.await.unwrap_or_else(|e| {
1528        Err(QuorumDriverError::QuorumDriverInternal(IotaError::Unknown(
1529            format!("transaction submission task panicked: {e}"),
1530        )))
1531    })
1532}
1533
1534/// Prometheus metrics which can be displayed in Grafana, queried and alerted on
1535#[derive(Clone)]
1536pub struct TransactionOrchestratorMetrics {
1537    total_req_received_single_writer: GenericCounter<AtomicU64>,
1538    total_req_received_shared_object: GenericCounter<AtomicU64>,
1539
1540    good_response_single_writer: GenericCounter<AtomicU64>,
1541    good_response_shared_object: GenericCounter<AtomicU64>,
1542
1543    req_in_flight_single_writer: GenericGauge<AtomicI64>,
1544    req_in_flight_shared_object: GenericGauge<AtomicI64>,
1545
1546    wait_for_finality_in_flight: GenericGauge<AtomicI64>,
1547    wait_for_finality_finished: GenericCounter<AtomicU64>,
1548    wait_for_finality_timeout: GenericCounter<AtomicU64>,
1549
1550    local_execution_in_flight: GenericGauge<AtomicI64>,
1551    local_execution_success: GenericCounter<AtomicU64>,
1552    local_execution_timeout: GenericCounter<AtomicU64>,
1553    local_execution_failure: GenericCounter<AtomicU64>,
1554
1555    early_cached_response: GenericCounter<AtomicU64>,
1556
1557    // Bumped when the skip-effect-certification path reconciles against the
1558    // local cache but the cache has no events for a tx the single submitter
1559    // claimed had events. Uncertified events are rejected and the request
1560    // fails via the safety guard.
1561    skip_effect_cert_events_cache_miss: GenericCounter<AtomicU64>,
1562
1563    // Bumped when local checkpoint inclusion completes before the TD
1564    // skip-effect-certification call returns. Indicates the driver was slow
1565    // (e.g., corroborating a single-validator rejection) and the checkpoint
1566    // race cancelled the in-flight driver work in favor of rebuilding from
1567    // the local cache.
1568    skip_effect_cert_checkpoint_overrode_driver: GenericCounter<AtomicU64>,
1569
1570    request_latency_single_writer: Histogram,
1571    request_latency_shared_obj: Histogram,
1572    wait_for_finality_latency_single_writer: Histogram,
1573    wait_for_finality_latency_shared_obj: Histogram,
1574    local_execution_latency_single_writer: Histogram,
1575    local_execution_latency_shared_obj: Histogram,
1576}
1577
1578// Note that labeled-metrics are stored upfront individually
1579// to mitigate the perf hit by MetricsVec.
1580// See https://github.com/tikv/rust-prometheus/tree/master/static-metric
1581impl TransactionOrchestratorMetrics {
1582    pub fn new(registry: &Registry) -> Self {
1583        let total_req_received = register_int_counter_vec_with_registry!(
1584            "tx_orchestrator_total_req_received",
1585            "Total number of executions request Transaction Orchestrator receives, group by tx type",
1586            &["tx_type"],
1587            registry;
1588            MetricLevel::Warn,
1589        )
1590            .unwrap();
1591
1592        let total_req_received_single_writer =
1593            total_req_received.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1594        let total_req_received_shared_object =
1595            total_req_received.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1596
1597        let good_response = register_int_counter_vec_with_registry!(
1598            "tx_orchestrator_good_response",
1599            "Total number of good responses Transaction Orchestrator generates, group by tx type",
1600            &["tx_type"],
1601            registry;
1602            MetricLevel::Warn,
1603        )
1604        .unwrap();
1605
1606        let good_response_single_writer =
1607            good_response.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1608        let good_response_shared_object = good_response.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1609
1610        let req_in_flight = register_int_gauge_vec_with_registry!(
1611            "tx_orchestrator_req_in_flight",
1612            "Number of requests in flights Transaction Orchestrator processes, group by tx type",
1613            &["tx_type"],
1614            registry;
1615            MetricLevel::Warn,
1616        )
1617        .unwrap();
1618
1619        let req_in_flight_single_writer =
1620            req_in_flight.with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]);
1621        let req_in_flight_shared_object = req_in_flight.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]);
1622
1623        let request_latency = register_histogram_vec_with_registry!(
1624            "tx_orchestrator_request_latency",
1625            "Time spent in processing one Transaction Orchestrator request",
1626            &["tx_type"],
1627            iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1628            registry;
1629            MetricLevel::Warn,
1630        )
1631        .unwrap();
1632        let wait_for_finality_latency = register_histogram_vec_with_registry!(
1633            "tx_orchestrator_wait_for_finality_latency",
1634            "Time spent in waiting for one Transaction Orchestrator request gets finalized",
1635            &["tx_type"],
1636            iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1637            registry;
1638            MetricLevel::Warn,
1639        )
1640        .unwrap();
1641        let local_execution_latency = register_histogram_vec_with_registry!(
1642            "tx_orchestrator_local_execution_latency",
1643            "Time spent in waiting for one Transaction Orchestrator gets locally executed",
1644            &["tx_type"],
1645            iota_metrics::COARSE_LATENCY_SEC_BUCKETS.to_vec(),
1646            registry;
1647            MetricLevel::Warn,
1648        )
1649        .unwrap();
1650
1651        Self {
1652            total_req_received_single_writer,
1653            total_req_received_shared_object,
1654            good_response_single_writer,
1655            good_response_shared_object,
1656            req_in_flight_single_writer,
1657            req_in_flight_shared_object,
1658            wait_for_finality_in_flight: register_int_gauge_with_registry!(
1659                "tx_orchestrator_wait_for_finality_in_flight",
1660                "Number of in flight txns Transaction Orchestrator are waiting for finality for",
1661                registry;
1662                MetricLevel::Warn,
1663            )
1664                .unwrap(),
1665            wait_for_finality_finished: register_int_counter_with_registry!(
1666                "tx_orchestrator_wait_for_finality_finished",
1667                "Total number of txns Transaction Orchestrator gets responses from Quorum Driver before timeout, either success or failure",
1668                registry;
1669                MetricLevel::Warn,
1670            )
1671                .unwrap(),
1672            wait_for_finality_timeout: register_int_counter_with_registry!(
1673                "tx_orchestrator_wait_for_finality_timeout",
1674                "Total number of txns timing out in waiting for finality Transaction Orchestrator handles",
1675                registry;
1676                MetricLevel::Warn,
1677            )
1678                .unwrap(),
1679            local_execution_in_flight: register_int_gauge_with_registry!(
1680                "tx_orchestrator_local_execution_in_flight",
1681                "Number of local execution txns in flights Transaction Orchestrator handles",
1682                registry;
1683                MetricLevel::Warn,
1684            )
1685                .unwrap(),
1686            local_execution_success: register_int_counter_with_registry!(
1687                "tx_orchestrator_local_execution_success",
1688                "Total number of successful local execution txns Transaction Orchestrator handles",
1689                registry;
1690                MetricLevel::Warn,
1691            )
1692                .unwrap(),
1693            local_execution_timeout: register_int_counter_with_registry!(
1694                "tx_orchestrator_local_execution_timeout",
1695                "Total number of timed-out local execution txns Transaction Orchestrator handles",
1696                registry;
1697                MetricLevel::Warn,
1698            )
1699                .unwrap(),
1700            local_execution_failure: register_int_counter_with_registry!(
1701                "tx_orchestrator_local_execution_failure",
1702                "Total number of failed local execution txns Transaction Orchestrator handles",
1703                registry;
1704                MetricLevel::Warn,
1705            )
1706                .unwrap(),
1707            early_cached_response: register_int_counter_with_registry!(
1708                "tx_orchestrator_early_cached_response",
1709                "Total number of requests returning cached results for already-executed transactions",
1710                registry,
1711            )
1712                .unwrap(),
1713            skip_effect_cert_events_cache_miss: register_int_counter_with_registry!(
1714                "tx_orchestrator_skip_effect_cert_events_cache_miss",
1715                "Number of skip-effect-certification responses rejected because the \
1716                 single submitter claimed to have events but the local cache did not \
1717                 corroborate them",
1718                registry,
1719            )
1720                .unwrap(),
1721            skip_effect_cert_checkpoint_overrode_driver: register_int_counter_with_registry!(
1722                "tx_orchestrator_skip_effect_cert_checkpoint_overrode_driver",
1723                "Number of skip-effect-certification requests where local checkpoint \
1724                 inclusion completed before the TransactionDriver call returned; the \
1725                 driver future was cancelled and the response was rebuilt from cache",
1726                registry,
1727            )
1728                .unwrap(),
1729            request_latency_single_writer: request_latency
1730                .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1731            request_latency_shared_obj: request_latency.with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1732            wait_for_finality_latency_single_writer: wait_for_finality_latency
1733                .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1734            wait_for_finality_latency_shared_obj: wait_for_finality_latency
1735                .with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1736            local_execution_latency_single_writer: local_execution_latency
1737                .with_label_values(&[TX_TYPE_SINGLE_WRITER_TX]),
1738            local_execution_latency_shared_obj: local_execution_latency
1739                .with_label_values(&[TX_TYPE_SHARED_OBJ_TX]),
1740        }
1741    }
1742
1743    pub fn new_for_tests() -> Self {
1744        let registry = Registry::new();
1745        Self::new(&registry)
1746    }
1747}
1748
1749#[async_trait::async_trait]
1750impl<A> iota_types::transaction_executor::TransactionExecutor for TransactionOrchestrator<A>
1751where
1752    A: AuthorityAPI + Send + Sync + 'static + Clone,
1753{
1754    async fn execute_transaction(
1755        &self,
1756        request: ExecuteTransactionRequestV1,
1757        skip_certification: bool,
1758        client_addr: Option<std::net::SocketAddr>,
1759    ) -> Result<ExecuteTransactionResponseV1, QuorumDriverError> {
1760        self.execute_transaction_v1(request, skip_certification, client_addr)
1761            .await
1762    }
1763
1764    fn simulate_transaction(
1765        &self,
1766        transaction: Transaction,
1767        checks: VmChecks,
1768    ) -> Result<SimulateTransactionResult, IotaError> {
1769        self.validator_state
1770            .simulate_transaction(transaction, checks)
1771    }
1772
1773    /// Wait for the given transactions to be included in a checkpoint.
1774    ///
1775    /// Returns a mapping from transaction digest to
1776    /// `(checkpoint_sequence_number, checkpoint_timestamp_ms)`.
1777    /// On timeout, returns partial results for any transactions that were
1778    /// already checkpointed.
1779    async fn wait_for_checkpoint_inclusion(
1780        &self,
1781        digests: &[TransactionDigest],
1782        timeout: Duration,
1783    ) -> Result<BTreeMap<TransactionDigest, (CheckpointSequenceNumber, u64)>, IotaError> {
1784        self.validator_state
1785            .wait_for_checkpoint_inclusion(digests, timeout)
1786            .await
1787    }
1788
1789    fn read_transaction_from_cache(
1790        &self,
1791        digest: &TransactionDigest,
1792        include_events: bool,
1793        include_input_objects: bool,
1794        include_output_objects: bool,
1795    ) -> Result<Option<iota_types::transaction_executor::CachedTransactionData>, IotaError> {
1796        read_cached_transaction_data(
1797            &self.validator_state,
1798            digest,
1799            include_events,
1800            include_input_objects,
1801            include_output_objects,
1802        )
1803    }
1804}
1805
1806/// Read a transaction's authoritative data from the local cache. Returns
1807/// `Ok(None)` if the tx hasn't been executed locally yet. Shared by the
1808/// orchestrator's skip-cert response builder and the `TransactionExecutor`
1809/// trait method consumed by the gRPC handler.
1810fn read_cached_transaction_data(
1811    validator_state: &Arc<AuthorityState>,
1812    digest: &TransactionDigest,
1813    include_events: bool,
1814    include_input_objects: bool,
1815    include_output_objects: bool,
1816) -> Result<Option<iota_types::transaction_executor::CachedTransactionData>, IotaError> {
1817    let cache = validator_state.get_transaction_cache_reader();
1818    let Some(effects) = cache.try_get_executed_effects(digest)? else {
1819        return Ok(None);
1820    };
1821
1822    let events = if include_events && effects.events_digest().is_some() {
1823        Some(validator_state.get_transaction_events(digest)?)
1824    } else {
1825        None
1826    };
1827
1828    let input_objects = if include_input_objects {
1829        Some(
1830            validator_state
1831                .get_transaction_input_objects(&effects)
1832                .map_err(|e| IotaError::Unknown(format!("input objects: {e:?}")))?,
1833        )
1834    } else {
1835        None
1836    };
1837    let output_objects = if include_output_objects {
1838        Some(
1839            validator_state
1840                .get_transaction_output_objects(&effects)
1841                .map_err(|e| IotaError::Unknown(format!("output objects: {e:?}")))?,
1842        )
1843    } else {
1844        None
1845    };
1846
1847    Ok(Some(
1848        iota_types::transaction_executor::CachedTransactionData {
1849            effects,
1850            events,
1851            input_objects,
1852            output_objects,
1853        },
1854    ))
1855}
1856
1857/// Successful outcome of an in-flight driver submission: the unfiltered
1858/// driver response, or the local checkpoint the transaction was observed in
1859/// when the checkpoint race cancelled the driver (the cache is then the
1860/// authoritative source of the effects).
1861/// Outcome of an in-flight driver submission, shared with concurrent
1862/// submissions of the same digest.
1863type InFlightSubmissionResult = Result<Arc<QuorumTransactionResponse>, QuorumDriverError>;
1864
1865/// Digests currently being driven to finality by the TransactionDriver,
1866/// each with a channel through which the driving submission publishes its
1867/// outcome to concurrent duplicates.
1868type InFlightTransactions =
1869    Arc<Mutex<HashMap<TransactionDigest, watch::Sender<Option<InFlightSubmissionResult>>>>>;
1870
1871/// Result of trying to register a submission of a digest in the in-flight
1872/// map: either this caller drives the committee-wide submission, or another
1873/// submission of the same digest is already in flight and this caller should
1874/// await its published outcome instead.
1875enum TransactionSubmission {
1876    Driving(TransactionSubmissionGuard),
1877    AlreadyInFlight(watch::Receiver<Option<InFlightSubmissionResult>>),
1878}
1879
1880/// Tracks a transaction that is being submitted to finality so that
1881/// concurrent submissions of the same digest deduplicate.
1882///
1883/// Held only by the driving submission, which must `publish` its outcome so
1884/// concurrent duplicates can return it. Dropping the guard removes the
1885/// digest from the in-flight map on every exit path (success, error,
1886/// timeout, and cancellation); receivers subscribed before removal still
1887/// observe a published outcome, and if the entry is removed without any
1888/// outcome (checkpoint-race cancellation, panic, or shutdown) the closed
1889/// channel tells duplicates to fall back to checkpoint inclusion.
1890struct TransactionSubmissionGuard {
1891    in_flight_transactions: InFlightTransactions,
1892    tx_digest: TransactionDigest,
1893}
1894
1895impl TransactionSubmissionGuard {
1896    fn acquire(
1897        in_flight_transactions: InFlightTransactions,
1898        tx_digest: TransactionDigest,
1899    ) -> TransactionSubmission {
1900        {
1901            let mut in_flight = in_flight_transactions.lock();
1902            match in_flight.entry(tx_digest) {
1903                Entry::Occupied(entry) => {
1904                    return TransactionSubmission::AlreadyInFlight(entry.get().subscribe());
1905                }
1906                Entry::Vacant(entry) => {
1907                    let (sender, _initial_receiver) = watch::channel(None);
1908                    entry.insert(sender);
1909                    debug!(?tx_digest, "added transaction to in-flight map");
1910                }
1911            }
1912        }
1913        TransactionSubmission::Driving(Self {
1914            in_flight_transactions,
1915            tx_digest,
1916        })
1917    }
1918
1919    /// Publish the submission outcome to concurrent duplicate submissions.
1920    /// The outcome is stored in the channel even when nobody is subscribed
1921    /// yet, so a duplicate that subscribes after this call but before the
1922    /// entry is removed still reads it instead of a closed channel.
1923    fn publish(&self, result: InFlightSubmissionResult) {
1924        if let Some(sender) = self.in_flight_transactions.lock().get(&self.tx_digest) {
1925            sender.send_replace(Some(result));
1926        }
1927    }
1928}
1929
1930impl Drop for TransactionSubmissionGuard {
1931    fn drop(&mut self) {
1932        self.in_flight_transactions.lock().remove(&self.tx_digest);
1933    }
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938    use super::*;
1939
1940    fn acquire_driving(
1941        in_flight: &InFlightTransactions,
1942        tx_digest: TransactionDigest,
1943    ) -> TransactionSubmissionGuard {
1944        match TransactionSubmissionGuard::acquire(in_flight.clone(), tx_digest) {
1945            TransactionSubmission::Driving(guard) => guard,
1946            TransactionSubmission::AlreadyInFlight(_) => {
1947                panic!("expected to acquire the driving submission")
1948            }
1949        }
1950    }
1951
1952    fn acquire_duplicate(
1953        in_flight: &InFlightTransactions,
1954        tx_digest: TransactionDigest,
1955    ) -> watch::Receiver<Option<InFlightSubmissionResult>> {
1956        match TransactionSubmissionGuard::acquire(in_flight.clone(), tx_digest) {
1957            TransactionSubmission::Driving(_) => {
1958                panic!("expected the digest to already be in flight")
1959            }
1960            TransactionSubmission::AlreadyInFlight(receiver) => receiver,
1961        }
1962    }
1963
1964    #[tokio::test]
1965    async fn duplicate_submission_receives_published_outcome() {
1966        let in_flight = InFlightTransactions::default();
1967        let tx_digest = TransactionDigest::random();
1968
1969        let guard = acquire_driving(&in_flight, tx_digest);
1970        let mut receiver = acquire_duplicate(&in_flight, tx_digest);
1971
1972        guard.publish(Err(QuorumDriverError::TimeoutBeforeFinality));
1973        drop(guard);
1974
1975        // The published outcome must survive the guard drop for receivers
1976        // subscribed before the entry was removed.
1977        let outcome = receiver
1978            .wait_for(|outcome| outcome.is_some())
1979            .await
1980            .expect("outcome was published before the sender dropped")
1981            .clone()
1982            .expect("wait_for only returns once the outcome is Some");
1983        assert!(matches!(
1984            outcome,
1985            Err(QuorumDriverError::TimeoutBeforeFinality)
1986        ));
1987        assert!(
1988            in_flight.lock().is_empty(),
1989            "guard drop must remove the in-flight entry"
1990        );
1991    }
1992
1993    #[tokio::test]
1994    async fn duplicate_subscribing_after_publish_receives_outcome() {
1995        let in_flight = InFlightTransactions::default();
1996        let tx_digest = TransactionDigest::random();
1997
1998        let guard = acquire_driving(&in_flight, tx_digest);
1999        guard.publish(Err(QuorumDriverError::TimeoutBeforeFinality));
2000
2001        // Subscribing between the publish and the entry removal must still
2002        // resolve to the outcome; falling back to checkpoint inclusion here
2003        // would cost the duplicate a full finality timeout.
2004        let mut receiver = acquire_duplicate(&in_flight, tx_digest);
2005        drop(guard);
2006
2007        let outcome = receiver
2008            .wait_for(|outcome| outcome.is_some())
2009            .await
2010            .expect("the outcome is stored in the channel regardless of subscribers")
2011            .clone()
2012            .expect("wait_for only returns once the outcome is Some");
2013        assert!(matches!(
2014            outcome,
2015            Err(QuorumDriverError::TimeoutBeforeFinality)
2016        ));
2017    }
2018
2019    #[tokio::test]
2020    async fn dropped_guard_without_outcome_closes_channel() {
2021        let in_flight = InFlightTransactions::default();
2022        let tx_digest = TransactionDigest::random();
2023
2024        let guard = acquire_driving(&in_flight, tx_digest);
2025        let mut receiver = acquire_duplicate(&in_flight, tx_digest);
2026        drop(guard);
2027
2028        receiver
2029            .wait_for(|outcome| outcome.is_some())
2030            .await
2031            .expect_err("dropping the guard without publishing must close the channel");
2032        assert!(in_flight.lock().is_empty());
2033
2034        // The digest can be driven again once the entry is gone.
2035        let _guard = acquire_driving(&in_flight, tx_digest);
2036    }
2037
2038    async fn build_orchestrator_with_pcool(
2039        enable_pcool: bool,
2040    ) -> (
2041        Arc<AuthorityState>,
2042        TransactionOrchestrator<NetworkAuthorityClient>,
2043        tempfile::TempDir,
2044        tokio::sync::broadcast::Sender<IotaSystemState>,
2045    ) {
2046        use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
2047
2048        use crate::{
2049            authority::test_authority_builder::TestAuthorityBuilder,
2050            authority_aggregator::AuthorityAggregatorBuilder,
2051        };
2052
2053        telemetry_subscribers::init_for_testing();
2054        let network_config =
2055            iota_swarm_config::network_config_builder::ConfigBuilder::new_with_temp_dir().build();
2056
2057        let mut protocol_config =
2058            ProtocolConfig::get_for_version(ProtocolVersion::MAX, Chain::Unknown);
2059        protocol_config.set_enable_pcool_flow_for_testing(enable_pcool);
2060        let state = TestAuthorityBuilder::new()
2061            .with_network_config(&network_config, 0)
2062            .with_protocol_config(protocol_config)
2063            .build()
2064            .await;
2065
2066        let (aggregator, _clients) =
2067            AuthorityAggregatorBuilder::from_genesis(&network_config.genesis)
2068                .build_network_clients();
2069        let (reconfig_tx, reconfig_rx) = tokio::sync::broadcast::channel(16);
2070        let tempdir = tempfile::tempdir().unwrap();
2071        let orchestrator = TransactionOrchestrator::new_with_auth_aggregator(
2072            Arc::new(aggregator),
2073            state.clone(),
2074            reconfig_rx,
2075            tempdir.path(),
2076            &Registry::new(),
2077            None,
2078        );
2079        (state, orchestrator, tempdir, reconfig_tx)
2080    }
2081
2082    /// A flag-off boot builds the quorum driver and runs WAL recovery at
2083    /// construction.
2084    #[tokio::test(flavor = "multi_thread")]
2085    async fn qd_recovery_eager_on_flag_off_boot() {
2086        let (state, orchestrator, _tempdir, _reconfig_tx) =
2087            build_orchestrator_with_pcool(false).await;
2088        assert!(orchestrator.quorum_driver().is_some());
2089        assert!(
2090            orchestrator
2091                .select_driver_for_testing(&state.epoch_store_for_testing())
2092                .is_ok()
2093        );
2094    }
2095
2096    /// A node booted under P-COOL has no quorum driver. After a rollback it
2097    /// must reject quorum-driver selection until restarted.
2098    #[tokio::test(flavor = "multi_thread")]
2099    async fn flag_on_boot_rejects_selection_after_rollback() {
2100        use iota_protocol_config::{Chain, ProtocolConfig, ProtocolVersion};
2101
2102        let (state, orchestrator, _tempdir, _reconfig_tx) =
2103            build_orchestrator_with_pcool(true).await;
2104        assert!(orchestrator.quorum_driver().is_none());
2105
2106        // Selection under the flag serves the P-COOL flow.
2107        assert!(
2108            orchestrator
2109                .select_driver_for_testing(&state.epoch_store_for_testing())
2110                .is_ok()
2111        );
2112
2113        // Epoch 1 with P-COOL off: selection and the effects queue must both
2114        // report the missing quorum driver.
2115        let mut protocol_config =
2116            ProtocolConfig::get_for_version(ProtocolVersion::MAX, Chain::Unknown);
2117        protocol_config.set_enable_pcool_flow_for_testing(false);
2118        state
2119            .reconfigure_for_testing_with_protocol_config(protocol_config)
2120            .await;
2121        let epoch_store = state.epoch_store_for_testing();
2122        assert_eq!(epoch_store.epoch(), 1);
2123
2124        assert!(matches!(
2125            orchestrator.select_driver_for_testing(&epoch_store),
2126            Err(QuorumDriverError::QuorumDriverInternal(_))
2127        ));
2128        assert!(orchestrator.subscribe_to_effects_queue().is_none());
2129    }
2130}