Skip to main content

sui_core/transaction_driver/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod effects_certifier;
5mod error;
6mod metrics;
7mod reconfig_observer;
8mod request_retrier;
9mod transaction_submitter;
10
11/// Exports
12pub use error::TransactionDriverError;
13pub use metrics::*;
14pub use reconfig_observer::{OnsiteReconfigObserver, ReconfigObserver};
15
16use std::{
17    net::SocketAddr,
18    sync::Arc,
19    time::{Duration, Instant},
20};
21
22use arc_swap::ArcSwap;
23use effects_certifier::*;
24use mysten_common::backoff::ExponentialBackoff;
25use mysten_metrics::{monitored_future, spawn_logged_monitored_task};
26use nonempty::NonEmpty;
27use parking_lot::Mutex;
28use rand::Rng;
29use request_retrier::SELECT_LATENCY_DELTA;
30use sui_config::NodeConfig;
31use sui_types::{
32    base_types::AuthorityName,
33    committee::EpochId,
34    error::{ErrorCategory, UserInputError},
35    messages_grpc::{SubmitTxRequest, SubmitTxResult, TxType},
36    transaction::{AllowedProposers, TransactionDataAPI as _},
37    transaction_executor::ProposerSelector,
38};
39use tokio::{
40    task::JoinSet,
41    time::{interval, sleep},
42};
43use tracing::instrument;
44use transaction_submitter::*;
45
46use crate::{
47    authority_aggregator::AuthorityAggregator,
48    authority_client::AuthorityAPI,
49    validator_client_monitor::{
50        OperationFeedback, OperationType, TransactionClass, ValidatorClientMetrics,
51        ValidatorClientMonitor,
52    },
53};
54
55#[cfg(test)]
56#[path = "unit_tests/proposer_selector_tests.rs"]
57mod proposer_selector_tests;
58
59/// Trait for components that can update their AuthorityAggregator during reconfiguration.
60/// Used by ReconfigObserver to notify components of epoch changes.
61pub trait AuthorityAggregatorUpdatable<A: Clone>: Send + Sync + 'static {
62    fn epoch(&self) -> EpochId;
63    fn authority_aggregator(&self) -> Arc<AuthorityAggregator<A>>;
64    fn update_authority_aggregator(&self, new_authorities: Arc<AuthorityAggregator<A>>);
65}
66
67/// Options for submitting a transaction.
68#[derive(Clone, Default, Debug)]
69pub struct SubmitTransactionOptions {
70    /// When forwarding transactions on behalf of a client, this is the client's address
71    /// specified for ddos protection.
72    pub forwarded_client_addr: Option<SocketAddr>,
73
74    /// When submitting a transaction, only the validators in the allowed validator list can be used to submit the transaction to.
75    /// When the allowed validator list is empty, any validator can be used.
76    pub allowed_validators: Vec<String>,
77
78    /// When submitting a transaction, the validators in the blocked validator list cannot be used to submit the transaction to.
79    /// When the blocked validator list is empty, no restrictions are applied.
80    pub blocked_validators: Vec<String>,
81}
82
83#[derive(Clone, Debug)]
84pub struct QuorumTransactionResponse {
85    pub effects: sui_types::transaction_driver_types::FinalizedEffects,
86
87    pub events: Option<sui_types::effects::TransactionEvents>,
88    // Input objects will only be populated in the happy path
89    pub input_objects: Option<Vec<sui_types::object::Object>>,
90    // Output objects will only be populated in the happy path
91    pub output_objects: Option<Vec<sui_types::object::Object>>,
92    pub auxiliary_data: Option<Vec<u8>>,
93}
94
95pub struct TransactionDriver<A: Clone> {
96    authority_aggregator: Arc<ArcSwap<AuthorityAggregator<A>>>,
97    state: Mutex<State>,
98    metrics: Arc<TransactionDriverMetrics>,
99    submitter: TransactionSubmitter,
100    certifier: EffectsCertifier,
101    client_monitor: Arc<ValidatorClientMonitor<A>>,
102}
103
104impl<A> TransactionDriver<A>
105where
106    A: AuthorityAPI + Send + Sync + 'static + Clone,
107{
108    // TODO: accept a TransactionDriverConfig to set default allowed & blocked validators.
109    pub fn new(
110        authority_aggregator: Arc<AuthorityAggregator<A>>,
111        reconfig_observer: Arc<dyn ReconfigObserver<A> + Sync + Send>,
112        metrics: Arc<TransactionDriverMetrics>,
113        node_config: Option<&NodeConfig>,
114        client_metrics: Arc<ValidatorClientMetrics>,
115    ) -> Arc<Self> {
116        if std::env::var("TRANSACTION_DRIVER").is_ok() {
117            tracing::warn!(
118                "Transaction Driver is the only supported driver for transaction submission. Setting TRANSACTION_DRIVER is a no-op."
119            );
120        }
121
122        let shared_swap = Arc::new(ArcSwap::new(authority_aggregator));
123
124        // Extract validator client monitor config from NodeConfig or use default
125        let monitor_config = node_config
126            .and_then(|nc| nc.validator_client_monitor_config.clone())
127            .unwrap_or_default();
128        let client_monitor =
129            ValidatorClientMonitor::new(monitor_config, client_metrics, shared_swap.clone());
130
131        let driver = Arc::new(Self {
132            authority_aggregator: shared_swap,
133            state: Mutex::new(State::new()),
134            metrics: metrics.clone(),
135            submitter: TransactionSubmitter::new(metrics.clone()),
136            certifier: EffectsCertifier::new(metrics),
137            client_monitor,
138        });
139
140        let driver_clone = driver.clone();
141
142        spawn_logged_monitored_task!(Self::run_latency_checks(driver_clone));
143
144        driver.enable_reconfig(reconfig_observer);
145        driver
146    }
147
148    /// Returns the authority aggregator wrapper which upgrades on epoch changes.
149    pub fn authority_aggregator(&self) -> &Arc<ArcSwap<AuthorityAggregator<A>>> {
150        &self.authority_aggregator
151    }
152
153    pub fn select_preferred_validators(
154        &self,
155        delta: f64,
156        tx_class: TransactionClass,
157    ) -> Vec<AuthorityName> {
158        let authority_aggregator = self.authority_aggregator.load();
159        self.client_monitor.select_shuffled_preferred_validators(
160            &authority_aggregator.committee,
161            delta,
162            tx_class,
163        )
164    }
165
166    /// The validators this node would prefer to submit to, as committee indices.
167    ///
168    /// These are the same targets `RequestRetrier` would pick, so a transaction restricted to them
169    /// names the validators it was going to be sent to anyway.
170    fn preferred_proposers_impl(&self, max: usize) -> Option<AllowedProposers> {
171        // Before any latency has been observed the ranking is an arbitrary shuffle, so pinning to
172        // it would be worse than leaving the transaction unrestricted.
173        if !self.client_monitor.has_observed_latencies() {
174            return None;
175        }
176
177        let authority_aggregator = self.authority_aggregator.load();
178        let committee = &authority_aggregator.committee;
179        // The set restricts the transaction, so it is chosen from restricted-class
180        // stats — the view staggering holds cannot pollute.
181        let mut proposers: Vec<u32> = self
182            .client_monitor
183            .select_shuffled_preferred_validators(
184                committee,
185                SELECT_LATENCY_DELTA,
186                TransactionClass::Restricted,
187            )
188            .into_iter()
189            .filter_map(|name| committee.authority_index(&name))
190            .take(max)
191            .collect();
192        // The set is unordered preference; `Validity` requires it strictly increasing.
193        proposers.sort_unstable();
194        proposers.dedup();
195
196        Some(AllowedProposers {
197            epoch: committee.epoch(),
198            proposers: NonEmpty::from_vec(proposers)?,
199        })
200    }
201
202    /// Drives transaction to finalization.
203    ///
204    /// Internally, retries the attempt to finalize a transaction until:
205    /// - The transaction is finalized.
206    /// - The transaction observes a non-retriable error.
207    /// - Timeout is reached.
208    #[instrument(level = "error", skip_all, fields(tx_digest = ?request.transaction.as_ref().map(|t| t.digest()), ping = %request.ping_type.is_some()))]
209    pub async fn drive_transaction(
210        &self,
211        request: SubmitTxRequest,
212        options: SubmitTransactionOptions,
213        timeout_duration: Option<Duration>,
214    ) -> Result<QuorumTransactionResponse, TransactionDriverError> {
215        const MAX_DRIVE_TRANSACTION_RETRY_DELAY: Duration = Duration::from_secs(10);
216
217        let tx_data = request.transaction.as_ref().map(|t| t.transaction_data());
218        // gas_price=0 for gasless; use 1 for baseline (RGP-equivalent) priority
219        let amplification_factor =
220            if request.ping_type.is_some() || tx_data.is_some_and(|d| d.is_gasless_transaction()) {
221                1
222            } else {
223                let tx_data = tx_data.unwrap();
224                let gas_price = tx_data.gas_price();
225                let reference_gas_price = self.authority_aggregator.load().reference_gas_price;
226                let amplification_factor = gas_price / reference_gas_price.max(1);
227                if amplification_factor == 0 {
228                    return Err(TransactionDriverError::ValidationFailed {
229                        error: UserInputError::GasPriceUnderRGP {
230                            gas_price,
231                            reference_gas_price,
232                        }
233                        .to_string(),
234                    });
235                }
236                amplification_factor
237            };
238
239        let tx_type = request.tx_type();
240        let ping_label = if request.ping_type.is_some() {
241            "true"
242        } else {
243            "false"
244        };
245        let timer = Instant::now();
246
247        self.metrics
248            .total_transactions_submitted
249            .with_label_values(&[tx_type.as_str(), ping_label])
250            .inc();
251
252        let mut backoff = ExponentialBackoff::new(
253            Duration::from_millis(100),
254            MAX_DRIVE_TRANSACTION_RETRY_DELAY,
255        );
256        let mut attempts = 0;
257        let mut latest_retriable_error = None;
258
259        let retry_loop = async {
260            loop {
261                // TODO(fastpath): Check local state before submitting transaction
262                match self
263                    .drive_transaction_once(amplification_factor, request.clone(), &options)
264                    .await
265                {
266                    Ok(resp) => {
267                        let settlement_finality_latency = timer.elapsed().as_secs_f64();
268                        self.metrics
269                            .settlement_finality_latency
270                            .with_label_values(&[tx_type.as_str(), ping_label])
271                            .observe(settlement_finality_latency);
272                        let is_out_of_expected_range = settlement_finality_latency >= 8.0
273                            || settlement_finality_latency <= 0.1;
274                        tracing::debug!(
275                            ?tx_type,
276                            ?is_out_of_expected_range,
277                            "Settlement finality latency: {:.3} seconds",
278                            settlement_finality_latency
279                        );
280                        // Record the number of retries for successful transaction
281                        self.metrics
282                            .transaction_retries
283                            .with_label_values(&["success", tx_type.as_str(), ping_label])
284                            .observe(attempts as f64);
285                        return Ok(resp);
286                    }
287                    Err(e) => {
288                        self.metrics
289                            .drive_transaction_errors
290                            .with_label_values(&[
291                                e.categorize().into(),
292                                tx_type.as_str(),
293                                ping_label,
294                            ])
295                            .inc();
296                        if !e.is_submission_retriable() {
297                            // Record the number of retries for failed transaction
298                            self.metrics
299                                .transaction_retries
300                                .with_label_values(&["failure", tx_type.as_str(), ping_label])
301                                .observe(attempts as f64);
302                            if request.transaction.is_some() {
303                                tracing::info!(
304                                    "User transaction failed to finalize (attempt {}), with non-retriable error: {} ({})",
305                                    attempts,
306                                    e,
307                                    Into::<&str>::into(e.categorize())
308                                );
309                            }
310                            return Err(e);
311                        }
312                        if request.transaction.is_some() {
313                            tracing::info!(
314                                "User transaction failed to finalize (attempt {}): {} ({}). Retrying ...",
315                                attempts,
316                                e,
317                                Into::<&str>::into(e.categorize())
318                            );
319                        }
320                        // Buffer the latest retriable error to be returned in case of timeout
321                        latest_retriable_error = Some(e);
322                    }
323                }
324
325                let overload = if let Some(e) = &latest_retriable_error {
326                    e.categorize() == ErrorCategory::ValidatorOverloaded
327                } else {
328                    false
329                };
330                let delay = if overload {
331                    // Increase delay during overload.
332                    const OVERLOAD_ADDITIONAL_DELAY: Duration = Duration::from_secs(10);
333                    backoff.next().unwrap() + OVERLOAD_ADDITIONAL_DELAY
334                } else {
335                    backoff.next().unwrap()
336                };
337
338                tracing::debug!("Retrying after {:.3}s", delay.as_secs_f32());
339                sleep(delay).await;
340
341                attempts += 1;
342            }
343        };
344
345        match timeout_duration {
346            Some(duration) => {
347                tokio::time::timeout(duration, retry_loop)
348                    .await
349                    .unwrap_or_else(|_| {
350                        // Timeout occurred, return with latest retriable error if available
351                        let e = TransactionDriverError::TimeoutWithLastRetriableError {
352                            last_error: latest_retriable_error.map(Box::new),
353                            attempts,
354                            timeout: duration,
355                        };
356                        if request.transaction.is_some() {
357                            tracing::info!(
358                                "User transaction timed out after {} attempts. Last error: {}",
359                                attempts,
360                                e
361                            );
362                        }
363                        Err(e)
364                    })
365            }
366            None => retry_loop.await,
367        }
368    }
369
370    #[instrument(level = "error", skip_all, err(level = "debug"))]
371    async fn drive_transaction_once(
372        &self,
373        amplification_factor: u64,
374        request: SubmitTxRequest,
375        options: &SubmitTransactionOptions,
376    ) -> Result<QuorumTransactionResponse, TransactionDriverError> {
377        let auth_agg = self.authority_aggregator.load();
378        let start_time = Instant::now();
379        let tx_type = request.tx_type();
380        let tx_digest = request.tx_digest();
381        let ping_type = request.ping_type;
382        // Pings carry no transaction, so they are neither restricted nor unrestricted.
383        let tx_class = request.transaction.as_ref().map(|tx| {
384            if tx
385                .transaction_data()
386                .expiration()
387                .restricts_proposers(auth_agg.committee.epoch())
388            {
389                TransactionClass::Restricted
390            } else {
391                TransactionClass::Unrestricted
392            }
393        });
394
395        let (name, submit_txn_result) = self
396            .submitter
397            .submit_transaction(
398                &auth_agg,
399                &self.client_monitor,
400                tx_type,
401                amplification_factor,
402                request,
403                options,
404            )
405            .await?;
406        if let SubmitTxResult::Rejected { error } = &submit_txn_result {
407            return Err(TransactionDriverError::ClientInternal {
408                error: format!(
409                    "SubmitTxResult::Rejected should have been returned as an error in submit_transaction(): {}",
410                    error
411                ),
412            });
413        }
414
415        // Wait for quorum effects using EffectsCertifier
416        let result = self
417            .certifier
418            .get_certified_finalized_effects(
419                &auth_agg,
420                &self.client_monitor,
421                tx_digest,
422                tx_type,
423                name,
424                submit_txn_result,
425                options,
426            )
427            .await;
428
429        if result.is_ok() {
430            self.client_monitor
431                .record_interaction_result(OperationFeedback {
432                    authority_name: name,
433                    display_name: auth_agg.get_display_name(&name),
434                    operation: if tx_type == TxType::SingleWriter {
435                        OperationType::SingleWriterFinality
436                    } else {
437                        OperationType::SharedObjectFinality
438                    },
439                    tx_class,
440                    ping_type,
441                    result: Ok(start_time.elapsed()),
442                });
443        }
444        result
445    }
446
447    // Runs a background task to send ping transactions to all validators to perform latency checks for the consensus path.
448    async fn run_latency_checks(self: Arc<Self>) {
449        const INTERVAL_BETWEEN_RUNS: Duration = Duration::from_secs(15);
450        const MAX_JITTER: Duration = Duration::from_secs(10);
451        const PING_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
452
453        let mut interval = interval(INTERVAL_BETWEEN_RUNS);
454        interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
455
456        loop {
457            interval.tick().await;
458
459            // Only run latency checks for shared object transactions since single writer
460            // transactions no longer use a separate fast path and go through consensus.
461            let auth_agg = self.authority_aggregator.load().clone();
462            let validators = auth_agg.committee.names().cloned().collect::<Vec<_>>();
463
464            self.metrics.latency_check_runs.inc();
465
466            let mut tasks = JoinSet::new();
467
468            for name in validators {
469                let display_name = auth_agg.get_display_name(&name);
470                let delay_ms = rand::thread_rng().gen_range(0..MAX_JITTER.as_millis()) as u64;
471                let self_clone = self.clone();
472
473                let task = async move {
474                    // Add some random delay to the task to avoid all tasks running at the same time
475                    if delay_ms > 0 {
476                        tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
477                    }
478                    let start_time = Instant::now();
479
480                    // Send a consensus ping transaction to the validator
481                    match self_clone
482                        .drive_transaction(
483                            SubmitTxRequest::new_ping(),
484                            SubmitTransactionOptions {
485                                allowed_validators: vec![display_name.clone()],
486                                ..Default::default()
487                            },
488                            Some(PING_REQUEST_TIMEOUT),
489                        )
490                        .await
491                    {
492                        Ok(_) => {
493                            tracing::debug!(
494                                "Ping transaction to validator {} completed end to end in {} seconds",
495                                display_name,
496                                start_time.elapsed().as_secs_f64()
497                            );
498                        }
499                        Err(err) => {
500                            tracing::debug!(
501                                "Failed to get certified finalized effects for ping transaction to validator {}: {}",
502                                display_name,
503                                err
504                            );
505                        }
506                    }
507                };
508
509                tasks.spawn(task);
510            }
511
512            while let Some(result) = tasks.join_next().await {
513                if let Err(e) = result {
514                    tracing::debug!("Error while driving ping transaction: {}", e);
515                }
516            }
517        }
518    }
519
520    fn enable_reconfig(
521        self: &Arc<Self>,
522        reconfig_observer: Arc<dyn ReconfigObserver<A> + Sync + Send>,
523    ) {
524        let driver = self.clone();
525        self.state.lock().tasks.spawn(monitored_future!(async move {
526            let mut reconfig_observer = reconfig_observer.clone_boxed();
527            reconfig_observer.run(driver).await;
528        }));
529    }
530}
531
532impl<A> ProposerSelector for TransactionDriver<A>
533where
534    A: AuthorityAPI + Send + Sync + 'static + Clone,
535{
536    fn preferred_proposers(&self, max: usize) -> Option<AllowedProposers> {
537        self.preferred_proposers_impl(max)
538    }
539}
540
541impl<A> AuthorityAggregatorUpdatable<A> for TransactionDriver<A>
542where
543    A: AuthorityAPI + Send + Sync + 'static + Clone,
544{
545    fn epoch(&self) -> EpochId {
546        self.authority_aggregator.load().committee.epoch
547    }
548
549    fn authority_aggregator(&self) -> Arc<AuthorityAggregator<A>> {
550        self.authority_aggregator.load_full()
551    }
552
553    fn update_authority_aggregator(&self, new_authorities: Arc<AuthorityAggregator<A>>) {
554        tracing::info!(
555            "Transaction Driver updating AuthorityAggregator with committee {}",
556            new_authorities.committee
557        );
558
559        self.authority_aggregator.store(new_authorities);
560    }
561}
562
563// Inner state of TransactionDriver.
564struct State {
565    tasks: JoinSet<()>,
566}
567
568impl State {
569    fn new() -> Self {
570        Self {
571            tasks: JoinSet::new(),
572        }
573    }
574}