diff --git a/crates/iota-core/src/authority.rs b/crates/iota-core/src/authority.rs index d9aa518b5df..75f48cf3679 100644 --- a/crates/iota-core/src/authority.rs +++ b/crates/iota-core/src/authority.rs @@ -209,6 +209,7 @@ pub mod authority_store_types; pub mod epoch_start_configuration; pub mod shared_object_congestion_tracker; pub mod shared_object_version_manager; +pub mod suggested_gas_price_calculator; #[cfg(any(test, feature = "test-utils"))] pub mod test_authority_builder; pub mod transaction_deferral; diff --git a/crates/iota-core/src/authority/authority_per_epoch_store.rs b/crates/iota-core/src/authority/authority_per_epoch_store.rs index 9e97861072b..dc12e48f3af 100644 --- a/crates/iota-core/src/authority/authority_per_epoch_store.rs +++ b/crates/iota-core/src/authority/authority_per_epoch_store.rs @@ -94,6 +94,7 @@ use crate::{ shared_object_version_manager::{ AssignedTxAndVersions, ConsensusSharedObjVerAssignment, SharedObjVerManager, }, + suggested_gas_price_calculator::SuggestedGasPriceCalculator, }, checkpoints::{ BuilderCheckpointSummary, CheckpointHeight, CheckpointServiceNotify, EpochStats, @@ -193,7 +194,7 @@ impl DeferredTransaction { /// Represents a scheduling result: a transaction can be either scheduled /// for execution, or deferred for some reason. Scheduling result is /// returned by the `try_schedule` method of `AuthorityPerEpochStore`. -enum SchedulingResult { +pub(crate) enum SchedulingResult { /// Scheduling result indicating that a transaction is scheduled to be /// executed at start time Schedule(/* start_time */ ExecutionTime), @@ -3216,6 +3217,23 @@ impl AuthorityPerEpochStore { } ); + let mut suggested_gas_price_calculator = SuggestedGasPriceCalculator::new( + self.get_max_execution_duration_per_commit(), + self.reference_gas_price(), + self.protocol_config().max_gas_price(), + ); + + fail_point_arg!( + "initial_suggested_gas_price_calculator", + |calculator: SuggestedGasPriceCalculator| { + info!( + "Initialize suggested_gas_price_calculator to {:?}", + calculator + ); + suggested_gas_price_calculator = calculator; + } + ); + let mut randomness_state_updated = false; for tx in transactions { let key = tx.0.transaction.key(); @@ -3237,6 +3255,7 @@ impl AuthorityPerEpochStore { dkg_failed, randomness_round.is_some(), congestion_tracker, + &mut suggested_gas_price_calculator, authority_metrics, ) .await? @@ -3496,6 +3515,7 @@ impl AuthorityPerEpochStore { dkg_failed: bool, generating_randomness: bool, shared_object_congestion_tracker: &mut SharedObjectCongestionTracker, + suggested_gas_price_calculator: &mut SuggestedGasPriceCalculator, authority_metrics: &Arc, ) -> IotaResult { let _scope = monitored_scope("HandleConsensusTransaction"); @@ -3532,7 +3552,8 @@ impl AuthorityPerEpochStore { // certificate here it means authority is byzantine and sent certificate after // EndOfPublish (or we have some bug in ConsensusAdapter) warn!( - "[Byzantine authority] Authority {:?} sent a new, previously unseen certificate {:?} after it sent EndOfPublish message to consensus", + "[Byzantine authority] Authority {:?} sent a new, previously unseen + certificate {:?} after it sent EndOfPublish message to consensus", certificate_author.concise(), certificate.digest() ); @@ -3569,6 +3590,8 @@ impl AuthorityPerEpochStore { previously_deferred_tx_digests, shared_object_congestion_tracker, ); + let estimated_execution_duration = + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate); match scheduling_result { SchedulingResult::Defer(deferral_key, deferral_reason) => { @@ -3596,7 +3619,12 @@ impl AuthorityPerEpochStore { .congested_objects_gas_price_feedback_mechanism() { let current_commit_suggested_gas_price = - self.reference_gas_price(); + suggested_gas_price_calculator + .calculate_suggested_gas_price( + &certificate, + estimated_execution_duration, + ); + let suggested_gas_price = previously_deferred_tx_digests .get(certificate.digest()) .map_or_else( @@ -3651,7 +3679,8 @@ impl AuthorityPerEpochStore { } } }; - return Ok(deferral_result); + + Ok(deferral_result) } SchedulingResult::Schedule(start_time) => { if dkg_failed && certificate.transaction_data().uses_randomness() { @@ -3659,16 +3688,26 @@ impl AuthorityPerEpochStore { "Canceling randomness-using certificate for transaction {:?} because DKG failed", certificate.digest(), ); + return Ok(ConsensusCertificateResult::Cancelled(( certificate, CancelConsensusCertificateReason::DkgFailed, ))); } - // This certificate will be scheduled. Update object execution slots. + // This certificate will be scheduled. If it contains shared object(s), + // we have to update the following: + // - shared object execution slots (for congestion tracker); + // - shared object congestion info (for suggested gas price calculator). if certificate.contains_shared_object() { shared_object_congestion_tracker .bump_object_execution_slots(&certificate, start_time); + + suggested_gas_price_calculator.update_congestion_info( + &certificate, + start_time, + estimated_execution_duration, + ); } Ok(ConsensusCertificateResult::Scheduled { diff --git a/crates/iota-core/src/authority/shared_object_congestion_tracker.rs b/crates/iota-core/src/authority/shared_object_congestion_tracker.rs index 331e74593e1..86d442474d3 100644 --- a/crates/iota-core/src/authority/shared_object_congestion_tracker.rs +++ b/crates/iota-core/src/authority/shared_object_congestion_tracker.rs @@ -248,7 +248,7 @@ impl SharedObjectCongestionTracker { /// /// Before calling this function, the caller should ensure that the tracker /// is initialized for all objects in the transaction by first calling - /// `initialize_for_shared_objects`. + /// `initialize_object_execution_slots`. pub fn compute_tx_start_time( &self, shared_input_objects: &[SharedInputObject], @@ -428,6 +428,7 @@ impl SharedObjectCongestionTracker { .map(|obj| obj.id) .collect() }; + assert!(!congested_objects.is_empty()); let deferral_key = if let Some(previous_key_suggested_gas_price_pair) = @@ -623,19 +624,22 @@ pub mod shared_object_test_utils { use super::*; + pub const TEST_ONLY_GAS_PRICE: u64 = 1_000; + // Builds a certificate with a list of shared objects and their mutability. The // certificate is only used to test the SharedObjectCongestionTracker - // functions, therefore the content other than shared inputs and gas budget - // are not important. + // functions, therefore the content other than shared inputs, gas budget + // and gas price are not important. pub fn build_transaction( objects: &[(ObjectID, bool)], gas_budget: u64, + gas_price: u64, ) -> VerifiedExecutableTransaction { let (sender, keypair): (_, AccountKeyPair) = get_key_pair(); let gas_object = random_object_ref(); VerifiedExecutableTransaction::new_system( VerifiedTransaction::new_unchecked( - TestTransactionBuilder::new(sender, gas_object, 1000) + TestTransactionBuilder::new(sender, gas_object, gas_price) .with_gas_budget(gas_budget) .move_call( ObjectID::random(), @@ -702,15 +706,43 @@ pub mod shared_object_test_utils { match mode { PerObjectCongestionControlMode::None => {} PerObjectCongestionControlMode::TotalGasBudget => { - let transaction = build_transaction(&[(*object_id, true)], *duration); - let start_time = initialize_tracker_and_compute_tx_start_time(&mut shared_object_congestion_tracker, &transaction.data().inner().intent_message().value.shared_input_objects(), *duration).expect("initial value should be fit within the available range of slots in the tracker"); + let transaction = + build_transaction(&[(*object_id, true)], *duration, TEST_ONLY_GAS_PRICE); + let start_time = initialize_tracker_and_compute_tx_start_time( + &mut shared_object_congestion_tracker, + &transaction + .data() + .inner() + .intent_message() + .value + .shared_input_objects(), + *duration, + ) + .expect( + "initial value should be fit within the available range of slots \ + in the tracker", + ); shared_object_congestion_tracker .bump_object_execution_slots(&transaction, start_time); } PerObjectCongestionControlMode::TotalTxCount => { for _ in 0..*duration { - let transaction = build_transaction(&[(*object_id, true)], 1); - let start_time = initialize_tracker_and_compute_tx_start_time(&mut shared_object_congestion_tracker, &transaction.data().inner().intent_message().value.shared_input_objects(), 1).expect("initial value should be fit within the available range of slots in the tracker"); + let transaction = + build_transaction(&[(*object_id, true)], 1, TEST_ONLY_GAS_PRICE); + let start_time = initialize_tracker_and_compute_tx_start_time( + &mut shared_object_congestion_tracker, + &transaction + .data() + .inner() + .intent_message() + .value + .shared_input_objects(), + 1, + ) + .expect( + "initial value should be fit within the available range of \ + slots in the tracker", + ); shared_object_congestion_tracker .bump_object_execution_slots(&transaction, start_time); } @@ -784,7 +816,7 @@ mod object_cost_tests { Some(9) ); // now add this transaction to the tracker. - let tx = build_transaction(objects, 1); + let tx = build_transaction(objects, 1, TEST_ONLY_GAS_PRICE); shared_object_congestion_tracker.bump_object_execution_slots(&tx, 9); // That tracker now has the following object execution slots: @@ -969,7 +1001,11 @@ mod object_cost_tests { } }; // add a transaction that writes to object 0 and 1. - let tx = build_transaction(&[(shared_obj_0, true), (shared_obj_1, true)], 1); + let tx = build_transaction( + &[(shared_obj_0, true), (shared_obj_1, true)], + 1, + TEST_ONLY_GAS_PRICE, + ); shared_object_congestion_tracker.bump_object_execution_slots( &tx, match mode { @@ -993,7 +1029,11 @@ mod object_cost_tests { // Read/write to object 0 should be deferred. for mutable in [true, false].iter() { - let tx = build_transaction(&[(shared_obj_0, *mutable)], tx_gas_budget); + let tx = build_transaction( + &[(shared_obj_0, *mutable)], + tx_gas_budget, + TEST_ONLY_GAS_PRICE, + ); if let SequencingResult::Defer(_, congested_objects) = shared_object_congestion_tracker .try_schedule(&tx, max_execution_duration_per_commit, &HashMap::new(), 0) { @@ -1007,7 +1047,11 @@ mod object_cost_tests { // Read/write to object 1 should be scheduled with start_time 1 with // `assign_min_free_execution_slot` and deferred otherwise. for mutable in [true, false].iter() { - let tx = build_transaction(&[(shared_obj_1, *mutable)], tx_gas_budget); + let tx = build_transaction( + &[(shared_obj_1, *mutable)], + tx_gas_budget, + TEST_ONLY_GAS_PRICE, + ); let sequencing_result = initialize_tracker_and_try_schedule( &mut shared_object_congestion_tracker, &tx, @@ -1032,6 +1076,7 @@ mod object_cost_tests { let tx = build_transaction( &[(shared_obj_0, *mutable_0), (shared_obj_1, *mutable_1)], tx_gas_budget, + TEST_ONLY_GAS_PRICE, ); if let SequencingResult::Defer(_, congested_objects) = initialize_tracker_and_try_schedule( @@ -1062,7 +1107,7 @@ mod object_cost_tests { mode: PerObjectCongestionControlMode, ) { let shared_obj_0 = ObjectID::random(); - let tx = build_transaction(&[(shared_obj_0, true)], 100); + let tx = build_transaction(&[(shared_obj_0, true)], 100, TEST_ONLY_GAS_PRICE); // Make try_schedule always defers transactions. let max_execution_duration_per_commit = 0; let mut shared_object_congestion_tracker = SharedObjectCongestionTracker::new(mode, false); @@ -1191,7 +1236,11 @@ mod object_cost_tests { ); // Read two objects should not change the object execution slots. - let cert = build_transaction(&[(object_id_0, false), (object_id_1, false)], 10); + let cert = build_transaction( + &[(object_id_0, false), (object_id_1, false)], + 10, + TEST_ONLY_GAS_PRICE, + ); let cert_duration = shared_object_congestion_tracker.get_estimated_execution_duration(&cert); let start_time = initialize_tracker_and_compute_tx_start_time( @@ -1222,7 +1271,11 @@ mod object_cost_tests { // Write to object 0 should only bump object 0's execution slots. The start time // should be object 1's duration. - let cert = build_transaction(&[(object_id_0, true), (object_id_1, false)], 10); + let cert = build_transaction( + &[(object_id_0, true), (object_id_1, false)], + 10, + TEST_ONLY_GAS_PRICE, + ); let cert_duration = shared_object_congestion_tracker.get_estimated_execution_duration(&cert); let start_time = initialize_tracker_and_compute_tx_start_time( @@ -1272,6 +1325,7 @@ mod object_cost_tests { (object_id_2, true), ], 10, + TEST_ONLY_GAS_PRICE, ); let expected_object_duration = match mode { PerObjectCongestionControlMode::None => unreachable!(), @@ -1346,7 +1400,7 @@ mod object_cost_tests { assign_min_free_execution_slot, ); - let tx = build_transaction(&[(object_id_0, true)], 1); + let tx = build_transaction(&[(object_id_0, true)], 1, TEST_ONLY_GAS_PRICE); if let SequencingResult::Schedule(start_time) = initialize_tracker_and_try_schedule( &mut shared_object_congestion_tracker, &tx, @@ -1383,7 +1437,11 @@ mod object_cost_tests { panic!("transaction is not congesting, should not defer"); } - let tx = build_transaction(&[(object_id_0, true), (object_id_1, true)], 1); + let tx = build_transaction( + &[(object_id_0, true), (object_id_1, true)], + 1, + TEST_ONLY_GAS_PRICE, + ); if let SequencingResult::Defer(_, congested_objects) = initialize_tracker_and_try_schedule( &mut shared_object_congestion_tracker, &tx, @@ -1402,6 +1460,7 @@ mod object_cost_tests { } else { panic!("transaction is congesting, should defer"); } + let cert_duration = shared_object_congestion_tracker.get_estimated_execution_duration(&tx); assert!( initialize_tracker_and_compute_tx_start_time( @@ -1436,6 +1495,7 @@ mod object_cost_tests { (object_id_2, true), ], MAX_EXECUTION_TIME - 1, + TEST_ONLY_GAS_PRICE, ); if let SequencingResult::Defer(_, congested_objects) = initialize_tracker_and_try_schedule( &mut shared_object_congestion_tracker, @@ -1487,7 +1547,7 @@ mod object_cost_tests { assign_min_free_execution_slot, ); - let tx = build_transaction(&[(object_id_0, true)], u64::MAX); + let tx = build_transaction(&[(object_id_0, true)], u64::MAX, TEST_ONLY_GAS_PRICE); if let SequencingResult::Defer(_, congested_objects) = initialize_tracker_and_try_schedule( &mut shared_object_congestion_tracker, &tx, diff --git a/crates/iota-core/src/authority/suggested_gas_price_calculator.rs b/crates/iota-core/src/authority/suggested_gas_price_calculator.rs new file mode 100644 index 00000000000..eaaaa87511a --- /dev/null +++ b/crates/iota-core/src/authority/suggested_gas_price_calculator.rs @@ -0,0 +1,1389 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::{BTreeMap, HashMap}; + +use iota_types::{ + base_types::ObjectID, executable_transaction::VerifiedExecutableTransaction, + transaction::TransactionDataAPI, +}; +use tracing::instrument; + +use super::shared_object_congestion_tracker::ExecutionTime; + +/// Holds shared object congestion info for a single scheduled shared-object +/// transaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ScheduledTransactionCongestionInfo { + /// Gas price of a scheduled shared-object transaction. + gas_price: u64, + + /// Estimated execution duration of a scheduled shared-object transaction. + estimated_execution_duration: ExecutionTime, +} + +impl ScheduledTransactionCongestionInfo { + /// Create a new congestion info for scheduled shared-object transaction + /// with `gas_price` and `estimated_execution_duration`. + fn new(gas_price: u64, estimated_execution_duration: ExecutionTime) -> Self { + Self { + gas_price, + estimated_execution_duration, + } + } +} + +/// Holds shared object congestion info for a single shared object, +/// keyed by transaction execution start time. +type PerObjectCongestionInfo = BTreeMap; + +/// Holds shared object congestion data for a single consensus commit round. +type PerCommitCongestionInfo = HashMap; + +/// `SuggestedGasPriceCalculator` calculates suggested gas prices for +/// deferred/cancelled shared-object transactions, using congestion +/// info from a single consensus commit. +/// +/// The congestion info stored by the calculator should only be updated +/// for scheduled certificates. In contrast, calculations of the suggested +/// gas price should only be invoked for deferred/cancelled certificates. +/// +/// Roughly speaking, the suggested gas price calculator works as follows: +/// 1. For every scheduled certificate, obtain its reference gas price, +/// execution start time and estimated execution duration. +/// 2. For every input shared object accessed mutably by the scheduled +/// transaction, keep and update a map, ordered by execution start time +/// (key), whose values store scheduled certificate's gas price and estimated +/// execution duration. +/// 3. For every deferred/cancelled certificate, obtain its estimated execution +/// duration, as well as all input shared objects. +/// 4. Calculate a suggested gas price for the deferred/cancelled certificate as +/// follows: +/// - compute its (imaginary) execution start time as +/// `max_execution_duration_per_commit` minus its estimated execution +/// duration; +/// - for each input shared object, get the maximum gas price over scheduled +/// certificates whose end execution time is larger than our imaginary +/// start time; +/// - take the maximum over the values obtained in the previous step; +/// - the suggested gas price equals the maximum value obtained in the +/// previous step plus 1, but such that it does not become larger than the +/// maximum gas price set in the protocol. +/// +/// Note that if `max_execution_duration_per_commit` is set to `None`, +/// which means there is no shared object congestion control mechanism, +/// the calculator will suggest the reference gas price. +#[derive(Debug)] +pub(crate) struct SuggestedGasPriceCalculator { + /// Per-commit congestion info + congestion_info: PerCommitCongestionInfo, + + /// Maximum execution duration per shared object per commit. + max_execution_duration_per_commit: Option, + + /// The reference gas price, which will be suggested if + /// `max_execution_duration_per_commit` is set to `None`. + reference_gas_price: u64, + + /// Maximum gas price that can be set in transactions. This is + /// used to prevent suggesting feedback gas price larger than + /// this maximum value set in the protocol config. + max_gas_price: u64, +} + +impl SuggestedGasPriceCalculator { + /// Create a new `SuggestedGasPriceCalculator` with empty shared + /// object congestion data. + pub fn new( + max_execution_duration_per_commit: Option, + reference_gas_price: u64, + max_gas_price: u64, + ) -> Self { + Self { + congestion_info: PerCommitCongestionInfo::new(), + max_execution_duration_per_commit, + reference_gas_price, + max_gas_price, + } + } + + /// Update per-commit congestion info for a single certificate. This should + /// only be called for scheduled certificates that contain shared object(s); + /// otherwise, the calculator might wrongly calculate suggested gas price. + /// The `execution_start_time` and `estimated_execution_duration` parameters + /// are the outcomes of the shared object congestion tracker (sequencer). + pub fn update_congestion_info( + &mut self, + certificate: &VerifiedExecutableTransaction, + execution_start_time: ExecutionTime, + estimated_execution_duration: ExecutionTime, + ) { + // If we don't have a max execution duration, we don't need to update + // the congestion info since the reference gas price will be suggested. + if self.max_execution_duration_per_commit.is_none() { + return; + } + + let scheduled_transaction_congestion_info = ScheduledTransactionCongestionInfo::new( + certificate.transaction_data().gas_price(), + estimated_execution_duration, + ); + + certificate + .shared_input_objects() + // Only consider shared objects accessed mutably as objects accessed immutably + // do not change object's execution slots in the sequencer. + .filter(|object| object.mutable) + .for_each(|object| { + self.congestion_info + .entry(object.id) + .and_modify(|per_object_congestion_info| { + per_object_congestion_info + .insert(execution_start_time, scheduled_transaction_congestion_info); + }) + .or_insert(PerObjectCongestionInfo::from([( + execution_start_time, + scheduled_transaction_congestion_info, + )])); + }); + } + + /// Calculate a suggested gas price for a deferred/cancelled `certificate` + /// using the single-commit congestion info held by the calculator. This + /// should only be called for certificates deferred/cancelled due to + /// shared object congestion; otherwise, there is a risk of panic. + #[instrument(level = "trace", skip_all)] + pub fn calculate_suggested_gas_price( + &self, + certificate: &VerifiedExecutableTransaction, + estimated_execution_duration: ExecutionTime, + ) -> u64 { + if let Some(max_execution_duration_per_commit) = self.max_execution_duration_per_commit { + debug_assert!( + estimated_execution_duration <= max_execution_duration_per_commit, + "This certificate alone has estimated execution duration of \ + {estimated_execution_duration}, which is larger than the maximum execution \ + duration per commit {max_execution_duration_per_commit}, so the certificate \ + cannot be scheduled regardless of suggested gas price. It is likely that \ + {max_execution_duration_per_commit} was set too low in the protocol config, \ + such that a commit cannot accommodate a single certificate." + ); + + let clearing_gas_price = self.find_clearing_gas_price( + certificate, + estimated_execution_duration, + max_execution_duration_per_commit, + ); + + // Suggested gas price equals `clearing_gas_price + 1`. We add 1 to make this + // transaction would be scheduled if the same commit structure was repeated. + let suggested_gas_price = clearing_gas_price + 1; + + // Make sure suggested gas price is not larger than the maximum possible gas + // price. + suggested_gas_price.min(self.max_gas_price) + } else { + // ^ If we don't have a max execution duration, suggest the reference gas price. + + self.reference_gas_price + } + } + + /// Find the gas price for which a deferred/scheduled certificate would be + /// scheduled if that gas price was paid and if exactly the same set of + /// transactions appeared in a commit. + fn find_clearing_gas_price( + &self, + certificate: &VerifiedExecutableTransaction, + estimated_execution_duration: ExecutionTime, + max_execution_duration_per_commit: ExecutionTime, + ) -> u64 { + // Imaginary start time of the deferred/cancelled certificate. We consider + // only the highest possible (but sufficient for scheduling) start time as + // it is very likely that scheduled certificates with lower gas prices + // appear have higher start times. + let start_time_of_deferred_cert = + max_execution_duration_per_commit - estimated_execution_duration; + + certificate + .shared_input_objects() + .filter_map(|object| { + self.congestion_info + .get(&object.id) + .map(|per_object_congestion_info| { + per_object_congestion_info + .iter() + .filter_map(|(execution_start_time, tx_congestion_info)| { + let end_time_of_scheduled_cert = execution_start_time + + tx_congestion_info.estimated_execution_duration; + + if end_time_of_scheduled_cert > start_time_of_deferred_cert + { + // Store gas price of that scheduled certificate + Some(tx_congestion_info.gas_price) + } else { + None + } + }) + // Take the maximum over all found gas prices of scheduled certificates + // whose execution end time is larger than the imaginary start time + // of the deferred/cancelled transaction. It has to be maximum here + // since otherwise the suggested gas price will be insufficient to + // guarantee scheduling if the same set of certificates was repeated + // again in a commit. + .max() + }) + }) + // Take the maximum over all input shared objects, as we need to consider the + // "worst-case" (most congested) object; otherwise, the suggested gas price + // will be insufficient to guarantee scheduling if the same set of certificates + // was repeated again in a commit. + .max() + .flatten() + .unwrap_or_else(|| { + panic!( + "At least one of the shared input objects should have appeared in between \ + execution start time of {start_time_of_deferred_cert} and execution end time of \ + {max_execution_duration_per_commit}; otherwise, this deferred certificate \ + would be scheduled by the sequencer." + ); + }) + } +} + +#[cfg(test)] +pub mod suggested_gas_price_calculator_test_utils { + use iota_protocol_config::PerObjectCongestionControlMode; + use iota_types::base_types::ObjectID; + + use super::SuggestedGasPriceCalculator; + use crate::authority::shared_object_congestion_tracker::{ + ExecutionTime, SharedObjectCongestionTracker, + shared_object_test_utils::{ + build_transaction, initialize_tracker_and_compute_tx_start_time, + }, + }; + + pub(crate) fn new_suggested_gas_price_calculator_with_initial_values_for_test( + init_values: &[(ObjectID, ExecutionTime, u64)], + per_object_congestion_control_mode: PerObjectCongestionControlMode, + max_execution_duration_per_commit: Option, + min_free_execution_slot_assigned: bool, + reference_gas_price: u64, + max_gas_price: u64, + ) -> SuggestedGasPriceCalculator { + let mut suggested_gas_price_calculator = SuggestedGasPriceCalculator::new( + max_execution_duration_per_commit, + reference_gas_price, + max_gas_price, + ); + + let mut shared_object_congestion_tracker = SharedObjectCongestionTracker::new( + per_object_congestion_control_mode, + min_free_execution_slot_assigned, + ); + + for (object_id, duration, gas_price) in init_values { + match per_object_congestion_control_mode { + PerObjectCongestionControlMode::None => {} + PerObjectCongestionControlMode::TotalGasBudget => { + let certificate = + build_transaction(&[(*object_id, true)], *duration, *gas_price); + + let execution_start_time = initialize_tracker_and_compute_tx_start_time( + &mut shared_object_congestion_tracker, + &certificate.shared_input_objects().collect::>(), + *duration, + ) + .expect( + "initial value should be fit within the available range of slots \ + in the tracker", + ); + + shared_object_congestion_tracker + .bump_object_execution_slots(&certificate, execution_start_time); + + suggested_gas_price_calculator.update_congestion_info( + &certificate, + execution_start_time, + *duration, + ); + } + PerObjectCongestionControlMode::TotalTxCount => { + for _ in 0..*duration { + let certificate = build_transaction(&[(*object_id, true)], 1, *gas_price); + + let execution_start_time = initialize_tracker_and_compute_tx_start_time( + &mut shared_object_congestion_tracker, + &certificate.shared_input_objects().collect::>(), + *duration, + ) + .expect( + "initial value should be fit within the available range of slots \ + in the tracker", + ); + + shared_object_congestion_tracker + .bump_object_execution_slots(&certificate, execution_start_time); + + suggested_gas_price_calculator.update_congestion_info( + &certificate, + execution_start_time, + 1, + ); + } + } + } + } + + suggested_gas_price_calculator + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use iota_protocol_config::{PerObjectCongestionControlMode, ProtocolConfig}; + use iota_types::{base_types::ObjectID, executable_transaction::VerifiedExecutableTransaction}; + use rstest::rstest; + + use super::SuggestedGasPriceCalculator; + use crate::authority::{ + shared_object_congestion_tracker::{ + ExecutionTime, SequencingResult, SharedObjectCongestionTracker, + shared_object_test_utils::build_transaction, + }, + suggested_gas_price_calculator::{ + PerCommitCongestionInfo, PerObjectCongestionInfo, ScheduledTransactionCongestionInfo, + }, + }; + + const REFERENCE_GAS_PRICE: u64 = 1_000; + + #[derive(Copy, Clone)] + struct TxGasData { + global_ordering_index: usize, + gas_price: u64, + gas_budget: u64, + } + + fn build_and_try_sequencing_certificate( + input_shared_objects: &[(ObjectID, bool)], + tx_gas_data: TxGasData, + max_execution_duration_per_commit: ExecutionTime, + shared_object_congestion_tracker: &mut SharedObjectCongestionTracker, + ) -> (VerifiedExecutableTransaction, SequencingResult) { + let certificate = build_transaction( + input_shared_objects, + tx_gas_data.gas_budget, + tx_gas_data.gas_price, + ); + let shared_input_objects: Vec<_> = certificate.shared_input_objects().collect(); + shared_object_congestion_tracker.initialize_object_execution_slots(&shared_input_objects); + + let sequencing_result = shared_object_congestion_tracker.try_schedule( + &certificate, + max_execution_duration_per_commit, + // The next two inputs are not important for testing. + &HashMap::new(), + 0, + ); + + (certificate, sequencing_result) + } + + fn update_data_for_scheduled_certificate( + certificate: &VerifiedExecutableTransaction, + execution_start_time: ExecutionTime, + shared_object_congestion_tracker: &mut SharedObjectCongestionTracker, + suggested_gas_price_calculator: &mut SuggestedGasPriceCalculator, + ) { + shared_object_congestion_tracker + .bump_object_execution_slots(certificate, execution_start_time); + suggested_gas_price_calculator.update_congestion_info( + certificate, + execution_start_time, + shared_object_congestion_tracker.get_estimated_execution_duration(certificate), + ); + } + + #[rstest] + fn update_congestion_info( + #[values( + None, + Some(10), // the value is not important in this test + )] + max_execution_duration_per_commit: Option, + ) { + let max_gas_price = ProtocolConfig::get_for_max_version_UNSAFE().max_gas_price(); + let mut suggested_gas_price_calculator = SuggestedGasPriceCalculator::new( + max_execution_duration_per_commit, + REFERENCE_GAS_PRICE, + max_gas_price, + ); + + let object_1 = ObjectID::random(); + let object_2 = ObjectID::random(); + let object_3 = ObjectID::random(); + let object_4 = ObjectID::random(); + let object_5 = ObjectID::random(); + + // Construct the first certificate that touches shared objects: + // - `object_1` by mutable reference, + // - `object_2` by immutable reference. + let objects_1 = vec![(object_1, true), (object_2, false)]; + let gas_budget_1 = 1_003_000; // not important in this test + let gas_price_1 = 1_003; + let certificate_1 = build_transaction(&objects_1, gas_budget_1, gas_price_1); + let execution_start_time_1 = 0; + let estimated_execution_duration_1 = 3; + // Update the calculator's congestion info for this certificate. + suggested_gas_price_calculator.update_congestion_info( + &certificate_1, + execution_start_time_1, + estimated_execution_duration_1, + ); + // + if let Some(_max_execution_duration_per_commit) = max_execution_duration_per_commit { + // Note that `object_2` should not appear because it is accessed immutably. + let object_1_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_1, + ScheduledTransactionCongestionInfo::new( + gas_price_1, + estimated_execution_duration_1, + ), + )]); + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::from([(object_1, object_1_expected_congestion_info)]), + ); + } else { + // We don't have max execution duration per commit, so there is no need + // in updating the calculator's congestion info. + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::new() + ); + } + + // Construct the second certificate that touches shared objects: + // - `object_2` by mutable reference, + // - `object_3` by immutable reference, + // - `object_4` by mutable reference. + let objects_2 = vec![(object_2, true), (object_3, false), (object_4, true)]; + let gas_budget_2 = 1_002_000; // not important in this test + let gas_price_2 = 1_002; + let certificate_2 = build_transaction(&objects_2, gas_budget_2, gas_price_2); + let execution_start_time_2 = 1; + let estimated_execution_duration_2 = 2; + // Update the calculator's congestion info for this certificate. + suggested_gas_price_calculator.update_congestion_info( + &certificate_2, + execution_start_time_2, + estimated_execution_duration_2, + ); + // + if let Some(_max_execution_duration_per_commit) = max_execution_duration_per_commit { + // Note that `object_3` should not appear because it is accessed immutably. + let object_1_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_1, + ScheduledTransactionCongestionInfo::new( + gas_price_1, + estimated_execution_duration_1, + ), + )]); + let object_2_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_2, + ScheduledTransactionCongestionInfo::new( + gas_price_2, + estimated_execution_duration_2, + ), + )]); + let object_4_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_2, + ScheduledTransactionCongestionInfo::new( + gas_price_2, + estimated_execution_duration_2, + ), + )]); + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::from([ + (object_1, object_1_expected_congestion_info), + (object_2, object_2_expected_congestion_info), + (object_4, object_4_expected_congestion_info), + ]), + ); + } else { + // We don't have max execution duration per commit, so there is no need + // in updating the calculator's congestion info. + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::new() + ); + } + + // Construct the third certificate that touches shared objects: + // - `object_4` by immutable reference, + // - `object_5` by mutable reference. + let objects_3 = vec![(object_4, false), (object_5, true)]; + let gas_budget_3 = 1_001_000; // not important in this test + let gas_price_3 = 1_001; + let certificate_3 = build_transaction(&objects_3, gas_budget_3, gas_price_3); + let execution_start_time_3 = 2; + let estimated_execution_duration_3 = 1; + // Update the calculator's congestion info for this certificate. + suggested_gas_price_calculator.update_congestion_info( + &certificate_3, + execution_start_time_3, + estimated_execution_duration_3, + ); + // + if let Some(_max_execution_duration_per_commit) = max_execution_duration_per_commit { + // Note that `object_3` should not appear because it is accessed immutably. + let object_1_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_1, + ScheduledTransactionCongestionInfo::new( + gas_price_1, + estimated_execution_duration_1, + ), + )]); + let object_2_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_2, + ScheduledTransactionCongestionInfo::new( + gas_price_2, + estimated_execution_duration_2, + ), + )]); + let object_4_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_2, + ScheduledTransactionCongestionInfo::new( + gas_price_2, + estimated_execution_duration_2, + ), + )]); + let object_5_expected_congestion_info = PerObjectCongestionInfo::from([( + execution_start_time_3, + ScheduledTransactionCongestionInfo::new( + gas_price_3, + estimated_execution_duration_3, + ), + )]); + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::from([ + (object_1, object_1_expected_congestion_info), + (object_2, object_2_expected_congestion_info), + (object_4, object_4_expected_congestion_info), + (object_5, object_5_expected_congestion_info), + ]), + ); + } else { + // We don't have max execution duration per commit, so there is no need + // in updating the calculator's congestion info. + assert_eq!( + suggested_gas_price_calculator.congestion_info, + PerCommitCongestionInfo::new() + ); + } + } + + #[rstest] + fn calculate_suggested_gas_price( + #[values( + PerObjectCongestionControlMode::TotalTxCount, + PerObjectCongestionControlMode::TotalGasBudget + )] + mode: PerObjectCongestionControlMode, + #[values(false, true)] min_free_execution_slot_assigned: bool, + ) { + // Allow only two transactions per shared object per commit. In the + // `TotalGasBudget` mode, gas budget of transactions will be set + // accordingly. + let max_execution_duration_per_commit = match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => 3, + PerObjectCongestionControlMode::TotalGasBudget => 9_000_000, + }; + + let max_gas_price = ProtocolConfig::get_for_max_version_UNSAFE().max_gas_price(); + + let mut shared_object_congestion_tracker = + SharedObjectCongestionTracker::new(mode, min_free_execution_slot_assigned); + + let mut suggested_gas_price_calculator = SuggestedGasPriceCalculator::new( + Some(max_execution_duration_per_commit), + REFERENCE_GAS_PRICE, + max_gas_price, + ); + + let object_1 = ObjectID::random(); + let object_2 = ObjectID::random(); + + // Gas prices (sorted in descending order) and gas budget to build transactions + let txs_gas_data = [ + (max_gas_price, 3_000_000), // 0 + (9_000, 1_000_000), // 1 + (8_000, 4_000_000), // 2 + (7_000, 2_000_000), // 3 + (7_000, 1_000_001), // 4 + (7_000, 5_000_000), // 5 + (7_000, 5_000_001), // 6 + (7_000, 8_000_000), // 7 + (6_000, 4_000_000), // 8 + (5_000, 2_000_000), // 9 + (5_000, 1_000_001), // 10 + (5_000, 5_000_001), // 11 + (5_000, 9_000_000), // 12 + ] + .into_iter() + .enumerate() + .map(|(index, (gas_price, gas_budget))| TxGasData { + global_ordering_index: index, + gas_price, + gas_budget, + }) + .collect::>(); + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_1, true), (object_2, false)], + txs_gas_data[0], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // Allocations of mutably accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // |::::::::::::::::::::::::|::::::::::::::::::::::::|::::::::::::| + // | | | | + // |------------------------| |---- 3M | + // | | | | + // | | |---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled", + txs_gas_data[0].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_1, false), (object_2, true)], + txs_gas_data[1], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // Allocations of mutably accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // |::::::::::::::::::::::::|::::::::::::::::::::::::|::::::::::::| + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | | |---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // Certificate 1 cannot be scheduled at start time 0 because it touches + // object 1, even though immutably. + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled", + txs_gas_data[1].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_1, false), (object_2, true)], + txs_gas_data[2], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // Allocations of mutably accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | | |---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled", + txs_gas_data[2].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_2, true)], + txs_gas_data[3], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // If `min_free_execution_slot_assigned = false` (old sequencer), this + // certificate must be deferred. + if min_free_execution_slot_assigned { + // ^ This corresponds the new sequencer's logic + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled in the new sequencer", + txs_gas_data[3].global_ordering_index + ); + } + } else { + // ^ This corresponds the old sequencer's logic + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + assert_eq!(congested_objects, vec![object_2]); + + let suggested_gas_price = suggested_gas_price_calculator + .calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker + .get_estimated_execution_duration(&certificate), + ); + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } else { + panic!( + "Certificate {} must be deferred in the old sequencer", + txs_gas_data[3].global_ordering_index + ); + } + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_2, false)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[4], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + if min_free_execution_slot_assigned { + // ^ this corresponds the new sequencer's logic + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + } else { + // ^ this corresponds the old sequencer's logic + assert_eq!(congested_objects, vec![object_2]); + } + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[4].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_2, true)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[5], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + if min_free_execution_slot_assigned { + // ^ this corresponds the new sequencer's logic + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + } else { + // ^ this corresponds the old sequencer's logic + assert_eq!(congested_objects, vec![object_2]); + } + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[5].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_1, true), (object_2, true)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[6], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + if min_free_execution_slot_assigned { + // ^ this corresponds the new sequencer's logic + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + } else { + // ^ this corresponds the old sequencer's logic + assert_eq!(congested_objects, vec![object_2]); + } + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => { + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } + PerObjectCongestionControlMode::TotalGasBudget => { + assert_eq!(suggested_gas_price, txs_gas_data[1].gas_price + 1); + } + } + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[6].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_1, true), (object_2, true)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[7], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // | | |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + + if min_free_execution_slot_assigned { + // ^ this corresponds the new sequencer's logic + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + } else { + // ^ this corresponds the old sequencer's logic + match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => { + assert_eq!(congested_objects, vec![object_2]); + } + PerObjectCongestionControlMode::TotalGasBudget => { + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + } + } + } + + match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => { + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } + PerObjectCongestionControlMode::TotalGasBudget => { + assert_eq!(suggested_gas_price, max_gas_price); + } + } + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[7].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_1, true)], + txs_gas_data[8], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | |------------------------|---- 8M | + // | | | | + // |------------------------| |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | cert. 8 (g=6000, d=4M) | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled", + txs_gas_data[8].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &[(object_1, true)], + txs_gas_data[9], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | cert. 9 (g=5000, d=2M) |------------------------|---- 8M | + // | | | | + // |------------------------| |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | cert. 8 (g=6000, d=4M) | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + if let SequencingResult::Schedule(execution_start_time) = sequencing_result { + update_data_for_scheduled_certificate( + &certificate, + execution_start_time, + &mut shared_object_congestion_tracker, + &mut suggested_gas_price_calculator, + ); + } else { + panic!( + "Certificate {} must be scheduled", + txs_gas_data[9].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_1, false), (object_2, false)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[10], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | cert. 9 (g=5000, d=2M) |------------------------|---- 8M | + // | | | | + // |------------------------| |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | cert. 8 (g=6000, d=4M) | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[10].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_1, true), (object_2, false)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[11], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | cert. 9 (g=5000, d=2M) |------------------------|---- 8M | + // | | | | + // |------------------------| |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | cert. 8 (g=6000, d=4M) | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => { + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } + PerObjectCongestionControlMode::TotalGasBudget => { + assert_eq!(suggested_gas_price, txs_gas_data[1].gas_price + 1); + } + } + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[11].global_ordering_index + ); + } + + // Construct a certificate with some shared objects (note mutability), + // and try scheduling it. + let input_shared_objects = vec![(object_1, false), (object_2, true)]; + let (certificate, sequencing_result) = build_and_try_sequencing_certificate( + &input_shared_objects, + txs_gas_data[12], + max_execution_duration_per_commit, + &mut shared_object_congestion_tracker, + ); + // If `min_free_execution_slot_assigned = true`, allocations of mutably + // accessed shared objects should look as follows: + // |-------------------------------------------------|------------| + // | object_1 | object_2 | start time | + // |________________________|________________________|____________| + // |------------------------|------------------------|---- 9M | + // | | | | + // | cert. 9 (g=5000, d=2M) |------------------------|---- 8M | + // | | | | + // |------------------------| |---- 7M | + // | | | | + // | | cert. 2 (g=8000, d=4M) |---- 6M | + // | | | | + // | cert. 8 (g=6000, d=4M) | |---- 5M | + // | | | | + // | |------------------------|---- 4M | + // | | cert. 1 (g=9000, d=1M) | | + // |------------------------|------------------------|---- 3M | + // | | | | + // | |------------------------|---- 2M | + // | cert. 0 (g=100K, d=3M) | | | + // | | cert. 3 (g=7000, d=2M) |---- 1M | + // | | | | + // |-------------------------------------------------|---- 0 -----| + // That is, this certificate must be deferred in both new and old sequencers. + if let SequencingResult::Defer(_key, congested_objects) = sequencing_result { + assert_eq!( + congested_objects, + input_shared_objects + .into_iter() + .map(|(id, _)| id) + .collect::>() + ); + + let suggested_gas_price = suggested_gas_price_calculator.calculate_suggested_gas_price( + &certificate, + shared_object_congestion_tracker.get_estimated_execution_duration(&certificate), + ); + match mode { + PerObjectCongestionControlMode::None => unreachable!(), + PerObjectCongestionControlMode::TotalTxCount => { + assert_eq!(suggested_gas_price, txs_gas_data[2].gas_price + 1); + } + PerObjectCongestionControlMode::TotalGasBudget => { + assert_eq!(suggested_gas_price, max_gas_price); + } + } + } else { + panic!( + "Certificate {} must be deferred", + txs_gas_data[12].global_ordering_index + ); + } + } +} diff --git a/crates/iota-core/src/unit_tests/authority_tests.rs b/crates/iota-core/src/unit_tests/authority_tests.rs index 4df7ee56277..05484f153ef 100644 --- a/crates/iota-core/src/unit_tests/authority_tests.rs +++ b/crates/iota-core/src/unit_tests/authority_tests.rs @@ -6343,6 +6343,7 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { let mut certificates: Vec = vec![]; let gas_price_of_non_cancelled_txs = 2_000; let gas_price_of_cancelled_txs = 1_000; + let suggested_gas_price = gas_price_of_non_cancelled_txs + 1; // Create 3 transactions that operate on shared_objects[0]. These transactions // will go through eventually. @@ -6435,11 +6436,11 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { [ ( shared_objects[0].id(), - SequenceNumber::new_congested_with_suggested_gas_price(gas_price_of_cancelled_txs) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) ), ( shared_objects[1].id(), - SequenceNumber::new_congested_with_suggested_gas_price(gas_price_of_cancelled_txs) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) ) ] .into_iter() @@ -6473,11 +6474,11 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { vec![ SharedInput::Cancelled(( shared_objects[0].id(), - SequenceNumber::new_congested_with_suggested_gas_price(gas_price_of_cancelled_txs) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) )), SharedInput::Cancelled(( shared_objects[1].id(), - SequenceNumber::new_congested_with_suggested_gas_price(gas_price_of_cancelled_txs) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) )) ] ); @@ -6490,7 +6491,7 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { ); assert_eq!( cancellation_reason, - SequenceNumber::new_congested_with_suggested_gas_price(gas_price_of_cancelled_txs) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) ); // Consensus commit prologue contains cancelled txn shared object version @@ -6507,13 +6508,13 @@ async fn test_consensus_handler_congestion_control_transaction_cancellation() { ( shared_objects[0].id(), SequenceNumber::new_congested_with_suggested_gas_price( - gas_price_of_cancelled_txs + suggested_gas_price ), ), ( shared_objects[1].id(), SequenceNumber::new_congested_with_suggested_gas_price( - gas_price_of_cancelled_txs + suggested_gas_price ), ) ] diff --git a/crates/iota-core/src/unit_tests/congestion_control_tests.rs b/crates/iota-core/src/unit_tests/congestion_control_tests.rs index c26d03e03da..cfec4ed3a4e 100644 --- a/crates/iota-core/src/unit_tests/congestion_control_tests.rs +++ b/crates/iota-core/src/unit_tests/congestion_control_tests.rs @@ -31,6 +31,7 @@ use crate::{ }, move_integration_tests::build_and_publish_test_package, shared_object_congestion_tracker::shared_object_test_utils::new_congestion_tracker_with_initial_value_for_test, + suggested_gas_price_calculator::suggested_gas_price_calculator_test_utils::new_suggested_gas_price_calculator_with_initial_values_for_test, test_authority_builder::TestAuthorityBuilder, }, move_call, @@ -309,16 +310,34 @@ async fn test_congestion_control_execution_cancellation() { // Initialize shared object queue so that any transaction touches // shared_object_1 should result in congestion and cancellation. + let congestion_control_min_free_execution_slot = test_setup + .protocol_config + .congestion_control_min_free_execution_slot(); register_fail_point_arg("initial_congestion_tracker", move || { Some(new_congestion_tracker_with_initial_value_for_test( &[(shared_object_1.0, 10)], PerObjectCongestionControlMode::TotalGasBudget, - test_setup - .protocol_config - .congestion_control_min_free_execution_slot(), + congestion_control_min_free_execution_slot, )) }); + register_fail_point_arg("initial_suggested_gas_price_calculator", move || { + Some( + new_suggested_gas_price_calculator_with_initial_values_for_test( + &[(shared_object_1.0, 10, TEST_ONLY_GAS_PRICE)], + PerObjectCongestionControlMode::TotalGasBudget, + test_setup + .protocol_config + .max_accumulated_txn_cost_per_object_in_mysticeti_commit_as_option(), + test_setup + .protocol_config + .congestion_control_min_free_execution_slot(), + TEST_ONLY_GAS_PRICE, + test_setup.protocol_config.max_gas_price(), + ), + ) + }); + // Runs a transaction that touches shared_object_1, shared_object_2 and a owned // object. let (congested_tx, effects) = update_objects( @@ -338,6 +357,8 @@ async fn test_congestion_control_execution_cancellation() { ) .await; + let suggested_gas_price = TEST_ONLY_GAS_PRICE + 1; + // Transaction should be cancelled with `shared_object_1` and `shared_object_2` // as the congested objects, and the suggested gas price should be // `TEST_ONLY_GAS_PRICE`. @@ -346,7 +367,7 @@ async fn test_congestion_control_execution_cancellation() { &ExecutionStatus::Failure { error: ExecutionFailureStatus::ExecutionCancelledDueToSharedObjectCongestionV2 { congested_objects: CongestedObjects(vec![shared_object_1.0, shared_object_2.0]), - suggested_gas_price: TEST_ONLY_GAS_PRICE, + suggested_gas_price, }, command: None } @@ -358,11 +379,11 @@ async fn test_congestion_control_execution_cancellation() { vec![ InputSharedObject::Cancelled( shared_object_1.0, - SequenceNumber::new_congested_with_suggested_gas_price(TEST_ONLY_GAS_PRICE) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) ), InputSharedObject::Cancelled( shared_object_2.0, - SequenceNumber::new_congested_with_suggested_gas_price(TEST_ONLY_GAS_PRICE) + SequenceNumber::new_congested_with_suggested_gas_price(suggested_gas_price) ) ] ); @@ -388,7 +409,7 @@ async fn test_congestion_control_execution_cancellation() { execution_error.unwrap().to_execution_status().0, ExecutionFailureStatus::ExecutionCancelledDueToSharedObjectCongestionV2 { congested_objects: CongestedObjects(vec![shared_object_1.0, shared_object_2.0]), - suggested_gas_price: TEST_ONLY_GAS_PRICE, + suggested_gas_price, } ); assert_eq!(&effects, effects_2.data())