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, ValidatorClientMetrics, ValidatorClientMonitor,
51 },
52};
53
54#[cfg(test)]
55#[path = "unit_tests/proposer_selector_tests.rs"]
56mod proposer_selector_tests;
57
58pub 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#[derive(Clone, Default, Debug)]
68pub struct SubmitTransactionOptions {
69 pub forwarded_client_addr: Option<SocketAddr>,
72
73 pub allowed_validators: Vec<String>,
76
77 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 pub input_objects: Option<Vec<sui_types::object::Object>>,
89 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 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 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 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 fn preferred_proposers_impl(&self, max: usize) -> Option<AllowedProposers> {
163 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 proposers.sort_unstable();
180 proposers.dedup();
181
182 Some(AllowedProposers {
183 epoch: committee.epoch(),
184 proposers: NonEmpty::from_vec(proposers)?,
185 })
186 }
187
188 #[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 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 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 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 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 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 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 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 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 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 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 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 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
536struct State {
538 tasks: JoinSet<()>,
539}
540
541impl State {
542 fn new() -> Self {
543 Self {
544 tasks: JoinSet::new(),
545 }
546 }
547}