Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduler - prioritization fees/cost #34888

Merged
merged 4 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ impl PrioGraphScheduler {
saturating_add_assign!(num_scheduled, 1);

let sanitized_transaction_ttl = transaction_state.transition_to_pending();
let cost = transaction_state.transaction_cost().sum();
let cost = transaction_state.cost();

let SanitizedTransactionTTL {
transaction,
Expand Down Expand Up @@ -490,12 +490,9 @@ mod tests {
crate::banking_stage::consumer::TARGET_NUM_TRANSACTIONS_PER_BATCH,
crossbeam_channel::{unbounded, Receiver},
itertools::Itertools,
solana_cost_model::cost_model::CostModel,
solana_runtime::compute_budget_details::ComputeBudgetDetails,
solana_sdk::{
compute_budget::ComputeBudgetInstruction, feature_set::FeatureSet, hash::Hash,
message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer,
system_instruction, transaction::Transaction,
compute_budget::ComputeBudgetInstruction, hash::Hash, message::Message, pubkey::Pubkey,
signature::Keypair, signer::Signer, system_instruction, transaction::Transaction,
},
std::borrow::Borrow,
};
Expand Down Expand Up @@ -572,19 +569,16 @@ mod tests {
lamports,
compute_unit_price,
);
let transaction_cost = CostModel::calculate_cost(&transaction, &FeatureSet::default());
let transaction_ttl = SanitizedTransactionTTL {
transaction,
max_age_slot: Slot::MAX,
};
const TEST_TRANSACTION_COST: u64 = 5000;
container.insert_new_transaction(
id,
transaction_ttl,
ComputeBudgetDetails {
compute_unit_price,
compute_unit_limit: 1,
},
transaction_cost,
compute_unit_price,
TEST_TRANSACTION_COST,
);
}

Expand Down
103 changes: 73 additions & 30 deletions core/src/banking_stage/transaction_scheduler/scheduler_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ use {
itertools::MinMaxResult,
solana_cost_model::cost_model::CostModel,
solana_measure::measure_us,
solana_program_runtime::compute_budget_processor::process_compute_budget_instructions,
solana_runtime::{bank::Bank, bank_forks::BankForks},
solana_sdk::{
clock::MAX_PROCESSING_AGE, saturating_add_assign, timing::AtomicInterval,
transaction::SanitizedTransaction,
clock::MAX_PROCESSING_AGE,
feature_set::include_loaded_accounts_data_size_in_fee_calculation, fee::FeeBudgetLimits,
saturating_add_assign, timing::AtomicInterval, transaction::SanitizedTransaction,
},
solana_svm::transaction_error_metrics::TransactionErrorMetrics,
std::{
Expand Down Expand Up @@ -100,7 +102,7 @@ impl SchedulerController {
// Reset intervals when appropriate, regardless of report.
let should_report = self.count_metrics.has_data();
self.count_metrics
.update_prioritization_stats(self.container.get_min_max_prioritization_fees());
.update_priority_stats(self.container.get_min_max_priority());
self.count_metrics.maybe_report_and_reset(should_report);
self.timing_metrics.maybe_report_and_reset(should_report);
self.worker_metrics
Expand Down Expand Up @@ -311,21 +313,24 @@ impl SchedulerController {
let mut error_counts = TransactionErrorMetrics::default();
for chunk in packets.chunks(CHUNK_SIZE) {
let mut post_sanitization_count: usize = 0;
let (transactions, compute_budget_details): (Vec<_>, Vec<_>) = chunk
let (transactions, fee_budget_limits_vec): (Vec<_>, Vec<_>) = chunk
.iter()
.filter_map(|packet| {
packet
.build_sanitized_transaction(feature_set, vote_only, bank.as_ref())
.map(|tx| (tx, packet.compute_budget_details()))
packet.build_sanitized_transaction(feature_set, vote_only, bank.as_ref())
})
.inspect(|_| saturating_add_assign!(post_sanitization_count, 1))
.filter(|(tx, _)| {
.filter(|tx| {
SanitizedTransaction::validate_account_locks(
tx.message(),
transaction_account_lock_limit,
)
.is_ok()
})
.filter_map(|tx| {
process_compute_budget_instructions(tx.message().program_instructions_iter())
.map(|compute_budget| (tx, compute_budget.into()))
.ok()
})
.unzip();

let check_results = bank.check_transactions(
Expand All @@ -337,16 +342,17 @@ impl SchedulerController {
let post_lock_validation_count = transactions.len();

let mut post_transaction_check_count: usize = 0;
for ((transaction, compute_budget_details), _) in transactions
for ((transaction, fee_budget_limits), _) in transactions
.into_iter()
.zip(compute_budget_details)
.zip(fee_budget_limits_vec)
.zip(check_results)
.filter(|(_, check_result)| check_result.0.is_ok())
{
saturating_add_assign!(post_transaction_check_count, 1);
let transaction_id = self.transaction_id_generator.next();

let transaction_cost = CostModel::calculate_cost(&transaction, &bank.feature_set);
let (priority, cost) =
Self::calculate_priority_and_cost(&transaction, &fee_budget_limits, &bank);
let transaction_ttl = SanitizedTransactionTTL {
transaction,
max_age_slot: last_slot_in_epoch,
Expand All @@ -355,8 +361,8 @@ impl SchedulerController {
if self.container.insert_new_transaction(
transaction_id,
transaction_ttl,
compute_budget_details,
transaction_cost,
priority,
cost,
) {
saturating_add_assign!(self.count_metrics.num_dropped_on_capacity, 1);
}
Expand Down Expand Up @@ -384,6 +390,51 @@ impl SchedulerController {
);
}
}

/// Calculate priority and cost for a transaction:
///
/// Cost is calculated through the `CostModel`,
/// and priority is calculated through a formula here that attempts to sell
/// blockspace to the highest bidder.
///
/// The priority is calculated as:
/// P = R / (1 + C)
/// where P is the priority, R is the reward,
/// and C is the cost towards block-limits.
///
/// Current minimum costs are on the order of several hundred,
/// so the denominator is effectively C, and the +1 is simply
/// to avoid any division by zero due to a bug - these costs
/// are calculated by the cost-model and are not direct
/// from user input. They should never be zero.
/// Any difference in the prioritization is negligible for
/// the current transaction costs.
ryoqun marked this conversation as resolved.
Show resolved Hide resolved
fn calculate_priority_and_cost(
transaction: &SanitizedTransaction,
fee_budget_limits: &FeeBudgetLimits,
bank: &Bank,
) -> (u64, u64) {
let cost = CostModel::calculate_cost(transaction, &bank.feature_set).sum();
let fee = bank.fee_structure.calculate_fee(
transaction.message(),
5_000, // this just needs to be non-zero
fee_budget_limits,
bank.feature_set
.is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()),
);

// We need a multiplier here to avoid rounding down too aggressively.
// For many transactions, the cost will be greater than the fees in terms of raw lamports.
// For the purposes of calculating prioritization, we multiply the fees by a large number so that
// the cost is a small fraction.
// An offset of 1 is used in the denominator to explicitly avoid division by zero.
const MULTIPLIER: u64 = 1_000_000;
(
fee.saturating_mul(MULTIPLIER)
.saturating_div(cost.saturating_add(1)),
cost,
)
}
}

#[derive(Default)]
Expand Down Expand Up @@ -475,16 +526,8 @@ impl SchedulerCountMetrics {
i64
),
("num_dropped_on_capacity", self.num_dropped_on_capacity, i64),
(
"min_prioritization_fees",
self.get_min_prioritization_fees(),
i64
),
(
"max_prioritization_fees",
self.get_max_prioritization_fees(),
i64
)
("min_priority", self.get_min_priority(), i64),
("max_priority", self.get_max_priority(), i64)
);
}

Expand Down Expand Up @@ -524,8 +567,8 @@ impl SchedulerCountMetrics {
self.max_prioritization_fees = 0;
}

pub fn update_prioritization_stats(&mut self, min_max_fees: MinMaxResult<u64>) {
// update min/max priotization fees
pub fn update_priority_stats(&mut self, min_max_fees: MinMaxResult<u64>) {
// update min/max priority
match min_max_fees {
itertools::MinMaxResult::NoElements => {
// do nothing
Expand All @@ -541,7 +584,7 @@ impl SchedulerCountMetrics {
}
}

pub fn get_min_prioritization_fees(&self) -> u64 {
pub fn get_min_priority(&self) -> u64 {
// to avoid getting u64::max recorded by metrics / in case of edge cases
if self.min_prioritization_fees != u64::MAX {
self.min_prioritization_fees
Expand All @@ -550,7 +593,7 @@ impl SchedulerCountMetrics {
}
}

pub fn get_max_prioritization_fees(&self) -> u64 {
pub fn get_max_priority(&self) -> u64 {
self.max_prioritization_fees
}
}
Expand Down Expand Up @@ -728,7 +771,7 @@ mod tests {
from_keypair: &Keypair,
to_pubkey: &Pubkey,
lamports: u64,
priority: u64,
compute_unit_price: u64,
recent_blockhash: Hash,
) -> Transaction {
// Fund the sending key, so that the transaction does not get filtered by the fee-payer check.
Expand All @@ -743,7 +786,7 @@ mod tests {
}

let transfer = system_instruction::transfer(&from_keypair.pubkey(), to_pubkey, lamports);
let prioritization = ComputeBudgetInstruction::set_compute_unit_price(priority);
let prioritization = ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price);
let message = Message::new(&[transfer, prioritization], Some(&from_keypair.pubkey()));
Transaction::new(&vec![from_keypair], message, recent_blockhash)
}
Expand Down Expand Up @@ -999,7 +1042,7 @@ mod tests {
&Keypair::new(),
&Pubkey::new_unique(),
1,
i,
i * 10,
bank.last_blockhash(),
)
})
Expand Down
Loading
Loading