sui_core/authority/
shared_object_congestion_tracker.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use super::execution_time_estimator::ExecutionTimeEstimator;
5use crate::authority::transaction_deferral::DeferralKey;
6use crate::consensus_handler::{ConsensusCommitInfo, IndirectStateObserver};
7use mysten_common::fatal;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use sui_protocol_config::{PerObjectCongestionControlMode, ProtocolConfig};
11use sui_types::base_types::{ObjectID, TransactionDigest};
12use sui_types::executable_transaction::VerifiedExecutableTransaction;
13use sui_types::messages_consensus::Round;
14use sui_types::transaction::{Argument, SharedInputObject, TransactionDataAPI};
15use tracing::{debug, trace};
16
17#[derive(PartialEq, Eq, Clone, Debug)]
18struct Params {
19    mode: PerObjectCongestionControlMode,
20    for_randomness: bool,
21    max_accumulated_txn_cost_per_object_in_commit: u64,
22    gas_budget_based_txn_cost_cap_factor: Option<u64>,
23    gas_budget_based_txn_cost_absolute_cap: Option<u64>,
24    max_txn_cost_overage_per_object_in_commit: u64,
25    allowed_txn_cost_overage_burst_per_object_in_commit: u64,
26}
27
28impl Params {
29    // Get the target budget per commit. Over the long term, the scheduler will try to
30    // schedule no more than this much work per object per commit on average.
31    pub fn commit_budget(&self, commit_info: &ConsensusCommitInfo) -> u64 {
32        match self.mode {
33            PerObjectCongestionControlMode::ExecutionTimeEstimate(params) => {
34                let estimated_commit_period = commit_info.estimated_commit_period();
35                let commit_period_micros = estimated_commit_period.as_micros() as u64;
36                let mut budget =
37                    commit_period_micros.saturating_mul(params.target_utilization) / 100;
38                if self.for_randomness {
39                    budget = budget.saturating_mul(params.randomness_scalar) / 100;
40                }
41                budget
42            }
43            _ => self.max_accumulated_txn_cost_per_object_in_commit,
44        }
45    }
46
47    // The amount scheduled in a commit can "burst" up to this much over the target budget.
48    // The per-object debt limit will enforce the average limit over time.
49    pub fn max_burst(&self) -> u64 {
50        match self.mode {
51            PerObjectCongestionControlMode::ExecutionTimeEstimate(params) => {
52                let mut burst = params.allowed_txn_cost_overage_burst_limit_us;
53                if self.for_randomness {
54                    burst = burst.saturating_mul(params.randomness_scalar) / 100;
55                }
56                burst
57            }
58            _ => self.allowed_txn_cost_overage_burst_per_object_in_commit,
59        }
60    }
61
62    // The absolute maximum to schedule per commit, even for a single transaction.
63    // This should normally be very high, otherwise some transactions could be
64    // unschedulable regardless of congestion.
65    pub fn max_overage(&self) -> u64 {
66        match self.mode {
67            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => u64::MAX,
68            _ => self.max_txn_cost_overage_per_object_in_commit,
69        }
70    }
71
72    pub fn gas_budget_based_txn_cost_cap_factor(&self) -> u64 {
73        match self.mode {
74            PerObjectCongestionControlMode::TotalGasBudgetWithCap => self
75                .gas_budget_based_txn_cost_cap_factor
76                .expect("cap factor must be set if TotalGasBudgetWithCap mode is used."),
77            _ => fatal!(
78                "gas_budget_based_txn_cost_cap_factor is only used in TotalGasBudgetWithCap mode."
79            ),
80        }
81    }
82
83    pub fn gas_budget_based_txn_cost_absolute_cap(&self) -> Option<u64> {
84        match self.mode {
85            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
86                self.gas_budget_based_txn_cost_absolute_cap
87            }
88            _ => fatal!(
89                "gas_budget_based_txn_cost_absolute_cap is only used in TotalGasBudgetWithCap mode."
90            ),
91        }
92    }
93}
94
95// SharedObjectCongestionTracker stores the accumulated cost of executing transactions on an object, for
96// all transactions in a consensus commit.
97//
98// Cost is an indication of transaction execution latency. When transactions are scheduled by
99// the consensus handler, each scheduled transaction adds cost (execution latency) to all the objects it
100// reads or writes.
101//
102// The goal of this data structure is to capture the critical path of transaction execution latency on each
103// objects.
104//
105// The mode field determines how the cost is calculated. The cost can be calculated based on the total gas
106// budget, or total number of transaction count.
107#[derive(PartialEq, Eq, Clone, Debug)]
108pub struct SharedObjectCongestionTracker {
109    object_execution_cost: HashMap<ObjectID, u64>,
110    params: Params,
111}
112
113impl SharedObjectCongestionTracker {
114    pub fn new(
115        initial_object_debts: impl IntoIterator<Item = (ObjectID, u64)>,
116        mode: PerObjectCongestionControlMode,
117        for_randomness: bool,
118        max_accumulated_txn_cost_per_object_in_commit: Option<u64>,
119        gas_budget_based_txn_cost_cap_factor: Option<u64>,
120        gas_budget_based_txn_cost_absolute_cap_commit_count: Option<u64>,
121        max_txn_cost_overage_per_object_in_commit: u64,
122        allowed_txn_cost_overage_burst_per_object_in_commit: u64,
123    ) -> Self {
124        assert!(
125            allowed_txn_cost_overage_burst_per_object_in_commit
126                <= max_txn_cost_overage_per_object_in_commit,
127            "burst limit must be <= absolute limit; allowed_txn_cost_overage_burst_per_object_in_commit = {allowed_txn_cost_overage_burst_per_object_in_commit}, max_txn_cost_overage_per_object_in_commit = {max_txn_cost_overage_per_object_in_commit}"
128        );
129
130        let object_execution_cost: HashMap<ObjectID, u64> =
131            initial_object_debts.into_iter().collect();
132        let max_accumulated_txn_cost_per_object_in_commit =
133            if mode == PerObjectCongestionControlMode::None {
134                0
135            } else {
136                max_accumulated_txn_cost_per_object_in_commit.expect(
137                    "max_accumulated_txn_cost_per_object_in_commit must be set if mode is not None",
138                )
139            };
140        let gas_budget_based_txn_cost_absolute_cap =
141            gas_budget_based_txn_cost_absolute_cap_commit_count
142                .map(|m| m * max_accumulated_txn_cost_per_object_in_commit);
143        trace!(
144            "created SharedObjectCongestionTracker with
145             {} initial object debts,
146             mode: {mode:?},
147             max_accumulated_txn_cost_per_object_in_commit: {max_accumulated_txn_cost_per_object_in_commit:?},
148             gas_budget_based_txn_cost_cap_factor: {gas_budget_based_txn_cost_cap_factor:?},
149             gas_budget_based_txn_cost_absolute_cap: {gas_budget_based_txn_cost_absolute_cap:?},
150             max_txn_cost_overage_per_object_in_commit: {max_txn_cost_overage_per_object_in_commit:?}",
151            object_execution_cost.len(),
152        );
153        Self {
154            object_execution_cost,
155            params: Params {
156                mode,
157                for_randomness,
158                max_accumulated_txn_cost_per_object_in_commit,
159                gas_budget_based_txn_cost_cap_factor,
160                gas_budget_based_txn_cost_absolute_cap,
161                max_txn_cost_overage_per_object_in_commit,
162                allowed_txn_cost_overage_burst_per_object_in_commit,
163            },
164        }
165    }
166
167    pub fn from_protocol_config(
168        initial_object_debts: impl IntoIterator<Item = (ObjectID, u64)>,
169        protocol_config: &ProtocolConfig,
170        for_randomness: bool,
171    ) -> Self {
172        let max_accumulated_txn_cost_per_object_in_commit =
173            protocol_config.max_accumulated_txn_cost_per_object_in_mysticeti_commit_as_option();
174        Self::new(
175            initial_object_debts,
176            protocol_config.per_object_congestion_control_mode(),
177            for_randomness,
178            if for_randomness {
179                protocol_config
180                    .max_accumulated_randomness_txn_cost_per_object_in_mysticeti_commit_as_option()
181                    .or(max_accumulated_txn_cost_per_object_in_commit)
182            } else {
183                max_accumulated_txn_cost_per_object_in_commit
184            },
185            protocol_config.gas_budget_based_txn_cost_cap_factor_as_option(),
186            protocol_config.gas_budget_based_txn_cost_absolute_cap_commit_count_as_option(),
187            protocol_config
188                .max_txn_cost_overage_per_object_in_commit_as_option()
189                .unwrap_or(0),
190            protocol_config
191                .allowed_txn_cost_overage_burst_per_object_in_commit_as_option()
192                .unwrap_or(0),
193        )
194    }
195
196    // Given a list of shared input objects, returns the starting cost of a transaction that operates on
197    // these objects.
198    //
199    // Starting cost is a proxy for the starting time of the transaction. It is determined by all the input
200    // shared objects' last write.
201    pub fn compute_tx_start_at_cost(&self, shared_input_objects: &[SharedInputObject]) -> u64 {
202        shared_input_objects
203            .iter()
204            .map(|obj| *self.object_execution_cost.get(&obj.id).unwrap_or(&0))
205            .max()
206            .expect("There must be at least one object in shared_input_objects.")
207    }
208
209    pub fn get_tx_cost(
210        &self,
211        execution_time_estimator: Option<&ExecutionTimeEstimator>,
212        cert: &VerifiedExecutableTransaction,
213        indirect_state_observer: &mut IndirectStateObserver,
214    ) -> Option<u64> {
215        let tx_cost = match &self.params.mode {
216            PerObjectCongestionControlMode::None => None,
217            PerObjectCongestionControlMode::TotalGasBudget => Some(cert.gas_budget()),
218            PerObjectCongestionControlMode::TotalTxCount => Some(1),
219            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
220                Some(std::cmp::min(cert.gas_budget(), self.get_tx_cost_cap(cert)))
221            }
222            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
223                let estimate_us = execution_time_estimator
224                    .expect("`execution_time_estimator` must be set for PerObjectCongestionControlMode::ExecutionTimeEstimate")
225                    .get_estimate(cert.transaction_data())
226                    .as_micros()
227                    .try_into()
228                    .unwrap_or(u64::MAX);
229                if estimate_us >= 15_000 {
230                    let digest = cert.digest();
231                    debug!(
232                        ?digest,
233                        "expensive tx cost estimate detected: {estimate_us}us"
234                    );
235                }
236
237                Some(estimate_us)
238            }
239        };
240        indirect_state_observer.observe_indirect_state(&tx_cost);
241        tx_cost
242    }
243
244    // Given a transaction, returns the deferral key and the congested objects if the transaction should be deferred.
245    pub fn should_defer_due_to_object_congestion(
246        &self,
247        tx_cost: Option<u64>,
248        cert: &VerifiedExecutableTransaction,
249        previously_deferred_tx_digests: &HashMap<TransactionDigest, DeferralKey>,
250        commit_info: &ConsensusCommitInfo,
251    ) -> Option<(DeferralKey, Vec<ObjectID>)> {
252        let commit_round = commit_info.round;
253
254        let tx_cost = tx_cost?;
255
256        let shared_input_objects: Vec<_> = cert.shared_input_objects().collect();
257        if shared_input_objects.is_empty() {
258            // No shared object used by this transaction. No need to defer.
259            return None;
260        }
261        let start_cost = self.compute_tx_start_at_cost(&shared_input_objects);
262        let end_cost = start_cost.saturating_add(tx_cost);
263
264        let budget = self.params.commit_budget(commit_info);
265
266        // Allow tx if it's within configured limits.
267        let burst_limit = budget.saturating_add(self.params.max_burst());
268        let absolute_limit = budget.saturating_add(self.params.max_overage());
269
270        if start_cost <= burst_limit && end_cost <= absolute_limit {
271            return None;
272        }
273
274        // Finds out the congested objects.
275        //
276        // Note that the congested objects here may be caused by transaction dependency of other congested objects.
277        // Consider in a consensus commit, there are many transactions touching object A, and later in processing the
278        // consensus commit, there is a transaction touching both object A and B. Although there are fewer transactions
279        // touching object B, because it's starting execution is delayed due to dependency to other transactions on
280        // object A, it may be shown up as congested objects.
281        let mut congested_objects = vec![];
282        for obj in shared_input_objects {
283            // TODO: right now, we only return objects that are on the execution critical path in this consensus commit.
284            // However, for objects that are no on the critical path, they may potentially also be congested (e.g., an
285            // object has start cost == start_cost - 1, and adding the gas budget will exceed the limit). We don't
286            // return them for now because it's unclear how they can be used to return suggested gas price for the
287            // user. We need to revisit this later once we have a clear idea of how to determine the suggested gas price.
288            if &start_cost == self.object_execution_cost.get(&obj.id).unwrap_or(&0) {
289                congested_objects.push(obj.id);
290            }
291        }
292
293        assert!(!congested_objects.is_empty());
294
295        let deferral_key =
296            if let Some(previous_key) = previously_deferred_tx_digests.get(cert.digest()) {
297                // This transaction has been deferred in previous consensus commit. Use its previous deferred_from_round.
298                DeferralKey::new_for_consensus_round(
299                    commit_round + 1,
300                    previous_key.deferred_from_round(),
301                )
302            } else {
303                // This transaction has not been deferred before. Use the current commit round
304                // as the deferred_from_round.
305                DeferralKey::new_for_consensus_round(commit_round + 1, commit_round)
306            };
307        Some((deferral_key, congested_objects))
308    }
309
310    // Update shared objects' execution cost used in `cert` using `cert`'s execution cost.
311    // This is called when `cert` is scheduled for execution.
312    pub fn bump_object_execution_cost(
313        &mut self,
314        tx_cost: Option<u64>,
315        cert: &VerifiedExecutableTransaction,
316    ) {
317        let shared_input_objects: Vec<_> = cert.shared_input_objects().collect();
318        if shared_input_objects.is_empty() {
319            return;
320        }
321
322        let Some(tx_cost) = tx_cost else {
323            return;
324        };
325
326        let start_cost = self.compute_tx_start_at_cost(&shared_input_objects);
327        let end_cost = start_cost.saturating_add(tx_cost);
328
329        for obj in shared_input_objects {
330            if obj.is_accessed_exclusively() {
331                let old_end_cost = self.object_execution_cost.insert(obj.id, end_cost);
332                assert!(old_end_cost.is_none() || old_end_cost.unwrap() <= end_cost);
333            }
334        }
335    }
336
337    // Returns accumulated debts for objects whose budgets have been exceeded over the course
338    // of the commit. Consumes the tracker object, since this should only be called once after
339    // all tx have been processed.
340    pub fn accumulated_debts(self, commit_info: &ConsensusCommitInfo) -> Vec<(ObjectID, u64)> {
341        if self.params.max_overage() == 0 {
342            return vec![]; // early-exit if overage is not allowed
343        }
344
345        self.object_execution_cost
346            .into_iter()
347            .filter_map(|(obj_id, cost)| {
348                let remaining_cost = cost.saturating_sub(self.params.commit_budget(commit_info));
349                if remaining_cost > 0 {
350                    Some((obj_id, remaining_cost))
351                } else {
352                    None
353                }
354            })
355            .collect()
356    }
357
358    // Returns the maximum cost of all objects.
359    pub fn max_cost(&self) -> u64 {
360        self.object_execution_cost
361            .values()
362            .max()
363            .copied()
364            .unwrap_or(0)
365    }
366
367    fn get_tx_cost_cap(&self, cert: &VerifiedExecutableTransaction) -> u64 {
368        let mut number_of_move_call = 0;
369        let mut number_of_move_input = 0;
370        for command in cert.transaction_data().kind().iter_commands() {
371            if let sui_types::transaction::Command::MoveCall(move_call) = command {
372                number_of_move_call += 1;
373                for aug in move_call.arguments.iter() {
374                    if let Argument::Input(_) = aug {
375                        number_of_move_input += 1;
376                    }
377                }
378            }
379        }
380        let cap = (number_of_move_call + number_of_move_input) as u64
381            * self.params.gas_budget_based_txn_cost_cap_factor();
382
383        // Apply absolute cap if configured.
384        std::cmp::min(
385            cap,
386            self.params
387                .gas_budget_based_txn_cost_absolute_cap()
388                .unwrap_or(u64::MAX),
389        )
390    }
391}
392
393#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
394pub enum CongestionPerObjectDebt {
395    V1(Round, u64),
396}
397
398impl CongestionPerObjectDebt {
399    pub fn new(round: Round, debt: u64) -> Self {
400        Self::V1(round, debt)
401    }
402
403    pub fn into_v1(self) -> (Round, u64) {
404        match self {
405            Self::V1(round, debt) => (round, debt),
406        }
407    }
408}
409
410#[cfg(test)]
411mod object_cost_tests {
412    use super::*;
413
414    use rstest::rstest;
415    use std::time::Duration;
416    use sui_protocol_config::ExecutionTimeEstimateParams;
417    use sui_test_transaction_builder::TestTransactionBuilder;
418    use sui_types::Identifier;
419    use sui_types::base_types::{SequenceNumber, random_object_ref};
420    use sui_types::crypto::{AccountKeyPair, get_key_pair};
421    use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder;
422    use sui_types::transaction::{CallArg, ObjectArg, SharedObjectMutability, VerifiedTransaction};
423
424    fn construct_shared_input_objects(objects: &[(ObjectID, bool)]) -> Vec<SharedInputObject> {
425        objects
426            .iter()
427            .map(|(id, mutable)| SharedInputObject {
428                id: *id,
429                initial_shared_version: SequenceNumber::new(),
430                mutability: if *mutable {
431                    SharedObjectMutability::Mutable
432                } else {
433                    SharedObjectMutability::Immutable
434                },
435            })
436            .collect()
437    }
438
439    #[test]
440    fn test_compute_tx_start_at_cost() {
441        let object_id_0 = ObjectID::random();
442        let object_id_1 = ObjectID::random();
443        let object_id_2 = ObjectID::random();
444
445        let shared_object_congestion_tracker = SharedObjectCongestionTracker::new(
446            [(object_id_0, 5), (object_id_1, 10)],
447            PerObjectCongestionControlMode::TotalGasBudget,
448            false,
449            Some(0), // not part of this test
450            None,
451            None,
452            0,
453            0,
454        );
455
456        let shared_input_objects = construct_shared_input_objects(&[(object_id_0, false)]);
457        assert_eq!(
458            shared_object_congestion_tracker.compute_tx_start_at_cost(&shared_input_objects),
459            5
460        );
461
462        let shared_input_objects = construct_shared_input_objects(&[(object_id_1, true)]);
463        assert_eq!(
464            shared_object_congestion_tracker.compute_tx_start_at_cost(&shared_input_objects),
465            10
466        );
467
468        let shared_input_objects =
469            construct_shared_input_objects(&[(object_id_0, false), (object_id_1, false)]);
470        assert_eq!(
471            shared_object_congestion_tracker.compute_tx_start_at_cost(&shared_input_objects),
472            10
473        );
474
475        let shared_input_objects =
476            construct_shared_input_objects(&[(object_id_0, true), (object_id_1, true)]);
477        assert_eq!(
478            shared_object_congestion_tracker.compute_tx_start_at_cost(&shared_input_objects),
479            10
480        );
481
482        // Test tx that touch object for the first time, which should start from 0.
483        let shared_input_objects = construct_shared_input_objects(&[(object_id_2, true)]);
484        assert_eq!(
485            shared_object_congestion_tracker.compute_tx_start_at_cost(&shared_input_objects),
486            0
487        );
488    }
489
490    // Builds a certificate with a list of shared objects and their mutability. The certificate is only used to
491    // test the SharedObjectCongestionTracker functions, therefore the content other than shared inputs and gas budget
492    // are not important.
493    fn build_transaction(
494        objects: &[(ObjectID, bool)],
495        gas_budget: u64,
496    ) -> VerifiedExecutableTransaction {
497        let (sender, keypair): (_, AccountKeyPair) = get_key_pair();
498        let gas_object = random_object_ref();
499        VerifiedExecutableTransaction::new_system(
500            VerifiedTransaction::new_unchecked(
501                TestTransactionBuilder::new(sender, gas_object, 1000)
502                    .with_gas_budget(gas_budget)
503                    .move_call(
504                        ObjectID::random(),
505                        "unimportant_module",
506                        "unimportant_function",
507                        objects
508                            .iter()
509                            .map(|(id, mutable)| {
510                                CallArg::Object(ObjectArg::SharedObject {
511                                    id: *id,
512                                    initial_shared_version: SequenceNumber::new(),
513                                    mutability: if *mutable {
514                                        SharedObjectMutability::Mutable
515                                    } else {
516                                        SharedObjectMutability::Immutable
517                                    },
518                                })
519                            })
520                            .collect(),
521                    )
522                    .build_and_sign(&keypair),
523            ),
524            0,
525        )
526    }
527
528    fn build_programmable_transaction(
529        objects: &[(ObjectID, bool)],
530        number_of_commands: u64,
531        gas_budget: u64,
532    ) -> VerifiedExecutableTransaction {
533        let (sender, keypair): (_, AccountKeyPair) = get_key_pair();
534        let gas_object = random_object_ref();
535
536        let package_id = ObjectID::random();
537        let mut pt_builder = ProgrammableTransactionBuilder::new();
538        let mut arguments = Vec::new();
539        for object in objects {
540            arguments.push(
541                pt_builder
542                    .obj(ObjectArg::SharedObject {
543                        id: object.0,
544                        initial_shared_version: SequenceNumber::new(),
545                        mutability: if object.1 {
546                            SharedObjectMutability::Mutable
547                        } else {
548                            SharedObjectMutability::Immutable
549                        },
550                    })
551                    .unwrap(),
552            );
553        }
554        for _ in 0..number_of_commands {
555            pt_builder.programmable_move_call(
556                package_id,
557                Identifier::new("unimportant_module").unwrap(),
558                Identifier::new("unimportant_function").unwrap(),
559                vec![],
560                arguments.clone(),
561            );
562        }
563
564        let pt = pt_builder.finish();
565        VerifiedExecutableTransaction::new_system(
566            VerifiedTransaction::new_unchecked(
567                TestTransactionBuilder::new(sender, gas_object, 1000)
568                    .with_gas_budget(gas_budget)
569                    .programmable(pt)
570                    .build_and_sign(&keypair),
571            ),
572            0,
573        )
574    }
575
576    #[rstest]
577    fn test_should_defer_return_correct_congested_objects(
578        #[values(
579            PerObjectCongestionControlMode::TotalGasBudget,
580            PerObjectCongestionControlMode::TotalTxCount,
581            PerObjectCongestionControlMode::TotalGasBudgetWithCap
582        )]
583        mode: PerObjectCongestionControlMode,
584    ) {
585        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
586
587        // Creates two shared objects and three transactions that operate on these objects.
588        let shared_obj_0 = ObjectID::random();
589        let shared_obj_1 = ObjectID::random();
590
591        let tx_gas_budget = 100;
592
593        // Set max_accumulated_txn_cost_per_object_in_commit to only allow 1 transaction to go through.
594        let max_accumulated_txn_cost_per_object_in_commit = match mode {
595            PerObjectCongestionControlMode::None => unreachable!(),
596            PerObjectCongestionControlMode::TotalGasBudget => tx_gas_budget + 1,
597            PerObjectCongestionControlMode::TotalTxCount => 2,
598            PerObjectCongestionControlMode::TotalGasBudgetWithCap => tx_gas_budget - 1,
599            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 0, // ignored
600        };
601
602        let shared_object_congestion_tracker = match mode {
603            PerObjectCongestionControlMode::None => unreachable!(),
604            PerObjectCongestionControlMode::TotalGasBudget => {
605                // Construct object execution cost as following
606                //                1     10
607                // object 0:            |
608                // object 1:      |
609                SharedObjectCongestionTracker::new(
610                    [(shared_obj_0, 10), (shared_obj_1, 1)],
611                    mode,
612                    false,
613                    Some(max_accumulated_txn_cost_per_object_in_commit),
614                    None,
615                    None,
616                    0,
617                    0,
618                )
619            }
620            PerObjectCongestionControlMode::TotalTxCount => {
621                // Construct object execution cost as following
622                //                1     2
623                // object 0:            |
624                // object 1:      |
625                SharedObjectCongestionTracker::new(
626                    [(shared_obj_0, 2), (shared_obj_1, 1)],
627                    mode,
628                    false,
629                    Some(max_accumulated_txn_cost_per_object_in_commit),
630                    None,
631                    None,
632                    0,
633                    0,
634                )
635            }
636            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
637                // Construct object execution cost as following
638                //                1     10
639                // object 0:            |
640                // object 1:      |
641                SharedObjectCongestionTracker::new(
642                    [(shared_obj_0, 10), (shared_obj_1, 1)],
643                    mode,
644                    false,
645                    Some(max_accumulated_txn_cost_per_object_in_commit),
646                    Some(45), // Make the cap just less than the gas budget, there are 1 objects in tx.
647                    None,
648                    0,
649                    0,
650                )
651            }
652            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
653                // Construct object execution cost as following
654                //                0     750
655                // object 0:            |
656                // object 1:      |
657                SharedObjectCongestionTracker::new(
658                    [(shared_obj_0, 750), (shared_obj_1, 0)],
659                    mode,
660                    false,
661                    Some(max_accumulated_txn_cost_per_object_in_commit),
662                    None,
663                    None,
664                    0,
665                    0,
666                )
667            }
668        };
669
670        // Read/write to object 0 should be deferred.
671        for mutable in [true, false].iter() {
672            let tx = build_transaction(&[(shared_obj_0, *mutable)], tx_gas_budget);
673            if let Some((_, congested_objects)) = shared_object_congestion_tracker
674                .should_defer_due_to_object_congestion(
675                    shared_object_congestion_tracker.get_tx_cost(
676                        Some(&execution_time_estimator),
677                        &tx,
678                        &mut IndirectStateObserver::new(),
679                    ),
680                    &tx,
681                    &HashMap::new(),
682                    &ConsensusCommitInfo::new_for_congestion_test(
683                        0,
684                        0,
685                        Duration::from_micros(1_500),
686                    ),
687                )
688            {
689                assert_eq!(congested_objects.len(), 1);
690                assert_eq!(congested_objects[0], shared_obj_0);
691            } else {
692                panic!("should defer");
693            }
694        }
695
696        // Read/write to object 1 should go through.
697        // When congestion control mode is TotalGasBudgetWithCap, even though the gas budget is over the limit,
698        // the cap should prevent the transaction from being deferred.
699        for mutable in [true, false].iter() {
700            let tx = build_transaction(&[(shared_obj_1, *mutable)], tx_gas_budget);
701            assert!(
702                shared_object_congestion_tracker
703                    .should_defer_due_to_object_congestion(
704                        shared_object_congestion_tracker.get_tx_cost(
705                            Some(&execution_time_estimator),
706                            &tx,
707                            &mut IndirectStateObserver::new(),
708                        ),
709                        &tx,
710                        &HashMap::new(),
711                        &ConsensusCommitInfo::new_for_congestion_test(
712                            0,
713                            0,
714                            Duration::from_micros(1_500),
715                        ),
716                    )
717                    .is_none()
718            );
719        }
720
721        // Transactions touching both objects should be deferred, with object 0 as the congested object.
722        for mutable_0 in [true, false].iter() {
723            for mutable_1 in [true, false].iter() {
724                let tx = build_transaction(
725                    &[(shared_obj_0, *mutable_0), (shared_obj_1, *mutable_1)],
726                    tx_gas_budget,
727                );
728                if let Some((_, congested_objects)) = shared_object_congestion_tracker
729                    .should_defer_due_to_object_congestion(
730                        shared_object_congestion_tracker.get_tx_cost(
731                            Some(&execution_time_estimator),
732                            &tx,
733                            &mut IndirectStateObserver::new(),
734                        ),
735                        &tx,
736                        &HashMap::new(),
737                        &ConsensusCommitInfo::new_for_congestion_test(
738                            0,
739                            0,
740                            Duration::from_micros(1_500),
741                        ),
742                    )
743                {
744                    assert_eq!(congested_objects.len(), 1);
745                    assert_eq!(congested_objects[0], shared_obj_0);
746                } else {
747                    panic!("should defer");
748                }
749            }
750        }
751    }
752
753    #[rstest]
754    fn test_should_defer_return_correct_deferral_key(
755        #[values(
756            PerObjectCongestionControlMode::TotalGasBudget,
757            PerObjectCongestionControlMode::TotalTxCount,
758            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
759            PerObjectCongestionControlMode::ExecutionTimeEstimate(ExecutionTimeEstimateParams {
760                target_utilization: 0,
761                allowed_txn_cost_overage_burst_limit_us: 0,
762                max_estimate_us: u64::MAX,
763                randomness_scalar: 0,
764                stored_observations_num_included_checkpoints: 10,
765                stored_observations_limit: u64::MAX,
766                stake_weighted_median_threshold: 0,
767                default_none_duration_for_new_keys: false,
768                observations_chunk_size: None,
769            }),
770        )]
771        mode: PerObjectCongestionControlMode,
772    ) {
773        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
774
775        let shared_obj_0 = ObjectID::random();
776        let tx = build_transaction(&[(shared_obj_0, true)], 100);
777
778        let shared_object_congestion_tracker = SharedObjectCongestionTracker::new(
779            [(shared_obj_0, 1)], // set initial cost that exceeds 0 burst limit
780            mode,
781            false,
782            Some(0), // Make should_defer_due_to_object_congestion always defer transactions.
783            Some(2),
784            None,
785            0,
786            0,
787        );
788
789        // Insert a random pre-existing transaction.
790        let mut previously_deferred_tx_digests = HashMap::new();
791        previously_deferred_tx_digests.insert(
792            TransactionDigest::random(),
793            DeferralKey::ConsensusRound {
794                future_round: 10,
795                deferred_from_round: 5,
796            },
797        );
798
799        // Test deferral key for a transaction that has not been deferred before.
800        if let Some((
801            DeferralKey::ConsensusRound {
802                future_round,
803                deferred_from_round,
804            },
805            _,
806        )) = shared_object_congestion_tracker.should_defer_due_to_object_congestion(
807            shared_object_congestion_tracker.get_tx_cost(
808                Some(&execution_time_estimator),
809                &tx,
810                &mut IndirectStateObserver::new(),
811            ),
812            &tx,
813            &previously_deferred_tx_digests,
814            &ConsensusCommitInfo::new_for_congestion_test(
815                10,
816                10,
817                Duration::from_micros(10_000_000),
818            ),
819        ) {
820            assert_eq!(future_round, 11);
821            assert_eq!(deferred_from_round, 10);
822        } else {
823            panic!("should defer");
824        }
825
826        // Insert `tx` as previously deferred transaction due to randomness.
827        previously_deferred_tx_digests.insert(
828            *tx.digest(),
829            DeferralKey::Randomness {
830                deferred_from_round: 4,
831            },
832        );
833
834        // New deferral key should have deferred_from_round equal to the deferred randomness round.
835        if let Some((
836            DeferralKey::ConsensusRound {
837                future_round,
838                deferred_from_round,
839            },
840            _,
841        )) = shared_object_congestion_tracker.should_defer_due_to_object_congestion(
842            shared_object_congestion_tracker.get_tx_cost(
843                Some(&execution_time_estimator),
844                &tx,
845                &mut IndirectStateObserver::new(),
846            ),
847            &tx,
848            &previously_deferred_tx_digests,
849            &ConsensusCommitInfo::new_for_congestion_test(
850                10,
851                10,
852                Duration::from_micros(10_000_000),
853            ),
854        ) {
855            assert_eq!(future_round, 11);
856            assert_eq!(deferred_from_round, 4);
857        } else {
858            panic!("should defer");
859        }
860
861        // Insert `tx` as previously deferred consensus transaction.
862        previously_deferred_tx_digests.insert(
863            *tx.digest(),
864            DeferralKey::ConsensusRound {
865                future_round: 10,
866                deferred_from_round: 5,
867            },
868        );
869
870        // New deferral key should have deferred_from_round equal to the one in the old deferral key.
871        if let Some((
872            DeferralKey::ConsensusRound {
873                future_round,
874                deferred_from_round,
875            },
876            _,
877        )) = shared_object_congestion_tracker.should_defer_due_to_object_congestion(
878            shared_object_congestion_tracker.get_tx_cost(
879                Some(&execution_time_estimator),
880                &tx,
881                &mut IndirectStateObserver::new(),
882            ),
883            &tx,
884            &previously_deferred_tx_digests,
885            &ConsensusCommitInfo::new_for_congestion_test(
886                10,
887                10,
888                Duration::from_micros(10_000_000),
889            ),
890        ) {
891            assert_eq!(future_round, 11);
892            assert_eq!(deferred_from_round, 5);
893        } else {
894            panic!("should defer");
895        }
896    }
897
898    #[rstest]
899    fn test_should_defer_allow_overage(
900        #[values(
901            PerObjectCongestionControlMode::TotalGasBudget,
902            PerObjectCongestionControlMode::TotalTxCount,
903            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
904            PerObjectCongestionControlMode::ExecutionTimeEstimate(ExecutionTimeEstimateParams {
905                target_utilization: 16,
906                allowed_txn_cost_overage_burst_limit_us: 0,
907                randomness_scalar: 0,
908                max_estimate_us: u64::MAX,
909                stored_observations_num_included_checkpoints: 10,
910                stored_observations_limit: u64::MAX,
911                stake_weighted_median_threshold: 0,
912                default_none_duration_for_new_keys: false,
913                observations_chunk_size: None,
914            }),
915        )]
916        mode: PerObjectCongestionControlMode,
917    ) {
918        telemetry_subscribers::init_for_testing();
919
920        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
921
922        // Creates two shared objects and three transactions that operate on these objects.
923        let shared_obj_0 = ObjectID::random();
924        let shared_obj_1 = ObjectID::random();
925
926        let tx_gas_budget = 100;
927
928        // Set max_accumulated_txn_cost_per_object_in_commit to only allow 1 transaction to go through
929        // before overage occurs.
930        let max_accumulated_txn_cost_per_object_in_commit = match mode {
931            PerObjectCongestionControlMode::None => unreachable!(),
932            PerObjectCongestionControlMode::TotalGasBudget => tx_gas_budget + 1,
933            PerObjectCongestionControlMode::TotalTxCount => 2,
934            PerObjectCongestionControlMode::TotalGasBudgetWithCap => tx_gas_budget - 1,
935            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 0, // ignored
936        };
937
938        let shared_object_congestion_tracker = match mode {
939            PerObjectCongestionControlMode::None => unreachable!(),
940            PerObjectCongestionControlMode::TotalGasBudget => {
941                // Construct object execution cost as following
942                //                90    102
943                // object 0:            |
944                // object 1:      |
945                SharedObjectCongestionTracker::new(
946                    [(shared_obj_0, 102), (shared_obj_1, 90)],
947                    mode,
948                    false,
949                    Some(max_accumulated_txn_cost_per_object_in_commit),
950                    None,
951                    None,
952                    max_accumulated_txn_cost_per_object_in_commit * 10,
953                    0,
954                )
955            }
956            PerObjectCongestionControlMode::TotalTxCount => {
957                // Construct object execution cost as following
958                //                2     3
959                // object 0:            |
960                // object 1:      |
961                SharedObjectCongestionTracker::new(
962                    [(shared_obj_0, 3), (shared_obj_1, 2)],
963                    mode,
964                    false,
965                    Some(max_accumulated_txn_cost_per_object_in_commit),
966                    None,
967                    None,
968                    max_accumulated_txn_cost_per_object_in_commit * 10,
969                    0,
970                )
971            }
972            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
973                // Construct object execution cost as following
974                //                90    100
975                // object 0:            |
976                // object 1:      |
977                SharedObjectCongestionTracker::new(
978                    [(shared_obj_0, 100), (shared_obj_1, 90)],
979                    mode,
980                    false,
981                    Some(max_accumulated_txn_cost_per_object_in_commit),
982                    Some(45), // Make the cap just less than the gas budget, there are 1 objects in tx.
983                    None,
984                    max_accumulated_txn_cost_per_object_in_commit * 10,
985                    0,
986                )
987            }
988            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
989                // Construct object execution cost as following
990                //                300K  1.7M
991                // object 0:            |
992                // object 1:      |
993                SharedObjectCongestionTracker::new(
994                    [(shared_obj_0, 1_700_000), (shared_obj_1, 300_000)],
995                    mode,
996                    false,
997                    Some(max_accumulated_txn_cost_per_object_in_commit),
998                    None,
999                    None,
1000                    max_accumulated_txn_cost_per_object_in_commit * 10,
1001                    0,
1002                )
1003            }
1004        };
1005
1006        // Read/write to object 0 should be deferred.
1007        for mutable in [true, false].iter() {
1008            let tx = build_transaction(&[(shared_obj_0, *mutable)], tx_gas_budget);
1009            if let Some((_, congested_objects)) = shared_object_congestion_tracker
1010                .should_defer_due_to_object_congestion(
1011                    shared_object_congestion_tracker.get_tx_cost(
1012                        Some(&execution_time_estimator),
1013                        &tx,
1014                        &mut IndirectStateObserver::new(),
1015                    ),
1016                    &tx,
1017                    &HashMap::new(),
1018                    &ConsensusCommitInfo::new_for_congestion_test(
1019                        0,
1020                        0,
1021                        Duration::from_micros(10_000_000),
1022                    ),
1023                )
1024            {
1025                assert_eq!(congested_objects.len(), 1);
1026                assert_eq!(congested_objects[0], shared_obj_0);
1027            } else {
1028                panic!("should defer");
1029            }
1030        }
1031
1032        // Read/write to object 1 should go through even though the budget is exceeded.
1033        for mutable in [true, false].iter() {
1034            let tx = build_transaction(&[(shared_obj_1, *mutable)], tx_gas_budget);
1035            assert!(
1036                shared_object_congestion_tracker
1037                    .should_defer_due_to_object_congestion(
1038                        shared_object_congestion_tracker.get_tx_cost(
1039                            Some(&execution_time_estimator),
1040                            &tx,
1041                            &mut IndirectStateObserver::new(),
1042                        ),
1043                        &tx,
1044                        &HashMap::new(),
1045                        &ConsensusCommitInfo::new_for_congestion_test(
1046                            0,
1047                            0,
1048                            Duration::from_micros(10_000_000)
1049                        ),
1050                    )
1051                    .is_none()
1052            );
1053        }
1054
1055        // Transactions touching both objects should be deferred, with object 0 as the congested object.
1056        for mutable_0 in [true, false].iter() {
1057            for mutable_1 in [true, false].iter() {
1058                let tx = build_transaction(
1059                    &[(shared_obj_0, *mutable_0), (shared_obj_1, *mutable_1)],
1060                    tx_gas_budget,
1061                );
1062                if let Some((_, congested_objects)) = shared_object_congestion_tracker
1063                    .should_defer_due_to_object_congestion(
1064                        shared_object_congestion_tracker.get_tx_cost(
1065                            Some(&execution_time_estimator),
1066                            &tx,
1067                            &mut IndirectStateObserver::new(),
1068                        ),
1069                        &tx,
1070                        &HashMap::new(),
1071                        &ConsensusCommitInfo::new_for_congestion_test(
1072                            0,
1073                            0,
1074                            Duration::from_micros(10_000_000),
1075                        ),
1076                    )
1077                {
1078                    assert_eq!(congested_objects.len(), 1);
1079                    assert_eq!(congested_objects[0], shared_obj_0);
1080                } else {
1081                    panic!("should defer");
1082                }
1083            }
1084        }
1085    }
1086
1087    #[rstest]
1088    fn test_should_defer_allow_overage_with_burst(
1089        #[values(
1090            PerObjectCongestionControlMode::TotalGasBudget,
1091            PerObjectCongestionControlMode::TotalTxCount,
1092            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
1093            PerObjectCongestionControlMode::ExecutionTimeEstimate(ExecutionTimeEstimateParams {
1094                target_utilization: 16,
1095                allowed_txn_cost_overage_burst_limit_us: 1_500_000,
1096                randomness_scalar: 0,
1097                max_estimate_us: u64::MAX,
1098                stored_observations_num_included_checkpoints: 10,
1099                stored_observations_limit: u64::MAX,
1100                stake_weighted_median_threshold: 0,
1101                default_none_duration_for_new_keys: false,
1102                observations_chunk_size: None,
1103            }),
1104        )]
1105        mode: PerObjectCongestionControlMode,
1106    ) {
1107        telemetry_subscribers::init_for_testing();
1108
1109        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
1110
1111        let shared_obj_0 = ObjectID::random();
1112        let shared_obj_1 = ObjectID::random();
1113
1114        let tx_gas_budget = 100;
1115
1116        // Set max_accumulated_txn_cost_per_object_in_commit to allow 1 transaction to go through
1117        // before overage occurs.
1118        let max_accumulated_txn_cost_per_object_in_commit = match mode {
1119            PerObjectCongestionControlMode::None => unreachable!(),
1120            PerObjectCongestionControlMode::TotalGasBudget => tx_gas_budget,
1121            PerObjectCongestionControlMode::TotalTxCount => 2,
1122            PerObjectCongestionControlMode::TotalGasBudgetWithCap => tx_gas_budget,
1123            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 0, // ignored
1124        };
1125
1126        // Set burst limit to allow 1 extra transaction to go through.
1127        let allowed_txn_cost_overage_burst_per_object_in_commit = match mode {
1128            PerObjectCongestionControlMode::None => unreachable!(),
1129            PerObjectCongestionControlMode::TotalGasBudget => tx_gas_budget * 2,
1130            PerObjectCongestionControlMode::TotalTxCount => 2,
1131            PerObjectCongestionControlMode::TotalGasBudgetWithCap => tx_gas_budget * 2,
1132            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 0, // ignored
1133        };
1134
1135        let shared_object_congestion_tracker = match mode {
1136            PerObjectCongestionControlMode::None => unreachable!(),
1137            PerObjectCongestionControlMode::TotalGasBudget => {
1138                // Construct object execution cost as following
1139                //                199   301
1140                // object 0:            |
1141                // object 1:      |
1142                //
1143                // burst limit is 100 + 200 = 300
1144                // tx cost is 100 (gas budget)
1145                SharedObjectCongestionTracker::new(
1146                    [(shared_obj_0, 301), (shared_obj_1, 199)],
1147                    mode,
1148                    false,
1149                    Some(max_accumulated_txn_cost_per_object_in_commit),
1150                    None,
1151                    None,
1152                    max_accumulated_txn_cost_per_object_in_commit * 10,
1153                    allowed_txn_cost_overage_burst_per_object_in_commit,
1154                )
1155            }
1156            PerObjectCongestionControlMode::TotalTxCount => {
1157                // Construct object execution cost as following
1158                //                4     5
1159                // object 0:            |
1160                // object 1:      |
1161                //
1162                // burst limit is 2 + 2 = 4
1163                // tx cost is 1 (tx count)
1164                SharedObjectCongestionTracker::new(
1165                    [(shared_obj_0, 5), (shared_obj_1, 4)],
1166                    mode,
1167                    false,
1168                    Some(max_accumulated_txn_cost_per_object_in_commit),
1169                    None,
1170                    None,
1171                    max_accumulated_txn_cost_per_object_in_commit * 10,
1172                    allowed_txn_cost_overage_burst_per_object_in_commit,
1173                )
1174            }
1175            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
1176                // Construct object execution cost as following
1177                //                250   301
1178                // object 0:            |
1179                // object 1:      |
1180                //
1181                // burst limit is 100 + 200 = 300
1182                // tx cost is 90 (gas budget capped at 45*(1 move call + 1 input))
1183                SharedObjectCongestionTracker::new(
1184                    [(shared_obj_0, 301), (shared_obj_1, 250)],
1185                    mode,
1186                    false,
1187                    Some(max_accumulated_txn_cost_per_object_in_commit),
1188                    Some(45), // Make the cap just less than the gas budget, there are 1 objects in tx.
1189                    None,
1190                    max_accumulated_txn_cost_per_object_in_commit * 10,
1191                    allowed_txn_cost_overage_burst_per_object_in_commit,
1192                )
1193            }
1194            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
1195                // Construct object execution cost as following
1196                //                4M    2M
1197                // object 0:            |
1198                // object 1:      |
1199                //
1200                // burst limit is 1.6M + 1.5M = 3.1M
1201                // tx cost is 1.5M (default)
1202                SharedObjectCongestionTracker::new(
1203                    [(shared_obj_0, 4_000_000), (shared_obj_1, 2_000_000)],
1204                    mode,
1205                    false,
1206                    Some(max_accumulated_txn_cost_per_object_in_commit),
1207                    None,
1208                    None,
1209                    max_accumulated_txn_cost_per_object_in_commit * 10,
1210                    allowed_txn_cost_overage_burst_per_object_in_commit,
1211                )
1212            }
1213        };
1214
1215        // Read/write to object 0 should be deferred.
1216        for mutable in [true, false].iter() {
1217            let tx = build_transaction(&[(shared_obj_0, *mutable)], tx_gas_budget);
1218            if let Some((_, congested_objects)) = shared_object_congestion_tracker
1219                .should_defer_due_to_object_congestion(
1220                    shared_object_congestion_tracker.get_tx_cost(
1221                        Some(&execution_time_estimator),
1222                        &tx,
1223                        &mut IndirectStateObserver::new(),
1224                    ),
1225                    &tx,
1226                    &HashMap::new(),
1227                    &ConsensusCommitInfo::new_for_congestion_test(
1228                        0,
1229                        0,
1230                        Duration::from_micros(10_000_000),
1231                    ),
1232                )
1233            {
1234                assert_eq!(congested_objects.len(), 1);
1235                assert_eq!(congested_objects[0], shared_obj_0);
1236            } else {
1237                panic!("should defer");
1238            }
1239        }
1240
1241        // Read/write to object 1 should go through even though the budget is exceeded
1242        // even before the cost of this tx is considered.
1243        for mutable in [true, false].iter() {
1244            let tx = build_transaction(&[(shared_obj_1, *mutable)], tx_gas_budget);
1245            assert!(
1246                shared_object_congestion_tracker
1247                    .should_defer_due_to_object_congestion(
1248                        shared_object_congestion_tracker.get_tx_cost(
1249                            Some(&execution_time_estimator),
1250                            &tx,
1251                            &mut IndirectStateObserver::new(),
1252                        ),
1253                        &tx,
1254                        &HashMap::new(),
1255                        &ConsensusCommitInfo::new_for_congestion_test(
1256                            0,
1257                            0,
1258                            Duration::from_micros(10_000_000)
1259                        ),
1260                    )
1261                    .is_none()
1262            );
1263        }
1264
1265        // Transactions touching both objects should be deferred, with object 0 as the congested object.
1266        for mutable_0 in [true, false].iter() {
1267            for mutable_1 in [true, false].iter() {
1268                let tx = build_transaction(
1269                    &[(shared_obj_0, *mutable_0), (shared_obj_1, *mutable_1)],
1270                    tx_gas_budget,
1271                );
1272                if let Some((_, congested_objects)) = shared_object_congestion_tracker
1273                    .should_defer_due_to_object_congestion(
1274                        shared_object_congestion_tracker.get_tx_cost(
1275                            Some(&execution_time_estimator),
1276                            &tx,
1277                            &mut IndirectStateObserver::new(),
1278                        ),
1279                        &tx,
1280                        &HashMap::new(),
1281                        &ConsensusCommitInfo::new_for_congestion_test(
1282                            0,
1283                            0,
1284                            Duration::from_micros(10_000_000),
1285                        ),
1286                    )
1287                {
1288                    assert_eq!(congested_objects.len(), 1);
1289                    assert_eq!(congested_objects[0], shared_obj_0);
1290                } else {
1291                    panic!("should defer");
1292                }
1293            }
1294        }
1295    }
1296
1297    #[rstest]
1298    fn test_bump_object_execution_cost(
1299        #[values(
1300            PerObjectCongestionControlMode::TotalGasBudget,
1301            PerObjectCongestionControlMode::TotalTxCount,
1302            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
1303            PerObjectCongestionControlMode::ExecutionTimeEstimate(ExecutionTimeEstimateParams {
1304                // all params ignored in this test
1305                target_utilization: 0,
1306                allowed_txn_cost_overage_burst_limit_us: 0,
1307                randomness_scalar: 0,
1308                max_estimate_us: u64::MAX,
1309                stored_observations_num_included_checkpoints: 10,
1310                stored_observations_limit: u64::MAX,
1311                stake_weighted_median_threshold: 0,
1312                default_none_duration_for_new_keys: false,
1313                observations_chunk_size: None,
1314            }),
1315        )]
1316        mode: PerObjectCongestionControlMode,
1317    ) {
1318        telemetry_subscribers::init_for_testing();
1319
1320        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
1321
1322        let object_id_0 = ObjectID::random();
1323        let object_id_1 = ObjectID::random();
1324        let object_id_2 = ObjectID::random();
1325
1326        let cap_factor = Some(1);
1327
1328        let mut shared_object_congestion_tracker = SharedObjectCongestionTracker::new(
1329            [(object_id_0, 5), (object_id_1, 10)],
1330            mode,
1331            false,
1332            Some(0), // not part of this test
1333            cap_factor,
1334            None,
1335            0,
1336            0,
1337        );
1338        assert_eq!(shared_object_congestion_tracker.max_cost(), 10);
1339
1340        // Read two objects should not change the object execution cost.
1341        let cert = build_transaction(&[(object_id_0, false), (object_id_1, false)], 10);
1342        shared_object_congestion_tracker.bump_object_execution_cost(
1343            shared_object_congestion_tracker.get_tx_cost(
1344                Some(&execution_time_estimator),
1345                &cert,
1346                &mut IndirectStateObserver::new(),
1347            ),
1348            &cert,
1349        );
1350        assert_eq!(
1351            shared_object_congestion_tracker,
1352            SharedObjectCongestionTracker::new(
1353                [(object_id_0, 5), (object_id_1, 10)],
1354                mode,
1355                false,
1356                Some(0), // not part of this test
1357                cap_factor,
1358                None,
1359                0,
1360                0,
1361            )
1362        );
1363        assert_eq!(shared_object_congestion_tracker.max_cost(), 10);
1364
1365        // Write to object 0 should only bump object 0's execution cost. The start cost should be object 1's cost.
1366        let cert = build_transaction(&[(object_id_0, true), (object_id_1, false)], 10);
1367        shared_object_congestion_tracker.bump_object_execution_cost(
1368            shared_object_congestion_tracker.get_tx_cost(
1369                Some(&execution_time_estimator),
1370                &cert,
1371                &mut IndirectStateObserver::new(),
1372            ),
1373            &cert,
1374        );
1375        let expected_object_0_cost = match mode {
1376            PerObjectCongestionControlMode::None => unreachable!(),
1377            PerObjectCongestionControlMode::TotalGasBudget => 20,
1378            PerObjectCongestionControlMode::TotalTxCount => 11,
1379            PerObjectCongestionControlMode::TotalGasBudgetWithCap => 13, // 2 objects, 1 command.
1380            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 1_010,
1381        };
1382        assert_eq!(
1383            shared_object_congestion_tracker,
1384            SharedObjectCongestionTracker::new(
1385                [(object_id_0, expected_object_0_cost), (object_id_1, 10)],
1386                mode,
1387                false,
1388                Some(0), // not part of this test
1389                cap_factor,
1390                None,
1391                0,
1392                0,
1393            )
1394        );
1395        assert_eq!(
1396            shared_object_congestion_tracker.max_cost(),
1397            expected_object_0_cost
1398        );
1399
1400        // Write to all objects should bump all objects' execution cost, including objects that are seen for the first time.
1401        let cert = build_transaction(
1402            &[
1403                (object_id_0, true),
1404                (object_id_1, true),
1405                (object_id_2, true),
1406            ],
1407            10,
1408        );
1409        let expected_object_cost = match mode {
1410            PerObjectCongestionControlMode::None => unreachable!(),
1411            PerObjectCongestionControlMode::TotalGasBudget => 30,
1412            PerObjectCongestionControlMode::TotalTxCount => 12,
1413            PerObjectCongestionControlMode::TotalGasBudgetWithCap => 17, // 3 objects, 1 command
1414            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 2_010,
1415        };
1416        shared_object_congestion_tracker.bump_object_execution_cost(
1417            shared_object_congestion_tracker.get_tx_cost(
1418                Some(&execution_time_estimator),
1419                &cert,
1420                &mut IndirectStateObserver::new(),
1421            ),
1422            &cert,
1423        );
1424        assert_eq!(
1425            shared_object_congestion_tracker,
1426            SharedObjectCongestionTracker::new(
1427                [
1428                    (object_id_0, expected_object_cost),
1429                    (object_id_1, expected_object_cost),
1430                    (object_id_2, expected_object_cost)
1431                ],
1432                mode,
1433                false,
1434                Some(0), // not part of this test
1435                cap_factor,
1436                None,
1437                0,
1438                0,
1439            )
1440        );
1441        assert_eq!(
1442            shared_object_congestion_tracker.max_cost(),
1443            expected_object_cost
1444        );
1445
1446        // Write to all objects with PTBs containing 7 commands.
1447        let cert = build_programmable_transaction(
1448            &[
1449                (object_id_0, true),
1450                (object_id_1, true),
1451                (object_id_2, true),
1452            ],
1453            7,
1454            30,
1455        );
1456        let expected_object_cost = match mode {
1457            PerObjectCongestionControlMode::None => unreachable!(),
1458            PerObjectCongestionControlMode::TotalGasBudget => 60,
1459            PerObjectCongestionControlMode::TotalTxCount => 13,
1460            PerObjectCongestionControlMode::TotalGasBudgetWithCap => 45, // 3 objects, 7 commands
1461            // previous cost 2_010 + (unknown-command default of 1000 * 7 commands)
1462            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 9_010,
1463        };
1464        shared_object_congestion_tracker.bump_object_execution_cost(
1465            shared_object_congestion_tracker.get_tx_cost(
1466                Some(&execution_time_estimator),
1467                &cert,
1468                &mut IndirectStateObserver::new(),
1469            ),
1470            &cert,
1471        );
1472        assert_eq!(
1473            shared_object_congestion_tracker,
1474            SharedObjectCongestionTracker::new(
1475                [
1476                    (object_id_0, expected_object_cost),
1477                    (object_id_1, expected_object_cost),
1478                    (object_id_2, expected_object_cost)
1479                ],
1480                mode,
1481                false,
1482                Some(0), // not part of this test
1483                cap_factor,
1484                None,
1485                0,
1486                0,
1487            )
1488        );
1489        assert_eq!(
1490            shared_object_congestion_tracker.max_cost(),
1491            expected_object_cost
1492        );
1493    }
1494
1495    #[rstest]
1496    fn test_accumulated_debts(
1497        #[values(
1498            PerObjectCongestionControlMode::TotalGasBudget,
1499            PerObjectCongestionControlMode::TotalTxCount,
1500            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
1501            PerObjectCongestionControlMode::ExecutionTimeEstimate(ExecutionTimeEstimateParams {
1502                target_utilization: 100,
1503                // set a burst limit to verify that it does not affect debt calculation.
1504                allowed_txn_cost_overage_burst_limit_us: 1_600 * 5,
1505                randomness_scalar: 0,
1506                max_estimate_us: u64::MAX,
1507                stored_observations_num_included_checkpoints: 10,
1508                stored_observations_limit: u64::MAX,
1509                stake_weighted_median_threshold: 0,
1510                default_none_duration_for_new_keys: false,
1511                observations_chunk_size: None,
1512            }),
1513        )]
1514        mode: PerObjectCongestionControlMode,
1515    ) {
1516        telemetry_subscribers::init_for_testing();
1517
1518        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
1519
1520        // Creates two shared objects and three transactions that operate on these objects.
1521        let shared_obj_0 = ObjectID::random();
1522        let shared_obj_1 = ObjectID::random();
1523
1524        let tx_gas_budget = 100;
1525
1526        // Set max_accumulated_txn_cost_per_object_in_commit to only allow 1 transaction to go through
1527        // before overage occurs.
1528        let max_accumulated_txn_cost_per_object_in_commit = match mode {
1529            PerObjectCongestionControlMode::None => unreachable!(),
1530            PerObjectCongestionControlMode::TotalGasBudget
1531            | PerObjectCongestionControlMode::TotalGasBudgetWithCap => 90,
1532            PerObjectCongestionControlMode::TotalTxCount => 2,
1533            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => 0, // ignored
1534        };
1535
1536        let mut shared_object_congestion_tracker = match mode {
1537            PerObjectCongestionControlMode::None => unreachable!(),
1538            PerObjectCongestionControlMode::TotalGasBudget => {
1539                // Starting with two objects with accumulated cost 80.
1540                SharedObjectCongestionTracker::new(
1541                    [(shared_obj_0, 80), (shared_obj_1, 80)],
1542                    mode,
1543                    false,
1544                    Some(max_accumulated_txn_cost_per_object_in_commit),
1545                    None,
1546                    None,
1547                    max_accumulated_txn_cost_per_object_in_commit * 10,
1548                    // Set a burst limit to verify that it does not affect debt calculation.
1549                    max_accumulated_txn_cost_per_object_in_commit * 5,
1550                )
1551            }
1552            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
1553                // Starting with two objects with accumulated cost 80.
1554                SharedObjectCongestionTracker::new(
1555                    [(shared_obj_0, 80), (shared_obj_1, 80)],
1556                    mode,
1557                    false,
1558                    Some(max_accumulated_txn_cost_per_object_in_commit),
1559                    Some(45),
1560                    None,
1561                    max_accumulated_txn_cost_per_object_in_commit * 10,
1562                    // Set a burst limit to verify that it does not affect debt calculation.
1563                    max_accumulated_txn_cost_per_object_in_commit * 5,
1564                )
1565            }
1566            PerObjectCongestionControlMode::TotalTxCount => {
1567                // Starting with two objects with accumulated tx count 2.
1568                SharedObjectCongestionTracker::new(
1569                    [(shared_obj_0, 2), (shared_obj_1, 2)],
1570                    mode,
1571                    false,
1572                    Some(max_accumulated_txn_cost_per_object_in_commit),
1573                    None,
1574                    None,
1575                    max_accumulated_txn_cost_per_object_in_commit * 10,
1576                    // Set a burst limit to verify that it does not affect debt calculation.
1577                    max_accumulated_txn_cost_per_object_in_commit * 5,
1578                )
1579            }
1580            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
1581                // Starting with two objects with accumulated cost 500.
1582                SharedObjectCongestionTracker::new(
1583                    [(shared_obj_0, 500), (shared_obj_1, 500)],
1584                    mode,
1585                    false,
1586                    Some(max_accumulated_txn_cost_per_object_in_commit),
1587                    None,
1588                    None,
1589                    max_accumulated_txn_cost_per_object_in_commit * 10,
1590                    // Set a burst limit to verify that it does not affect debt calculation.
1591                    max_accumulated_txn_cost_per_object_in_commit * 5,
1592                )
1593            }
1594        };
1595
1596        // Simulate a tx on object 0 that exceeds the budget.
1597        for mutable in [true, false].iter() {
1598            let tx = build_transaction(&[(shared_obj_0, *mutable)], tx_gas_budget);
1599            shared_object_congestion_tracker.bump_object_execution_cost(
1600                shared_object_congestion_tracker.get_tx_cost(
1601                    Some(&execution_time_estimator),
1602                    &tx,
1603                    &mut IndirectStateObserver::new(),
1604                ),
1605                &tx,
1606            );
1607        }
1608
1609        // Verify that accumulated_debts reports the debt for object 0.
1610        let accumulated_debts = shared_object_congestion_tracker.accumulated_debts(
1611            &ConsensusCommitInfo::new_for_congestion_test(0, 0, Duration::from_micros(800)),
1612        );
1613        assert_eq!(accumulated_debts.len(), 1);
1614        match mode {
1615            PerObjectCongestionControlMode::None => unreachable!(),
1616            PerObjectCongestionControlMode::TotalGasBudget => {
1617                assert_eq!(accumulated_debts[0], (shared_obj_0, 90)); // init 80 + cost 100 - budget 90 = 90
1618            }
1619            PerObjectCongestionControlMode::TotalGasBudgetWithCap => {
1620                assert_eq!(accumulated_debts[0], (shared_obj_0, 80)); // init 80 + capped cost 90 - budget 90 = 80
1621            }
1622            PerObjectCongestionControlMode::TotalTxCount => {
1623                assert_eq!(accumulated_debts[0], (shared_obj_0, 1)); // init 2 + 1 tx - budget 2 = 1
1624            }
1625            PerObjectCongestionControlMode::ExecutionTimeEstimate(_) => {
1626                // init 500 + 1000 tx - budget 800 = 700
1627                assert_eq!(accumulated_debts[0], (shared_obj_0, 700));
1628            }
1629        }
1630    }
1631
1632    #[test]
1633    fn test_accumulated_debts_empty() {
1634        let object_id_0 = ObjectID::random();
1635        let object_id_1 = ObjectID::random();
1636        let object_id_2 = ObjectID::random();
1637
1638        let shared_object_congestion_tracker = SharedObjectCongestionTracker::new(
1639            [(object_id_0, 5), (object_id_1, 10), (object_id_2, 100)],
1640            PerObjectCongestionControlMode::TotalGasBudget,
1641            false,
1642            Some(100),
1643            None,
1644            None,
1645            0,
1646            0,
1647        );
1648
1649        let accumulated_debts = shared_object_congestion_tracker.accumulated_debts(
1650            &ConsensusCommitInfo::new_for_congestion_test(0, 0, Duration::ZERO),
1651        );
1652        assert!(accumulated_debts.is_empty());
1653    }
1654
1655    #[test]
1656    fn test_tx_cost_absolute_cap() {
1657        let execution_time_estimator = ExecutionTimeEstimator::new_for_testing();
1658
1659        let object_id_0 = ObjectID::random();
1660        let object_id_1 = ObjectID::random();
1661        let object_id_2 = ObjectID::random();
1662
1663        let tx_gas_budget = 2000;
1664
1665        let mut shared_object_congestion_tracker = SharedObjectCongestionTracker::new(
1666            [(object_id_0, 5), (object_id_1, 10), (object_id_2, 100)],
1667            PerObjectCongestionControlMode::TotalGasBudgetWithCap,
1668            false,
1669            Some(100),
1670            Some(1000),
1671            Some(2),
1672            1000,
1673            0,
1674        );
1675
1676        // Create a transaction using all three objects
1677        let tx = build_transaction(
1678            &[
1679                (object_id_0, false),
1680                (object_id_1, false),
1681                (object_id_2, true),
1682            ],
1683            tx_gas_budget,
1684        );
1685
1686        // Verify that the transaction is allowed to execute.
1687        // 2000 gas budget would exceed overage limit of 1000 but is capped to 200 by the absolute cap.
1688        let tx_cost = shared_object_congestion_tracker.get_tx_cost(
1689            Some(&execution_time_estimator),
1690            &tx,
1691            &mut IndirectStateObserver::new(),
1692        );
1693        assert!(
1694            shared_object_congestion_tracker
1695                .should_defer_due_to_object_congestion(
1696                    tx_cost,
1697                    &tx,
1698                    &HashMap::new(),
1699                    &ConsensusCommitInfo::new_for_congestion_test(0, 0, Duration::ZERO),
1700                )
1701                .is_none()
1702        );
1703
1704        // Verify max cost after bumping is limited by the absolute cap.
1705        shared_object_congestion_tracker.bump_object_execution_cost(tx_cost, &tx);
1706        assert_eq!(300, shared_object_congestion_tracker.max_cost());
1707
1708        // Verify accumulated debts still uses the per-commit budget to decrement.
1709        let accumulated_debts = shared_object_congestion_tracker.accumulated_debts(
1710            &ConsensusCommitInfo::new_for_congestion_test(0, 0, Duration::ZERO),
1711        );
1712        assert_eq!(accumulated_debts.len(), 1);
1713        assert_eq!(accumulated_debts[0], (object_id_2, 200));
1714    }
1715}