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