1use std::{
5 collections::HashMap,
6 hash::{Hash, Hasher},
7 num::NonZeroUsize,
8 sync::{Arc, Weak},
9 time::{Duration, SystemTime},
10};
11
12use serde::{Deserialize, Serialize};
13
14use super::authority_per_epoch_store::AuthorityPerEpochStore;
15use super::weighted_moving_average::WeightedMovingAverage;
16use crate::consensus_adapter::SubmitToConsensus;
17use governor::{Quota, RateLimiter, clock::MonotonicClock};
18use itertools::Itertools;
19use lru::LruCache;
20#[cfg(not(msim))]
21use mysten_common::in_antithesis;
22use mysten_common::{assert_reachable, debug_fatal, in_test_configuration};
23use mysten_metrics::{monitored_scope, spawn_monitored_task};
24use rand::{Rng, SeedableRng, random, rngs, thread_rng};
25use simple_moving_average::{SMA, SingleSumSMA};
26use sui_config::node::ExecutionTimeObserverConfig;
27use sui_protocol_config::{ExecutionTimeEstimateParams, PerObjectCongestionControlMode};
28use sui_types::{
29 base_types::ObjectID,
30 committee::Committee,
31 error::SuiErrorKind,
32 execution::{ExecutionTimeObservationKey, ExecutionTiming},
33 messages_consensus::{AuthorityIndex, ConsensusTransaction, ExecutionTimeObservation},
34 transaction::{
35 Command, ProgrammableTransaction, StoredExecutionTimeObservations, TransactionData,
36 TransactionDataAPI, TransactionKind,
37 },
38};
39use tokio::{sync::mpsc, time::Instant};
40use tracing::{debug, info, trace, warn};
41
42const SMA_LOCAL_OBSERVATION_WINDOW_SIZE: usize = 20;
45const OBJECT_UTILIZATION_METRIC_HASH_MODULUS: u8 = 32;
46
47#[cfg(not(msim))]
54fn antithesis_enable_injecting_synthetic_execution_time() -> bool {
55 use std::sync::OnceLock;
56 static ENABLE_INJECTION: OnceLock<bool> = OnceLock::new();
57 *ENABLE_INJECTION.get_or_init(|| {
58 if !in_antithesis() {
59 return false;
60 }
61
62 std::env::var("ANTITHESIS_ENABLE_EXECUTION_TIME_INJECTION")
63 .map(|v| v.to_lowercase() == "true" || v == "1")
64 .unwrap_or(true)
65 })
66}
67
68pub struct ExecutionTimeObserver {
70 epoch_store: Weak<AuthorityPerEpochStore>,
71 consensus_adapter: Box<dyn SubmitToConsensus>,
72
73 protocol_params: ExecutionTimeEstimateParams,
74 config: ExecutionTimeObserverConfig,
75
76 local_observations: LruCache<ExecutionTimeObservationKey, LocalObservations>,
77
78 object_utilization_tracker: LruCache<ObjectID, ObjectUtilization>,
82
83 indebted_objects: Vec<ObjectID>,
85
86 sharing_rate_limiter: RateLimiter<
87 governor::state::NotKeyed,
88 governor::state::InMemoryState,
89 governor::clock::MonotonicClock,
90 governor::middleware::NoOpMiddleware<
91 <governor::clock::MonotonicClock as governor::clock::Clock>::Instant,
92 >,
93 >,
94
95 next_generation_number: u64,
96}
97
98#[derive(Debug, Clone)]
99pub struct LocalObservations {
100 moving_average: SingleSumSMA<Duration, u32, SMA_LOCAL_OBSERVATION_WINDOW_SIZE>,
101 weighted_moving_average: WeightedMovingAverage,
102 last_shared: Option<(Duration, Instant)>,
103 config: ExecutionTimeObserverConfig,
104}
105
106impl LocalObservations {
107 fn new(config: ExecutionTimeObserverConfig, default_duration: Duration) -> Self {
108 let window_size = config.weighted_moving_average_window_size();
109 Self {
110 moving_average: SingleSumSMA::from_zero(Duration::ZERO),
111 weighted_moving_average: WeightedMovingAverage::new(
112 default_duration.as_micros() as u64,
113 window_size,
114 ),
115 last_shared: None,
116 config,
117 }
118 }
119
120 fn add_sample(&mut self, duration: Duration, gas_price: u64) {
121 self.moving_average.add_sample(duration);
122 self.weighted_moving_average
123 .add_sample(duration.as_micros() as u64, gas_price);
124 }
125
126 fn get_average(&self) -> Duration {
127 if self.config.enable_gas_price_weighting() {
128 Duration::from_micros(self.weighted_moving_average.get_weighted_average())
129 } else {
130 self.moving_average.get_average()
131 }
132 }
133
134 fn diff_exceeds_threshold(
135 &self,
136 new_average: Duration,
137 threshold: f64,
138 min_interval: Duration,
139 ) -> bool {
140 let Some((last_shared, last_shared_timestamp)) = self.last_shared else {
141 return true;
143 };
144
145 if last_shared_timestamp.elapsed() < min_interval {
146 return false;
147 }
148
149 if threshold >= 0.0 {
150 new_average
152 .checked_sub(last_shared)
153 .is_some_and(|diff| diff > last_shared.mul_f64(threshold))
154 } else {
155 last_shared
157 .checked_sub(new_average)
158 .is_some_and(|diff| diff > last_shared.mul_f64(-threshold))
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
164pub struct ObjectUtilization {
165 excess_execution_time: Duration,
166 last_measured: Option<Instant>,
167 was_overutilized: bool, }
169
170impl ObjectUtilization {
171 pub fn overutilized(&self, config: &ExecutionTimeObserverConfig) -> bool {
172 self.excess_execution_time > config.observation_sharing_object_utilization_threshold()
173 }
174}
175
176impl ExecutionTimeObserver {
178 pub fn spawn(
179 epoch_store: Arc<AuthorityPerEpochStore>,
180 consensus_adapter: Box<dyn SubmitToConsensus>,
181 config: ExecutionTimeObserverConfig,
182 ) {
183 let PerObjectCongestionControlMode::ExecutionTimeEstimate(protocol_params) = epoch_store
184 .protocol_config()
185 .per_object_congestion_control_mode()
186 else {
187 info!(
188 "ExecutionTimeObserver disabled because per-object congestion control mode is not ExecutionTimeEstimate"
189 );
190 return;
191 };
192
193 let (tx_local_execution_time, mut rx_local_execution_time) =
194 mpsc::channel(config.observation_channel_capacity().into());
195 let (tx_object_debts, mut rx_object_debts) =
196 mpsc::channel(config.object_debt_channel_capacity().into());
197 epoch_store.set_local_execution_time_channels(tx_local_execution_time, tx_object_debts);
198
199 let mut observer = Self {
201 epoch_store: Arc::downgrade(&epoch_store),
202 consensus_adapter,
203 local_observations: LruCache::new(config.observation_cache_size()),
204 object_utilization_tracker: LruCache::new(config.object_utilization_cache_size()),
205 indebted_objects: Vec::new(),
206 sharing_rate_limiter: RateLimiter::direct_with_clock(
207 Quota::per_second(config.observation_sharing_rate_limit())
208 .allow_burst(config.observation_sharing_burst_limit()),
209 &MonotonicClock,
210 ),
211 protocol_params,
212 config,
213 next_generation_number: SystemTime::now()
214 .duration_since(std::time::UNIX_EPOCH)
215 .expect("Sui did not exist prior to 1970")
216 .as_micros()
217 .try_into()
218 .expect("This build of sui is not supported in the year 500,000"),
219 };
220 spawn_monitored_task!(epoch_store.within_alive_epoch(async move {
221 loop {
222 tokio::select! {
223 Some(object_debts) = rx_object_debts.recv() => {
225 observer.update_indebted_objects(object_debts);
226 }
227 Some((tx, timings, total_duration, gas_price)) = rx_local_execution_time.recv() => {
228 observer
229 .record_local_observations(&tx, &timings, total_duration, gas_price);
230 }
231 else => { break }
232 }
233 }
234 info!("shutting down ExecutionTimeObserver");
235 }));
236 }
237
238 #[cfg(test)]
239 fn new_for_testing(
240 epoch_store: Arc<AuthorityPerEpochStore>,
241 consensus_adapter: Box<dyn SubmitToConsensus>,
242 observation_sharing_object_utilization_threshold: Duration,
243 enable_gas_price_weighting: bool,
244 ) -> Self {
245 let PerObjectCongestionControlMode::ExecutionTimeEstimate(protocol_params) = epoch_store
246 .protocol_config()
247 .per_object_congestion_control_mode()
248 else {
249 panic!(
250 "tried to construct test ExecutionTimeObserver when congestion control mode is not ExecutionTimeEstimate"
251 );
252 };
253 Self {
254 epoch_store: Arc::downgrade(&epoch_store),
255 consensus_adapter,
256 protocol_params,
257 config: ExecutionTimeObserverConfig {
258 observation_sharing_object_utilization_threshold: Some(
259 observation_sharing_object_utilization_threshold,
260 ),
261 enable_gas_price_weighting: Some(enable_gas_price_weighting),
262 ..ExecutionTimeObserverConfig::default()
263 },
264 local_observations: LruCache::new(NonZeroUsize::new(10000).unwrap()),
265 object_utilization_tracker: LruCache::new(NonZeroUsize::new(50000).unwrap()),
266 indebted_objects: Vec::new(),
267 sharing_rate_limiter: RateLimiter::direct_with_clock(
268 Quota::per_hour(std::num::NonZeroU32::MAX),
269 &MonotonicClock,
270 ),
271 next_generation_number: SystemTime::now()
272 .duration_since(std::time::UNIX_EPOCH)
273 .expect("Sui did not exist prior to 1970")
274 .as_micros()
275 .try_into()
276 .expect("This build of sui is not supported in the year 500,000"),
277 }
278 }
279
280 fn record_local_observations(
285 &mut self,
286 tx: &ProgrammableTransaction,
287 timings: &[ExecutionTiming],
288 total_duration: Duration,
289 gas_price: u64,
290 ) {
291 let _scope = monitored_scope("ExecutionTimeObserver::record_local_observations");
292
293 #[cfg(msim)]
295 let should_inject = self.config.inject_synthetic_execution_time();
296 #[cfg(not(msim))]
297 let should_inject = antithesis_enable_injecting_synthetic_execution_time();
298
299 if should_inject {
300 let (generated_timings, generated_duration) = self.generate_test_timings(tx, timings);
301 self.record_local_observations_timing(
302 tx,
303 &generated_timings,
304 generated_duration,
305 gas_price,
306 )
307 } else {
308 self.record_local_observations_timing(tx, timings, total_duration, gas_price)
309 }
310 }
311
312 fn record_local_observations_timing(
313 &mut self,
314 tx: &ProgrammableTransaction,
315 timings: &[ExecutionTiming],
316 total_duration: Duration,
317 gas_price: u64,
318 ) {
319 let Some(epoch_store) = self.epoch_store.upgrade() else {
320 debug!("epoch is ending, dropping execution time observation");
321 return;
322 };
323 let timings = if timings.len() > tx.commands.len() {
324 warn!(
325 executed_commands = timings.len(),
326 original_commands = tx.commands.len(),
327 "execution produced more timings than the original PTB commands; using the trailing timings for local execution-time observations"
328 );
329 &timings[timings.len() - tx.commands.len()..]
330 } else {
331 timings
332 };
333
334 let mut uses_indebted_object = false;
335
336 let max_excess_per_object_execution_time = tx
339 .shared_input_objects()
340 .filter_map(|obj| obj.is_accessed_exclusively().then_some(obj.id))
341 .map(|id| {
342 if !uses_indebted_object && self.indebted_objects.binary_search(&id).is_ok() {
344 uses_indebted_object = true;
345 }
346
347 let now = Instant::now();
357 let utilization =
358 self.object_utilization_tracker
359 .get_or_insert_mut(id, || ObjectUtilization {
360 excess_execution_time: Duration::ZERO,
361 last_measured: None,
362 was_overutilized: false,
363 });
364 let overutilized_at_start = utilization.overutilized(&self.config);
365 utilization.excess_execution_time += total_duration;
366 utilization.excess_execution_time =
367 utilization.excess_execution_time.saturating_sub(
368 utilization
369 .last_measured
370 .map(|last_measured| {
371 now.duration_since(last_measured)
372 .mul_f64(self.protocol_params.target_utilization as f64 / 100.0)
373 })
374 .unwrap_or(Duration::MAX),
375 );
376 utilization.last_measured = Some(now);
377 if utilization.overutilized(&self.config) {
378 utilization.was_overutilized = true;
379 }
380
381 if !overutilized_at_start && utilization.overutilized(&self.config) {
383 trace!("object {id:?} is overutilized");
384 epoch_store
385 .metrics
386 .epoch_execution_time_observer_overutilized_objects
387 .inc();
388 } else if overutilized_at_start && !utilization.overutilized(&self.config) {
389 epoch_store
390 .metrics
391 .epoch_execution_time_observer_overutilized_objects
392 .dec();
393 }
394 if utilization.was_overutilized {
395 let key = if self.config.report_object_utilization_metric_with_full_id() {
396 id.to_string()
397 } else {
398 let key_lsb = id.into_bytes()[ObjectID::LENGTH - 1];
399 let hash = key_lsb % OBJECT_UTILIZATION_METRIC_HASH_MODULUS;
400 format!("{:x}", hash)
401 };
402
403 epoch_store
404 .metrics
405 .epoch_execution_time_observer_object_utilization
406 .with_label_values(&[key.as_str()])
407 .inc_by(total_duration.as_secs_f64());
408 }
409
410 utilization.excess_execution_time
411 })
412 .max()
413 .unwrap_or(Duration::ZERO);
414 epoch_store
415 .metrics
416 .epoch_execution_time_observer_utilization_cache_size
417 .set(self.object_utilization_tracker.len() as i64);
418
419 let total_command_duration: Duration = timings.iter().map(|t| t.duration()).sum();
420 let extra_overhead = total_duration - total_command_duration;
421
422 let mut to_share = Vec::with_capacity(tx.commands.len());
423 for (i, timing) in timings.iter().enumerate() {
424 let command = &tx.commands[i];
425
426 if matches!(command, Command::Publish(_, _)) {
428 continue;
429 }
430
431 let mut command_duration = timing.duration();
433
434 let overhead_factor = if total_command_duration > Duration::ZERO {
436 command_duration.as_secs_f64() / total_command_duration.as_secs_f64()
437 } else {
438 1.0 / (tx.commands.len() as f64)
440 };
441 command_duration += extra_overhead.mul_f64(overhead_factor);
442
443 command_duration = command_duration.div_f64(command_length(command).get() as f64);
446
447 let key = ExecutionTimeObservationKey::from_command(command);
449 let local_observation = self.local_observations.get_or_insert_mut(key.clone(), || {
450 LocalObservations::new(self.config.clone(), Duration::ZERO)
451 });
452 local_observation.add_sample(command_duration, gas_price);
453
454 let new_average = local_observation.get_average();
459 let mut should_share = false;
460
461 if max_excess_per_object_execution_time
463 >= self
464 .config
465 .observation_sharing_object_utilization_threshold()
466 && local_observation.diff_exceeds_threshold(
467 new_average,
468 self.config.observation_sharing_diff_threshold(),
469 self.config.observation_sharing_min_interval(),
470 )
471 {
472 should_share = true;
473 epoch_store
474 .metrics
475 .epoch_execution_time_observations_sharing_reason
476 .with_label_values(&["utilization"])
477 .inc();
478 };
479
480 if uses_indebted_object
482 && local_observation.diff_exceeds_threshold(
483 new_average,
484 -self.config.observation_sharing_diff_threshold(),
485 self.config.observation_sharing_min_interval(),
486 )
487 {
488 should_share = true;
489 epoch_store
490 .metrics
491 .epoch_execution_time_observations_sharing_reason
492 .with_label_values(&["indebted"])
493 .inc();
494 }
495
496 if should_share {
497 debug!("sharing new execution time observation for {key:?}: {new_average:?}");
498 to_share.push((key, new_average));
499 local_observation.last_shared = Some((new_average, Instant::now()));
500 }
501 }
502
503 self.share_observations(to_share);
505 }
506
507 fn generate_test_timings(
508 &self,
509 tx: &ProgrammableTransaction,
510 timings: &[ExecutionTiming],
511 ) -> (Vec<ExecutionTiming>, Duration) {
512 #[allow(clippy::disallowed_methods)]
513 let generated_timings: Vec<_> = tx
514 .commands
515 .iter()
516 .zip(timings.iter())
518 .map(|(command, timing)| {
519 let key = ExecutionTimeObservationKey::from_command(command);
520 let duration = self.get_test_duration(&key);
521 if timing.is_abort() {
522 ExecutionTiming::Abort(duration)
523 } else {
524 ExecutionTiming::Success(duration)
525 }
526 })
527 .collect();
528
529 let total_duration = generated_timings
530 .iter()
531 .map(|t| t.duration())
532 .sum::<Duration>()
533 + thread_rng().gen_range(Duration::from_millis(10)..Duration::from_millis(50));
534
535 (generated_timings, total_duration)
536 }
537
538 fn get_test_duration(&self, key: &ExecutionTimeObservationKey) -> Duration {
539 #[cfg(msim)]
540 let should_inject = self.config.inject_synthetic_execution_time();
541 #[cfg(not(msim))]
542 let should_inject = false;
543
544 if !in_test_configuration() && !should_inject {
545 panic!("get_test_duration called in non-test configuration");
546 }
547
548 static PER_TEST_SEED: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
549
550 let mut hasher = std::collections::hash_map::DefaultHasher::new();
551
552 let checkpoint_digest_used = self
553 .epoch_store
554 .upgrade()
555 .and_then(|store| {
556 store
557 .get_lowest_non_genesis_checkpoint_summary()
558 .ok()
559 .flatten()
560 })
561 .map(|summary| summary.content_digest.hash(&mut hasher))
562 .is_some();
563
564 if !checkpoint_digest_used {
565 PER_TEST_SEED.get_or_init(random::<u64>).hash(&mut hasher);
566 }
567
568 key.hash(&mut hasher);
569 let mut rng = rngs::StdRng::seed_from_u64(hasher.finish());
570 rng.gen_range(Duration::from_millis(100)..Duration::from_millis(600))
571 }
572
573 fn share_observations(&mut self, to_share: Vec<(ExecutionTimeObservationKey, Duration)>) {
574 if to_share.is_empty() {
575 return;
576 }
577 let Some(epoch_store) = self.epoch_store.upgrade() else {
578 debug!("epoch is ending, dropping execution time observation");
579 return;
580 };
581
582 let num_observations = to_share.len() as u64;
583
584 if let Err(e) = self.sharing_rate_limiter.check() {
586 epoch_store
587 .metrics
588 .epoch_execution_time_observations_dropped
589 .with_label_values(&["global_rate_limit"])
590 .inc_by(num_observations);
591 debug!("rate limit exceeded, dropping execution time observation; {e:?}");
592 return;
593 }
594
595 let epoch_store = epoch_store.clone();
596 let transaction = ConsensusTransaction::new_execution_time_observation(
597 ExecutionTimeObservation::new(epoch_store.name, self.next_generation_number, to_share),
598 );
599 self.next_generation_number += 1;
600
601 if let Err(e) = self.consensus_adapter.submit_best_effort(
602 &transaction,
603 &epoch_store,
604 Duration::from_secs(5),
605 ) {
606 if !matches!(e.as_inner(), SuiErrorKind::EpochEnded(_)) {
607 epoch_store
608 .metrics
609 .epoch_execution_time_observations_dropped
610 .with_label_values(&["submit_to_consensus"])
611 .inc_by(num_observations);
612 warn!("failed to submit execution time observation: {e:?}");
613 }
614 } else {
615 assert_reachable!("successfully shares execution time observations");
619 epoch_store
620 .metrics
621 .epoch_execution_time_observations_shared
622 .inc_by(num_observations);
623 }
624 }
625
626 fn update_indebted_objects(&mut self, mut object_debts: Vec<ObjectID>) {
627 let _scope = monitored_scope("ExecutionTimeObserver::update_indebted_objects");
628
629 let Some(epoch_store) = self.epoch_store.upgrade() else {
630 debug!("epoch is ending, dropping indebted object update");
631 return;
632 };
633
634 object_debts.sort_unstable();
635 object_debts.dedup();
636 self.indebted_objects = object_debts;
637 epoch_store
638 .metrics
639 .epoch_execution_time_observer_indebted_objects
640 .set(self.indebted_objects.len() as i64);
641 }
642}
643
644pub const EXTRA_FIELD_EXECUTION_TIME_ESTIMATES_KEY: u64 = 0;
647
648pub const EXTRA_FIELD_EXECUTION_TIME_ESTIMATES_CHUNK_COUNT_KEY: u64 = 1;
650
651pub struct ExecutionTimeEstimator {
654 committee: Arc<Committee>,
655 protocol_params: ExecutionTimeEstimateParams,
656
657 consensus_observations: HashMap<ExecutionTimeObservationKey, ConsensusObservations>,
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct ConsensusObservations {
662 observations: Vec<(u64 , Option<Duration>)>, stake_weighted_median: Option<Duration>, }
665
666impl ConsensusObservations {
667 fn update_stake_weighted_median(
668 &mut self,
669 committee: &Committee,
670 config: &ExecutionTimeEstimateParams,
671 ) {
672 let mut stake_with_observations = 0;
673 let sorted_observations: Vec<_> = self
674 .observations
675 .iter()
676 .enumerate()
677 .filter_map(|(i, (_, duration))| {
678 duration.map(|duration| {
679 let authority_index: AuthorityIndex = i.try_into().unwrap();
680 stake_with_observations += committee.stake_by_index(authority_index).unwrap();
681 (duration, authority_index)
682 })
683 })
684 .sorted()
685 .collect();
686
687 if stake_with_observations < config.stake_weighted_median_threshold {
689 self.stake_weighted_median = None;
690 return;
691 }
692
693 let median_stake = stake_with_observations / 2;
695 let mut running_stake = 0;
696 for (duration, authority_index) in sorted_observations {
697 running_stake += committee.stake_by_index(authority_index).unwrap();
698 if running_stake > median_stake {
699 self.stake_weighted_median = Some(duration);
700 break;
701 }
702 }
703 }
704}
705
706impl ExecutionTimeEstimator {
707 pub fn new(
708 committee: Arc<Committee>,
709 protocol_params: ExecutionTimeEstimateParams,
710 initial_observations: impl Iterator<
711 Item = (
712 AuthorityIndex,
713 Option<u64>,
714 ExecutionTimeObservationKey,
715 Duration,
716 ),
717 >,
718 ) -> Self {
719 let mut estimator = Self {
720 committee,
721 protocol_params,
722 consensus_observations: HashMap::new(),
723 };
724 for (source, generation, key, duration) in initial_observations {
725 estimator.process_observation_from_consensus(
726 source,
727 generation,
728 key.to_owned(),
729 duration,
730 true,
731 );
732 }
733 for observation in estimator.consensus_observations.values_mut() {
734 observation
735 .update_stake_weighted_median(&estimator.committee, &estimator.protocol_params);
736 }
737 estimator
738 }
739
740 #[cfg(test)]
741 pub fn new_for_testing() -> Self {
742 let (committee, _) = Committee::new_simple_test_committee_of_size(1);
743 Self {
744 committee: Arc::new(committee),
745 protocol_params: ExecutionTimeEstimateParams {
746 target_utilization: 100,
747 max_estimate_us: u64::MAX,
748 ..ExecutionTimeEstimateParams::default()
749 },
750 consensus_observations: HashMap::new(),
751 }
752 }
753
754 pub fn process_observations_from_consensus(
755 &mut self,
756 source: AuthorityIndex,
757 generation: Option<u64>,
758 observations: &[(ExecutionTimeObservationKey, Duration)],
759 ) {
760 for (key, duration) in observations {
761 self.process_observation_from_consensus(
762 source,
763 generation,
764 key.to_owned(),
765 *duration,
766 false,
767 );
768 }
769 }
770
771 fn process_observation_from_consensus(
772 &mut self,
773 source: AuthorityIndex,
774 generation: Option<u64>,
775 observation_key: ExecutionTimeObservationKey,
776 duration: Duration,
777 skip_update: bool,
778 ) {
779 if matches!(observation_key, ExecutionTimeObservationKey::Publish) {
780 warn!(
782 "dropping Publish observation received from possibly-Byzanitine authority {source}"
783 );
784 return;
785 }
786
787 assert_reachable!("receives some valid execution time observations");
788
789 let observations = self
790 .consensus_observations
791 .entry(observation_key)
792 .or_insert_with(|| {
793 let len = self.committee.num_members();
794 let mut empty_observations = Vec::with_capacity(len);
795 empty_observations.resize(len, (0, None));
796 ConsensusObservations {
797 observations: empty_observations,
798 stake_weighted_median: if self
799 .protocol_params
800 .default_none_duration_for_new_keys
801 {
802 None
803 } else {
804 Some(Duration::ZERO)
805 },
806 }
807 });
808
809 let (obs_generation, obs_duration) =
810 &mut observations.observations[TryInto::<usize>::try_into(source).unwrap()];
811 if generation.is_some_and(|generation| *obs_generation >= generation) {
812 return;
814 }
815 *obs_generation = generation.unwrap_or(0);
816 *obs_duration = Some(duration);
817 if !skip_update {
818 observations.update_stake_weighted_median(&self.committee, &self.protocol_params);
819 }
820 }
821
822 pub fn get_estimate(&self, tx: &TransactionData) -> Duration {
823 let TransactionKind::ProgrammableTransaction(tx) = tx.kind() else {
824 debug_fatal!("get_estimate called on non-ProgrammableTransaction");
825 return Duration::ZERO;
826 };
827 tx.commands
828 .iter()
829 .map(|command| {
830 let key = ExecutionTimeObservationKey::from_command(command);
831 self.consensus_observations
832 .get(&key)
833 .and_then(|obs| obs.stake_weighted_median)
834 .unwrap_or_else(|| key.default_duration())
835 .mul_f64(command_length(command).get() as f64)
838 })
839 .sum::<Duration>()
840 .min(Duration::from_micros(self.protocol_params.max_estimate_us))
841 }
842
843 pub fn take_observations(&mut self) -> StoredExecutionTimeObservations {
844 StoredExecutionTimeObservations::V1(
845 self.consensus_observations
846 .drain()
847 .map(|(key, observations)| {
848 let observations = observations
849 .observations
850 .into_iter()
851 .enumerate()
852 .filter_map(|(idx, (_, duration))| {
853 duration.map(|d| {
854 (
855 self.committee
856 .authority_by_index(idx.try_into().unwrap())
857 .cloned()
858 .unwrap(),
859 d,
860 )
861 })
862 })
863 .collect();
864 (key, observations)
865 })
866 .collect(),
867 )
868 }
869
870 pub fn get_observations(&self) -> Vec<(ExecutionTimeObservationKey, ConsensusObservations)> {
871 self.consensus_observations
872 .iter()
873 .map(|(key, observations)| (key.clone(), observations.clone()))
874 .collect()
875 }
876}
877
878fn command_length(command: &Command) -> NonZeroUsize {
879 NonZeroUsize::new(match command {
882 Command::MoveCall(_) => 1,
883 Command::TransferObjects(src, _) => src.len() + 1,
884 Command::SplitCoins(_, amts) => amts.len() + 1,
885 Command::MergeCoins(_, src) => src.len() + 1,
886 Command::Publish(_, _) => 1,
887 Command::MakeMoveVec(_, src) => src.len() + 1,
888 Command::Upgrade(_, _, _, _) => 1,
889 })
890 .unwrap()
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896 use crate::authority::test_authority_builder::TestAuthorityBuilder;
897 use crate::checkpoints::CheckpointStore;
898 use crate::consensus_adapter::{
899 ConsensusAdapter, ConsensusAdapterMetrics, MockConsensusClient,
900 };
901 use sui_protocol_config::ProtocolConfig;
902 use sui_types::base_types::{ObjectID, SequenceNumber, SuiAddress};
903 use sui_types::transaction::{
904 Argument, CallArg, ObjectArg, ProgrammableMoveCall, SharedObjectMutability,
905 };
906 use {
907 rand::{Rng, SeedableRng},
908 sui_protocol_config::ProtocolVersion,
909 sui_types::supported_protocol_versions::Chain,
910 };
911
912 #[tokio::test]
913 async fn test_record_local_observations() {
914 telemetry_subscribers::init_for_testing();
915
916 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
917 config.set_per_object_congestion_control_mode_for_testing(
918 PerObjectCongestionControlMode::ExecutionTimeEstimate(
919 ExecutionTimeEstimateParams {
920 target_utilization: 100,
921 allowed_txn_cost_overage_burst_limit_us: 0,
922 randomness_scalar: 100,
923 max_estimate_us: u64::MAX,
924 stored_observations_num_included_checkpoints: 10,
925 stored_observations_limit: u64::MAX,
926 stake_weighted_median_threshold: 0,
927 default_none_duration_for_new_keys: true,
928 observations_chunk_size: Some(18),
929 },
930 ),
931 );
932 config
933 });
934
935 let mock_consensus_client = MockConsensusClient::new();
936 let authority = TestAuthorityBuilder::new().build().await;
937 let epoch_store = authority.epoch_store_for_testing();
938 let consensus_adapter = Arc::new(ConsensusAdapter::new(
939 Arc::new(mock_consensus_client),
940 CheckpointStore::new_for_tests(),
941 authority.name,
942 100_000,
943 100_000,
944 ConsensusAdapterMetrics::new_test(),
945 Arc::new(tokio::sync::Notify::new()),
946 ));
947 let mut observer = ExecutionTimeObserver::new_for_testing(
948 epoch_store.clone(),
949 Box::new(consensus_adapter.clone()),
950 Duration::ZERO, false, );
953
954 let package = ObjectID::random();
956 let module = "test_module".to_string();
957 let function = "test_function".to_string();
958 let ptb = ProgrammableTransaction {
959 inputs: vec![],
960 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
961 package,
962 module: module.clone(),
963 function: function.clone(),
964 type_arguments: vec![],
965 arguments: vec![],
966 }))],
967 };
968
969 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
971 let total_duration = Duration::from_millis(110);
972 observer.record_local_observations(&ptb, &timings, total_duration, 1);
973
974 let key = ExecutionTimeObservationKey::MoveEntryPoint {
975 package,
976 module: module.clone(),
977 function: function.clone(),
978 type_arguments: vec![],
979 };
980
981 let local_obs = observer.local_observations.get(&key).unwrap();
983 assert_eq!(
984 local_obs.get_average(),
985 Duration::from_millis(110)
987 );
988 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
989
990 let timings = vec![ExecutionTiming::Success(Duration::from_millis(110))];
992 let total_duration = Duration::from_millis(120);
993 observer.record_local_observations(&ptb, &timings, total_duration, 1);
994
995 let local_obs = observer.local_observations.get(&key).unwrap();
997 assert_eq!(
998 local_obs.get_average(),
999 Duration::from_millis(115)
1001 );
1002 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1004
1005 let timings = vec![ExecutionTiming::Success(Duration::from_millis(120))];
1007 let total_duration = Duration::from_millis(130);
1008 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1009
1010 let local_obs = observer.local_observations.get(&key).unwrap();
1012 assert_eq!(
1013 local_obs.get_average(),
1014 Duration::from_millis(120)
1016 );
1017 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1020
1021 observer
1023 .local_observations
1024 .get_mut(&key)
1025 .unwrap()
1026 .last_shared = Some((
1027 Duration::from_millis(110),
1028 Instant::now() - Duration::from_secs(60),
1029 ));
1030
1031 let timings = vec![ExecutionTiming::Success(Duration::from_millis(120))];
1033 let total_duration = Duration::from_millis(160);
1034 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1035
1036 let local_obs = observer.local_observations.get(&key).unwrap();
1039 assert_eq!(
1040 local_obs.get_average(),
1041 Duration::from_millis(130)
1043 );
1044 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(130));
1045 }
1046
1047 #[tokio::test]
1048 async fn test_record_local_observations_with_gas_price_weighting() {
1049 telemetry_subscribers::init_for_testing();
1050
1051 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1052 config.set_per_object_congestion_control_mode_for_testing(
1053 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1054 ExecutionTimeEstimateParams {
1055 target_utilization: 100,
1056 allowed_txn_cost_overage_burst_limit_us: 0,
1057 randomness_scalar: 100,
1058 max_estimate_us: u64::MAX,
1059 stored_observations_num_included_checkpoints: 10,
1060 stored_observations_limit: u64::MAX,
1061 stake_weighted_median_threshold: 0,
1062 default_none_duration_for_new_keys: true,
1063 observations_chunk_size: Some(18),
1064 },
1065 ),
1066 );
1067 config
1068 });
1069
1070 let mock_consensus_client = MockConsensusClient::new();
1071 let authority = TestAuthorityBuilder::new().build().await;
1072 let epoch_store = authority.epoch_store_for_testing();
1073 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1074 Arc::new(mock_consensus_client),
1075 CheckpointStore::new_for_tests(),
1076 authority.name,
1077 100_000,
1078 100_000,
1079 ConsensusAdapterMetrics::new_test(),
1080 Arc::new(tokio::sync::Notify::new()),
1081 ));
1082 let mut observer = ExecutionTimeObserver::new_for_testing(
1083 epoch_store.clone(),
1084 Box::new(consensus_adapter.clone()),
1085 Duration::ZERO, true, );
1088
1089 let package = ObjectID::random();
1091 let module = "test_module".to_string();
1092 let function = "test_function".to_string();
1093 let ptb = ProgrammableTransaction {
1094 inputs: vec![],
1095 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1096 package,
1097 module: module.clone(),
1098 function: function.clone(),
1099 type_arguments: vec![],
1100 arguments: vec![],
1101 }))],
1102 };
1103
1104 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
1106 let total_duration = Duration::from_millis(110);
1107 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1108
1109 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1110 package,
1111 module: module.clone(),
1112 function: function.clone(),
1113 type_arguments: vec![],
1114 };
1115
1116 let local_obs = observer.local_observations.get(&key).unwrap();
1118 assert_eq!(
1119 local_obs.get_average(),
1120 Duration::from_millis(110)
1122 );
1123 assert_eq!(local_obs.last_shared.unwrap().0, Duration::from_millis(110));
1124
1125 let timings = vec![ExecutionTiming::Success(Duration::from_millis(110))];
1127 let total_duration = Duration::from_millis(120);
1128 observer.record_local_observations(&ptb, &timings, total_duration, 2);
1129
1130 let local_obs = observer.local_observations.get(&key).unwrap();
1132 assert_eq!(
1133 local_obs.get_average(),
1134 Duration::from_micros(116_666)
1137 );
1138 }
1139
1140 #[tokio::test]
1141 async fn test_record_local_observations_with_multiple_commands() {
1142 telemetry_subscribers::init_for_testing();
1143
1144 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1145 config.set_per_object_congestion_control_mode_for_testing(
1146 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1147 ExecutionTimeEstimateParams {
1148 target_utilization: 100,
1149 allowed_txn_cost_overage_burst_limit_us: 0,
1150 randomness_scalar: 0,
1151 max_estimate_us: u64::MAX,
1152 stored_observations_num_included_checkpoints: 10,
1153 stored_observations_limit: u64::MAX,
1154 stake_weighted_median_threshold: 0,
1155 default_none_duration_for_new_keys: true,
1156 observations_chunk_size: Some(18),
1157 },
1158 ),
1159 );
1160 config
1161 });
1162
1163 let mock_consensus_client = MockConsensusClient::new();
1164 let authority = TestAuthorityBuilder::new().build().await;
1165 let epoch_store = authority.epoch_store_for_testing();
1166 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1167 Arc::new(mock_consensus_client),
1168 CheckpointStore::new_for_tests(),
1169 authority.name,
1170 100_000,
1171 100_000,
1172 ConsensusAdapterMetrics::new_test(),
1173 Arc::new(tokio::sync::Notify::new()),
1174 ));
1175 let mut observer = ExecutionTimeObserver::new_for_testing(
1176 epoch_store.clone(),
1177 Box::new(consensus_adapter.clone()),
1178 Duration::ZERO, false, );
1181
1182 let package = ObjectID::random();
1184 let module = "test_module".to_string();
1185 let function = "test_function".to_string();
1186 let ptb = ProgrammableTransaction {
1187 inputs: vec![],
1188 commands: vec![
1189 Command::MoveCall(Box::new(ProgrammableMoveCall {
1190 package,
1191 module: module.clone(),
1192 function: function.clone(),
1193 type_arguments: vec![],
1194 arguments: vec![],
1195 })),
1196 Command::TransferObjects(
1197 vec![Argument::Input(1), Argument::Input(2)],
1199 Argument::Input(0),
1200 ),
1201 ],
1202 };
1203 let timings = vec![
1204 ExecutionTiming::Success(Duration::from_millis(100)),
1205 ExecutionTiming::Success(Duration::from_millis(50)),
1206 ];
1207 let total_duration = Duration::from_millis(180);
1208 observer.record_local_observations(&ptb, &timings, total_duration, 1);
1209
1210 let move_key = ExecutionTimeObservationKey::MoveEntryPoint {
1212 package,
1213 module: module.clone(),
1214 function: function.clone(),
1215 type_arguments: vec![],
1216 };
1217 let move_obs = observer.local_observations.get(&move_key).unwrap();
1218 assert_eq!(
1219 move_obs.get_average(),
1220 Duration::from_millis(120)
1222 );
1223
1224 let transfer_obs = observer
1225 .local_observations
1226 .get(&ExecutionTimeObservationKey::TransferObjects)
1227 .unwrap();
1228 assert_eq!(
1229 transfer_obs.get_average(),
1230 Duration::from_millis(20)
1234 );
1235 }
1236
1237 #[tokio::test]
1238 async fn test_record_local_observations_with_object_utilization_threshold() {
1239 telemetry_subscribers::init_for_testing();
1240
1241 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1242 config.set_per_object_congestion_control_mode_for_testing(
1243 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1244 ExecutionTimeEstimateParams {
1245 target_utilization: 100,
1246 allowed_txn_cost_overage_burst_limit_us: 0,
1247 randomness_scalar: 0,
1248 max_estimate_us: u64::MAX,
1249 stored_observations_num_included_checkpoints: 10,
1250 stored_observations_limit: u64::MAX,
1251 stake_weighted_median_threshold: 0,
1252 default_none_duration_for_new_keys: true,
1253 observations_chunk_size: Some(18),
1254 },
1255 ),
1256 );
1257 config
1258 });
1259
1260 let mock_consensus_client = MockConsensusClient::new();
1261 let authority = TestAuthorityBuilder::new().build().await;
1262 let epoch_store = authority.epoch_store_for_testing();
1263 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1264 Arc::new(mock_consensus_client),
1265 CheckpointStore::new_for_tests(),
1266 authority.name,
1267 100_000,
1268 100_000,
1269 ConsensusAdapterMetrics::new_test(),
1270 Arc::new(tokio::sync::Notify::new()),
1271 ));
1272 let mut observer = ExecutionTimeObserver::new_for_testing(
1273 epoch_store.clone(),
1274 Box::new(consensus_adapter.clone()),
1275 Duration::from_millis(500), false, );
1278
1279 let package = ObjectID::random();
1281 let module = "test_module".to_string();
1282 let function = "test_function".to_string();
1283 let shared_object_id = ObjectID::random();
1284 let ptb = ProgrammableTransaction {
1285 inputs: vec![CallArg::Object(ObjectArg::SharedObject {
1286 id: shared_object_id,
1287 initial_shared_version: SequenceNumber::new(),
1288 mutability: SharedObjectMutability::Mutable,
1289 })],
1290 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1291 package,
1292 module: module.clone(),
1293 function: function.clone(),
1294 type_arguments: vec![],
1295 arguments: vec![],
1296 }))],
1297 };
1298 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1299 package,
1300 module: module.clone(),
1301 function: function.clone(),
1302 type_arguments: vec![],
1303 };
1304
1305 tokio::time::pause();
1306
1307 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1309 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1310 assert!(
1311 observer
1312 .local_observations
1313 .get(&key)
1314 .unwrap()
1315 .last_shared
1316 .is_none()
1317 );
1318
1319 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1321 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1322 assert_eq!(
1323 observer
1324 .local_observations
1325 .get(&key)
1326 .unwrap()
1327 .last_shared
1328 .unwrap()
1329 .0,
1330 Duration::from_secs(2)
1331 );
1332
1333 tokio::time::advance(Duration::from_secs(5)).await;
1335 let timings = vec![ExecutionTiming::Success(Duration::from_secs(3))];
1336 observer.record_local_observations(&ptb, &timings, Duration::from_secs(5), 1);
1337 assert_eq!(
1338 observer
1339 .local_observations
1340 .get(&key)
1341 .unwrap()
1342 .last_shared
1343 .unwrap()
1344 .0,
1345 Duration::from_secs(3)
1346 );
1347
1348 tokio::time::advance(Duration::from_millis(150)).await;
1351 let timings = vec![ExecutionTiming::Success(Duration::from_millis(100))];
1352 observer.record_local_observations(&ptb, &timings, Duration::from_millis(500), 1);
1353 assert_eq!(
1354 observer
1355 .local_observations
1356 .get(&key)
1357 .unwrap()
1358 .last_shared
1359 .unwrap()
1360 .0,
1361 Duration::from_secs(3) );
1363
1364 tokio::time::advance(Duration::from_secs(60)).await;
1366 let timings = vec![ExecutionTiming::Success(Duration::from_secs(11))];
1367 observer.record_local_observations(&ptb, &timings, Duration::from_secs(11), 1);
1368 assert_eq!(
1369 observer
1370 .local_observations
1371 .get(&key)
1372 .unwrap()
1373 .last_shared
1374 .unwrap()
1375 .0,
1376 Duration::from_secs(3) );
1378 }
1379
1380 #[tokio::test]
1381 async fn test_record_local_observations_with_indebted_objects() {
1382 telemetry_subscribers::init_for_testing();
1383
1384 let _guard = ProtocolConfig::apply_overrides_for_testing(|_, mut config| {
1385 config.set_per_object_congestion_control_mode_for_testing(
1386 PerObjectCongestionControlMode::ExecutionTimeEstimate(
1387 ExecutionTimeEstimateParams {
1388 target_utilization: 100,
1389 allowed_txn_cost_overage_burst_limit_us: 0,
1390 randomness_scalar: 0,
1391 max_estimate_us: u64::MAX,
1392 stored_observations_num_included_checkpoints: 10,
1393 stored_observations_limit: u64::MAX,
1394 stake_weighted_median_threshold: 0,
1395 default_none_duration_for_new_keys: true,
1396 observations_chunk_size: Some(18),
1397 },
1398 ),
1399 );
1400 config
1401 });
1402
1403 let mock_consensus_client = MockConsensusClient::new();
1404 let authority = TestAuthorityBuilder::new().build().await;
1405 let epoch_store = authority.epoch_store_for_testing();
1406 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1407 Arc::new(mock_consensus_client),
1408 CheckpointStore::new_for_tests(),
1409 authority.name,
1410 100_000,
1411 100_000,
1412 ConsensusAdapterMetrics::new_test(),
1413 Arc::new(tokio::sync::Notify::new()),
1414 ));
1415 let mut observer = ExecutionTimeObserver::new_for_testing(
1416 epoch_store.clone(),
1417 Box::new(consensus_adapter.clone()),
1418 Duration::from_millis(500), false, );
1421
1422 let package = ObjectID::random();
1424 let module = "test_module".to_string();
1425 let function = "test_function".to_string();
1426 let shared_object_id = ObjectID::random();
1427 let ptb = ProgrammableTransaction {
1428 inputs: vec![CallArg::Object(ObjectArg::SharedObject {
1429 id: shared_object_id,
1430 initial_shared_version: SequenceNumber::new(),
1431 mutability: SharedObjectMutability::Mutable,
1432 })],
1433 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1434 package,
1435 module: module.clone(),
1436 function: function.clone(),
1437 type_arguments: vec![],
1438 arguments: vec![],
1439 }))],
1440 };
1441 let key = ExecutionTimeObservationKey::MoveEntryPoint {
1442 package,
1443 module: module.clone(),
1444 function: function.clone(),
1445 type_arguments: vec![],
1446 };
1447
1448 tokio::time::pause();
1449
1450 let timings = vec![ExecutionTiming::Success(Duration::from_secs(1))];
1452 observer.record_local_observations(&ptb, &timings, Duration::from_secs(1), 1);
1453 assert!(
1454 observer
1455 .local_observations
1456 .get(&key)
1457 .unwrap()
1458 .last_shared
1459 .is_none()
1460 );
1461
1462 let timings = vec![ExecutionTiming::Success(Duration::from_secs(2))];
1464 observer.record_local_observations(&ptb, &timings, Duration::from_secs(2), 1);
1465 assert_eq!(
1466 observer
1467 .local_observations
1468 .get(&key)
1469 .unwrap()
1470 .last_shared
1471 .unwrap()
1472 .0,
1473 Duration::from_millis(1500) );
1475
1476 observer.update_indebted_objects(vec![shared_object_id]);
1478 observer
1479 .config
1480 .observation_sharing_object_utilization_threshold = Some(Duration::from_secs(1000));
1481
1482 tokio::time::advance(Duration::from_secs(60)).await;
1485 let timings = vec![ExecutionTiming::Success(Duration::from_millis(300))];
1486 observer.record_local_observations(&ptb, &timings, Duration::from_millis(300), 1);
1487
1488 assert_eq!(
1491 observer
1492 .local_observations
1493 .get(&key)
1494 .unwrap()
1495 .last_shared
1496 .unwrap()
1497 .0,
1498 Duration::from_millis(1100)
1499 );
1500 }
1501
1502 #[tokio::test]
1503 async fn test_stake_weighted_median() {
1504 telemetry_subscribers::init_for_testing();
1505
1506 let (committee, _) =
1507 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1508
1509 let params = ExecutionTimeEstimateParams {
1510 stake_weighted_median_threshold: 0,
1511 ..Default::default()
1512 };
1513
1514 let mut tracker = ConsensusObservations {
1515 observations: vec![
1516 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), ],
1521 stake_weighted_median: None,
1522 };
1523 tracker.update_stake_weighted_median(&committee, ¶ms);
1524 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1531
1532 let mut tracker = ConsensusObservations {
1534 observations: vec![
1535 (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), ],
1540 stake_weighted_median: None,
1541 };
1542 tracker.update_stake_weighted_median(&committee, ¶ms);
1543 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(2)));
1550
1551 let mut tracker = ConsensusObservations {
1553 observations: vec![
1554 (0, Some(Duration::from_secs(1))), (0, None), (0, Some(Duration::from_secs(3))), (0, Some(Duration::from_secs(4))), ],
1559 stake_weighted_median: None,
1560 };
1561 tracker.update_stake_weighted_median(&committee, ¶ms);
1562 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(4)));
1568
1569 let mut tracker = ConsensusObservations {
1571 observations: vec![
1572 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, None), (0, None), ],
1577 stake_weighted_median: None,
1578 };
1579 tracker.update_stake_weighted_median(&committee, ¶ms);
1580 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(2)));
1585
1586 let mut tracker = ConsensusObservations {
1588 observations: vec![
1589 (0, None), (0, None), (0, Some(Duration::from_secs(3))), (0, None), ],
1594 stake_weighted_median: None,
1595 };
1596 tracker.update_stake_weighted_median(&committee, ¶ms);
1597 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1599
1600 let mut tracker = ConsensusObservations {
1602 observations: vec![
1603 (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), (0, Some(Duration::from_secs(5))), ],
1608 stake_weighted_median: None,
1609 };
1610 tracker.update_stake_weighted_median(&committee, ¶ms);
1611 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(5)));
1612 }
1613
1614 #[tokio::test]
1615 async fn test_stake_weighted_median_threshold() {
1616 telemetry_subscribers::init_for_testing();
1617
1618 let (committee, _) =
1619 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1620
1621 let params = ExecutionTimeEstimateParams {
1623 stake_weighted_median_threshold: 5000,
1624 ..Default::default()
1625 };
1626
1627 let mut tracker = ConsensusObservations {
1629 observations: vec![
1630 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, None), (0, None), ],
1635 stake_weighted_median: None,
1636 };
1637 tracker.update_stake_weighted_median(&committee, ¶ms);
1638 assert_eq!(tracker.stake_weighted_median, None);
1640
1641 let mut tracker = ConsensusObservations {
1643 observations: vec![
1644 (0, Some(Duration::from_secs(1))), (0, Some(Duration::from_secs(2))), (0, Some(Duration::from_secs(3))), (0, None), ],
1649 stake_weighted_median: None,
1650 };
1651 tracker.update_stake_weighted_median(&committee, ¶ms);
1652 assert_eq!(tracker.stake_weighted_median, Some(Duration::from_secs(3)));
1654 }
1655
1656 #[tokio::test]
1657 async fn test_execution_time_estimator() {
1658 telemetry_subscribers::init_for_testing();
1659
1660 let (committee, _) =
1661 Committee::new_simple_test_committee_with_normalized_voting_power(vec![10, 20, 30, 40]);
1662 let mut estimator = ExecutionTimeEstimator::new(
1663 Arc::new(committee),
1664 ExecutionTimeEstimateParams {
1665 target_utilization: 50,
1666 max_estimate_us: 1_500_000,
1667
1668 allowed_txn_cost_overage_burst_limit_us: 0,
1670 randomness_scalar: 0,
1671 stored_observations_num_included_checkpoints: 10,
1672 stored_observations_limit: u64::MAX,
1673 stake_weighted_median_threshold: 0,
1674 default_none_duration_for_new_keys: true,
1675 observations_chunk_size: Some(18),
1676 },
1677 std::iter::empty(),
1678 );
1679 let package = ObjectID::random();
1681 let module = "test_module".to_string();
1682 let function = "test_function".to_string();
1683 let move_key = ExecutionTimeObservationKey::MoveEntryPoint {
1684 package,
1685 module: module.clone(),
1686 function: function.clone(),
1687 type_arguments: vec![],
1688 };
1689 let transfer_key = ExecutionTimeObservationKey::TransferObjects;
1690
1691 estimator.process_observation_from_consensus(
1694 0,
1695 Some(1),
1696 move_key.clone(),
1697 Duration::from_millis(1000),
1698 false,
1699 );
1700 estimator.process_observation_from_consensus(
1701 1,
1702 Some(1),
1703 move_key.clone(),
1704 Duration::from_millis(1000),
1705 false,
1706 );
1707 estimator.process_observation_from_consensus(
1708 2,
1709 Some(1),
1710 move_key.clone(),
1711 Duration::from_millis(1000),
1712 false,
1713 );
1714
1715 estimator.process_observation_from_consensus(
1716 0,
1717 Some(1),
1718 transfer_key.clone(),
1719 Duration::from_millis(500),
1720 false,
1721 );
1722 estimator.process_observation_from_consensus(
1723 1,
1724 Some(1),
1725 transfer_key.clone(),
1726 Duration::from_millis(500),
1727 false,
1728 );
1729 estimator.process_observation_from_consensus(
1730 2,
1731 Some(1),
1732 transfer_key.clone(),
1733 Duration::from_millis(500),
1734 false,
1735 );
1736
1737 estimator.process_observation_from_consensus(
1739 0,
1740 Some(2),
1741 move_key.clone(),
1742 Duration::from_millis(100),
1743 false,
1744 );
1745 estimator.process_observation_from_consensus(
1746 1,
1747 Some(2),
1748 move_key.clone(),
1749 Duration::from_millis(200),
1750 false,
1751 );
1752 estimator.process_observation_from_consensus(
1753 2,
1754 Some(2),
1755 move_key.clone(),
1756 Duration::from_millis(300),
1757 false,
1758 );
1759
1760 estimator.process_observation_from_consensus(
1761 0,
1762 Some(2),
1763 transfer_key.clone(),
1764 Duration::from_millis(50),
1765 false,
1766 );
1767 estimator.process_observation_from_consensus(
1768 1,
1769 Some(2),
1770 transfer_key.clone(),
1771 Duration::from_millis(60),
1772 false,
1773 );
1774 estimator.process_observation_from_consensus(
1775 2,
1776 Some(2),
1777 transfer_key.clone(),
1778 Duration::from_millis(70),
1779 false,
1780 );
1781
1782 estimator.process_observation_from_consensus(
1784 0,
1785 Some(1),
1786 move_key.clone(),
1787 Duration::from_millis(1000),
1788 false,
1789 );
1790 estimator.process_observation_from_consensus(
1791 1,
1792 Some(1),
1793 transfer_key.clone(),
1794 Duration::from_millis(500),
1795 false,
1796 );
1797 estimator.process_observation_from_consensus(
1798 2,
1799 Some(1),
1800 move_key.clone(),
1801 Duration::from_millis(1000),
1802 false,
1803 );
1804
1805 let single_move_tx = TransactionData::new_programmable(
1807 SuiAddress::ZERO,
1808 vec![],
1809 ProgrammableTransaction {
1810 inputs: vec![],
1811 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1812 package,
1813 module: module.clone(),
1814 function: function.clone(),
1815 type_arguments: vec![],
1816 arguments: vec![],
1817 }))],
1818 },
1819 100,
1820 100,
1821 );
1822
1823 assert_eq!(
1825 estimator.get_estimate(&single_move_tx),
1826 Duration::from_millis(300)
1827 );
1828
1829 let multi_command_tx = TransactionData::new_programmable(
1831 SuiAddress::ZERO,
1832 vec![],
1833 ProgrammableTransaction {
1834 inputs: vec![],
1835 commands: vec![
1836 Command::MoveCall(Box::new(ProgrammableMoveCall {
1837 package,
1838 module: module.clone(),
1839 function: function.clone(),
1840 type_arguments: vec![],
1841 arguments: vec![],
1842 })),
1843 Command::TransferObjects(
1844 vec![Argument::Input(1), Argument::Input(2)],
1845 Argument::Input(0),
1846 ),
1847 ],
1848 },
1849 100,
1850 100,
1851 );
1852
1853 assert_eq!(
1856 estimator.get_estimate(&multi_command_tx),
1857 Duration::from_millis(510)
1858 );
1859 }
1860
1861 #[derive(Debug, Clone, Serialize, Deserialize)]
1862 struct ExecutionTimeObserverSnapshot {
1863 protocol_version: u64,
1864 consensus_observations: Vec<(ExecutionTimeObservationKey, ConsensusObservations)>,
1865 transaction_estimates: Vec<(String, Duration)>, }
1867
1868 fn generate_test_inputs(
1869 seed: u64,
1870 num_validators: usize,
1871 generation_override: Option<u64>,
1872 ) -> Vec<(
1873 AuthorityIndex,
1874 Option<u64>,
1875 ExecutionTimeObservationKey,
1876 Duration,
1877 )> {
1878 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1879
1880 let observation_keys = [
1881 ExecutionTimeObservationKey::MoveEntryPoint {
1882 package: ObjectID::from_hex_literal("0x1").unwrap(),
1883 module: "coin".to_string(),
1884 function: "transfer".to_string(),
1885 type_arguments: vec![],
1886 },
1887 ExecutionTimeObservationKey::MoveEntryPoint {
1888 package: ObjectID::from_hex_literal("0x2").unwrap(),
1889 module: "nft".to_string(),
1890 function: "mint".to_string(),
1891 type_arguments: vec![],
1892 },
1893 ExecutionTimeObservationKey::TransferObjects,
1894 ExecutionTimeObservationKey::SplitCoins,
1895 ExecutionTimeObservationKey::MergeCoins,
1896 ExecutionTimeObservationKey::MakeMoveVec,
1897 ExecutionTimeObservationKey::Upgrade,
1898 ];
1899
1900 let mut inputs = Vec::new();
1901 let target_samples = 25;
1902
1903 for _ in 0..target_samples {
1904 let key = observation_keys[rng.gen_range(0..observation_keys.len())].clone();
1905 let authority_index =
1906 AuthorityIndex::try_from(rng.gen_range(0..num_validators)).unwrap();
1907
1908 let generation = generation_override.unwrap_or_else(|| rng.gen_range(1..=10));
1910
1911 let base_duration = if rng.gen_ratio(1, 20) {
1914 0
1916 } else {
1917 match &key {
1918 ExecutionTimeObservationKey::MoveEntryPoint { .. } => rng.gen_range(50..=500),
1919 ExecutionTimeObservationKey::TransferObjects => rng.gen_range(10..=100),
1920 ExecutionTimeObservationKey::SplitCoins => rng.gen_range(20..=80),
1921 ExecutionTimeObservationKey::MergeCoins => rng.gen_range(15..=70),
1922 ExecutionTimeObservationKey::MakeMoveVec => rng.gen_range(5..=30),
1923 ExecutionTimeObservationKey::Upgrade => rng.gen_range(100..=1000),
1924 ExecutionTimeObservationKey::Publish => rng.gen_range(200..=2000),
1925 }
1926 };
1927
1928 let duration = Duration::from_millis(base_duration);
1929
1930 inputs.push((authority_index, Some(generation), key, duration));
1931 }
1932
1933 inputs
1934 }
1935
1936 fn generate_test_transactions(seed: u64) -> Vec<(String, TransactionData)> {
1937 let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1938 let mut transactions = Vec::new();
1939
1940 let package3 = ObjectID::from_hex_literal("0x3").unwrap();
1941 transactions.push((
1942 "coin_transfer_call".to_string(),
1943 TransactionData::new_programmable(
1944 SuiAddress::ZERO,
1945 vec![],
1946 ProgrammableTransaction {
1947 inputs: vec![],
1948 commands: vec![Command::MoveCall(Box::new(ProgrammableMoveCall {
1949 package: ObjectID::from_hex_literal("0x1").unwrap(),
1950 module: "coin".to_string(),
1951 function: "transfer".to_string(),
1952 type_arguments: vec![],
1953 arguments: vec![],
1954 }))],
1955 },
1956 rng.gen_range(100..1000),
1957 rng.gen_range(100..1000),
1958 ),
1959 ));
1960
1961 transactions.push((
1962 "mixed_move_calls".to_string(),
1963 TransactionData::new_programmable(
1964 SuiAddress::ZERO,
1965 vec![],
1966 ProgrammableTransaction {
1967 inputs: vec![],
1968 commands: vec![
1969 Command::MoveCall(Box::new(ProgrammableMoveCall {
1970 package: ObjectID::from_hex_literal("0x1").unwrap(),
1971 module: "coin".to_string(),
1972 function: "transfer".to_string(),
1973 type_arguments: vec![],
1974 arguments: vec![],
1975 })),
1976 Command::MoveCall(Box::new(ProgrammableMoveCall {
1977 package: ObjectID::from_hex_literal("0x2").unwrap(),
1978 module: "nft".to_string(),
1979 function: "mint".to_string(),
1980 type_arguments: vec![],
1981 arguments: vec![],
1982 })),
1983 ],
1984 },
1985 rng.gen_range(100..1000),
1986 rng.gen_range(100..1000),
1987 ),
1988 ));
1989
1990 transactions.push((
1991 "native_commands_with_observations".to_string(),
1992 TransactionData::new_programmable(
1993 SuiAddress::ZERO,
1994 vec![],
1995 ProgrammableTransaction {
1996 inputs: vec![],
1997 commands: vec![
1998 Command::TransferObjects(vec![Argument::Input(0)], Argument::Input(1)),
1999 Command::SplitCoins(Argument::Input(2), vec![Argument::Input(3)]),
2000 Command::MergeCoins(Argument::Input(4), vec![Argument::Input(5)]),
2001 Command::MakeMoveVec(None, vec![Argument::Input(6)]),
2002 ],
2003 },
2004 rng.gen_range(100..1000),
2005 rng.gen_range(100..1000),
2006 ),
2007 ));
2008
2009 let num_objects = rng.gen_range(1..=5);
2010 transactions.push((
2011 format!("transfer_objects_{}_items", num_objects),
2012 TransactionData::new_programmable(
2013 SuiAddress::ZERO,
2014 vec![],
2015 ProgrammableTransaction {
2016 inputs: vec![],
2017 commands: vec![Command::TransferObjects(
2018 (0..num_objects).map(Argument::Input).collect(),
2019 Argument::Input(num_objects),
2020 )],
2021 },
2022 rng.gen_range(100..1000),
2023 rng.gen_range(100..1000),
2024 ),
2025 ));
2026
2027 let num_amounts = rng.gen_range(1..=4);
2028 transactions.push((
2029 format!("split_coins_{}_amounts", num_amounts),
2030 TransactionData::new_programmable(
2031 SuiAddress::ZERO,
2032 vec![],
2033 ProgrammableTransaction {
2034 inputs: vec![],
2035 commands: vec![Command::SplitCoins(
2036 Argument::Input(0),
2037 (1..=num_amounts).map(Argument::Input).collect(),
2038 )],
2039 },
2040 rng.gen_range(100..1000),
2041 rng.gen_range(100..1000),
2042 ),
2043 ));
2044
2045 let num_sources = rng.gen_range(1..=3);
2046 transactions.push((
2047 format!("merge_coins_{}_sources", num_sources),
2048 TransactionData::new_programmable(
2049 SuiAddress::ZERO,
2050 vec![],
2051 ProgrammableTransaction {
2052 inputs: vec![],
2053 commands: vec![Command::MergeCoins(
2054 Argument::Input(0),
2055 (1..=num_sources).map(Argument::Input).collect(),
2056 )],
2057 },
2058 rng.gen_range(100..1000),
2059 rng.gen_range(100..1000),
2060 ),
2061 ));
2062
2063 let num_elements = rng.gen_range(0..=6);
2064 transactions.push((
2065 format!("make_move_vec_{}_elements", num_elements),
2066 TransactionData::new_programmable(
2067 SuiAddress::ZERO,
2068 vec![],
2069 ProgrammableTransaction {
2070 inputs: vec![],
2071 commands: vec![Command::MakeMoveVec(
2072 None,
2073 (0..num_elements).map(Argument::Input).collect(),
2074 )],
2075 },
2076 rng.gen_range(100..1000),
2077 rng.gen_range(100..1000),
2078 ),
2079 ));
2080
2081 transactions.push((
2082 "mixed_commands".to_string(),
2083 TransactionData::new_programmable(
2084 SuiAddress::ZERO,
2085 vec![],
2086 ProgrammableTransaction {
2087 inputs: vec![],
2088 commands: vec![
2089 Command::MoveCall(Box::new(ProgrammableMoveCall {
2090 package: package3,
2091 module: "game".to_string(),
2092 function: "play".to_string(),
2093 type_arguments: vec![],
2094 arguments: vec![],
2095 })),
2096 Command::TransferObjects(
2097 vec![Argument::Input(1), Argument::Input(2)],
2098 Argument::Input(0),
2099 ),
2100 Command::SplitCoins(Argument::Input(3), vec![Argument::Input(4)]),
2101 ],
2102 },
2103 rng.gen_range(100..1000),
2104 rng.gen_range(100..1000),
2105 ),
2106 ));
2107
2108 transactions.push((
2109 "upgrade_package".to_string(),
2110 TransactionData::new_programmable(
2111 SuiAddress::ZERO,
2112 vec![],
2113 ProgrammableTransaction {
2114 inputs: vec![],
2115 commands: vec![Command::Upgrade(
2116 vec![],
2117 vec![],
2118 package3,
2119 Argument::Input(0),
2120 )],
2121 },
2122 rng.gen_range(100..1000),
2123 rng.gen_range(100..1000),
2124 ),
2125 ));
2126
2127 transactions
2128 }
2129
2130 #[test]
2142 fn snapshot_tests() {
2143 println!("\n============================================================================");
2144 println!("! !");
2145 println!("! IMPORTANT: never update snapshots from this test. only add new versions! !");
2146 println!("! !");
2147 println!("============================================================================\n");
2148
2149 let max_version = ProtocolVersion::MAX.as_u64();
2150
2151 let test_versions: Vec<u64> = (max_version.saturating_sub(9)..=max_version).collect();
2152
2153 for version in test_versions {
2154 let protocol_version = ProtocolVersion::new(version);
2155 let protocol_config = ProtocolConfig::get_for_version(protocol_version, Chain::Unknown);
2156 let (committee, _) = Committee::new_simple_test_committee_of_size(4);
2157 let committee = Arc::new(committee);
2158
2159 let initial_generation =
2160 if let PerObjectCongestionControlMode::ExecutionTimeEstimate(params) =
2161 protocol_config.per_object_congestion_control_mode()
2162 {
2163 if params.default_none_duration_for_new_keys {
2164 None
2165 } else {
2166 Some(0)
2167 }
2168 } else {
2169 Some(0) };
2171
2172 let initial_observations =
2173 generate_test_inputs(0, committee.num_members(), initial_generation);
2174 let mut estimator = ExecutionTimeEstimator::new(
2175 committee.clone(),
2176 ExecutionTimeEstimateParams {
2177 max_estimate_us: u64::MAX, ..ExecutionTimeEstimateParams::default()
2179 },
2180 initial_observations.into_iter(),
2181 );
2182
2183 let test_inputs = generate_test_inputs(version, committee.num_members(), None);
2184
2185 for (source, generation, observation_key, duration) in test_inputs {
2186 estimator.process_observation_from_consensus(
2187 source,
2188 generation,
2189 observation_key,
2190 duration,
2191 false,
2192 );
2193 }
2194
2195 let mut final_observations = estimator.get_observations();
2196 final_observations.sort_by_key(|a| a.0.to_string());
2197
2198 let test_transactions = generate_test_transactions(version);
2199 let mut transaction_estimates = Vec::new();
2200 for (description, tx_data) in test_transactions {
2201 let estimate = estimator.get_estimate(&tx_data);
2202 transaction_estimates.push((description, estimate));
2203 }
2204
2205 let snapshot_data = ExecutionTimeObserverSnapshot {
2206 protocol_version: version,
2207 consensus_observations: final_observations.clone(),
2208 transaction_estimates,
2209 };
2210 insta::assert_yaml_snapshot!(
2211 format!("execution_time_observer_v{}", version),
2212 snapshot_data
2213 );
2214 }
2215 }
2216}