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