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