sui_core/transaction_driver/
mod.rs1mod effects_certifier;
5mod error;
6mod metrics;
7mod reconfig_observer;
8mod request_retrier;
9mod transaction_submitter;
10
11pub 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
59pub 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#[derive(Clone, Default, Debug)]
69pub struct SubmitTransactionOptions {
70 pub forwarded_client_addr: Option<SocketAddr>,
73
74 pub allowed_validators: Vec<String>,
77
78 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 pub input_objects: Option<Vec<sui_types::object::Object>>,
90 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 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 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 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 fn preferred_proposers_impl(&self, max: usize) -> Option<AllowedProposers> {
171 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 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 proposers.sort_unstable();
194 proposers.dedup();
195
196 Some(AllowedProposers {
197 epoch: committee.epoch(),
198 proposers: NonEmpty::from_vec(proposers)?,
199 })
200 }
201
202 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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
563struct State {
565 tasks: JoinSet<()>,
566}
567
568impl State {
569 fn new() -> Self {
570 Self {
571 tasks: JoinSet::new(),
572 }
573 }
574}