Prioritize transactions in banking stage by their compute unit price (#25178)

* - get prioritization fee from compute_budget instruction;
- update compute_budget::process_instruction function to take instruction iter to support sanitized versioned message;
- updated runtime.md

* update transaction fee calculation for prioritization fee rate as lamports per 10K CUs

* review changes

* fix test

* fix a bpf test

* fix bpf test

* patch feedback

* fix clippy

* fix bpf test

* feedback

* rename prioritization fee rate to compute unit price

* feedback

Co-authored-by: Justin Starry <justin@solana.com>
This commit is contained in:
Tao Zhu 2022-05-15 23:06:33 -05:00 committed by GitHub
parent 52db2e19bc
commit b1b3702e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 435 additions and 171 deletions

View File

@ -89,7 +89,7 @@ pub enum CliCommand {
timeout: Duration,
blockhash: Option<Hash>,
print_timestamp: bool,
prioritization_fee: Option<u64>,
compute_unit_price: Option<u64>,
},
Rent {
data_length: usize,
@ -877,7 +877,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
timeout,
blockhash,
print_timestamp,
prioritization_fee,
compute_unit_price,
} => process_ping(
&rpc_client,
config,
@ -886,7 +886,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
timeout,
blockhash,
*print_timestamp,
prioritization_fee,
compute_unit_price,
),
CliCommand::Rent {
data_length,

View File

@ -271,12 +271,11 @@ impl ClusterQuerySubCommands for App<'_, '_> {
.help("Wait up to timeout seconds for transaction confirmation"),
)
.arg(
Arg::with_name("prioritization-fee")
.long("prioritization-fee")
.alias("additional-fee")
.value_name("NUMBER")
Arg::with_name("compute_unit_price")
.long("compute-unit-price")
.value_name("MICRO-LAMPORTS")
.takes_value(true)
.help("Set prioritization-fee for transaction"),
.help("Set the price in micro-lamports of each transaction compute unit"),
)
.arg(blockhash_arg()),
)
@ -523,7 +522,7 @@ pub fn parse_cluster_ping(
let timeout = Duration::from_secs(value_t_or_exit!(matches, "timeout", u64));
let blockhash = value_of(matches, BLOCKHASH_ARG.name);
let print_timestamp = matches.is_present("print_timestamp");
let prioritization_fee = value_of(matches, "prioritization_fee");
let compute_unit_price = value_of(matches, "compute_unit_price");
Ok(CliCommandInfo {
command: CliCommand::Ping {
interval,
@ -531,7 +530,7 @@ pub fn parse_cluster_ping(
timeout,
blockhash,
print_timestamp,
prioritization_fee,
compute_unit_price,
},
signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
})
@ -1364,7 +1363,7 @@ pub fn process_ping(
timeout: &Duration,
fixed_blockhash: &Option<Hash>,
print_timestamp: bool,
prioritization_fee: &Option<u64>,
compute_unit_price: &Option<u64>,
) -> ProcessResult {
let (signal_sender, signal_receiver) = unbounded();
ctrlc::set_handler(move || {
@ -1409,9 +1408,9 @@ pub fn process_ping(
&to,
lamports,
)];
if let Some(prioritization_fee) = prioritization_fee {
ixs.push(ComputeBudgetInstruction::set_prioritization_fee(
*prioritization_fee,
if let Some(compute_unit_price) = compute_unit_price {
ixs.push(ComputeBudgetInstruction::set_compute_unit_price(
*compute_unit_price,
));
}
Message::new(&ixs, Some(&config.signers[0].pubkey()))
@ -2338,7 +2337,7 @@ mod tests {
Hash::from_str("4CCNp28j6AhGq7PkjPDP4wbQWBS8LLbQin2xV5n8frKX").unwrap()
),
print_timestamp: true,
prioritization_fee: None,
compute_unit_price: None,
},
signers: vec![default_keypair.into()],
}

View File

@ -74,7 +74,7 @@ fn insert_packet_batches(
} else {
build_packet_batch(packet_per_batch_count)
};
let deserialized_packets = deserialize_packets(&packet_batch, &packet_indexes, None);
let deserialized_packets = deserialize_packets(&packet_batch, &packet_indexes);
unprocessed_packet_batches.insert_batch(deserialized_packets);
});
timer.stop();

View File

@ -2110,7 +2110,7 @@ impl BankingStage {
let number_of_dropped_packets = unprocessed_packet_batches.insert_batch(
// Passing `None` for bank for now will make all packet weights 0
unprocessed_packet_batches::deserialize_packets(packet_batch, packet_indexes, None),
unprocessed_packet_batches::deserialize_packets(packet_batch, packet_indexes),
);
saturating_add_assign!(*dropped_packets_count, number_of_dropped_packets);
@ -3236,7 +3236,7 @@ mod tests {
let transaction = system_transaction::transfer(&keypair, &pubkey, 1, blockhash);
let mut p = Packet::from_data(None, &transaction).unwrap();
p.meta.port = packets_id;
DeserializedPacket::new(p, None).unwrap()
DeserializedPacket::new(p).unwrap()
})
.collect_vec();
@ -4022,7 +4022,7 @@ mod tests {
Hash::new_unique(),
);
let packet = Packet::from_data(None, &tx).unwrap();
let deserialized_packet = DeserializedPacket::new(packet, None).unwrap();
let deserialized_packet = DeserializedPacket::new(packet).unwrap();
let genesis_config_info = create_slow_genesis_config(10_000);
let GenesisConfigInfo {
@ -4101,14 +4101,14 @@ mod tests {
let transaction = system_transaction::transfer(&keypair, &pubkey, 1, fwd_block_hash);
let mut packet = Packet::from_data(None, &transaction).unwrap();
packet.meta.flags |= PacketFlags::FORWARDED;
DeserializedPacket::new(packet, None).unwrap()
DeserializedPacket::new(packet).unwrap()
};
let normal_block_hash = Hash::new_unique();
let normal_packet = {
let transaction = system_transaction::transfer(&keypair, &pubkey, 1, normal_block_hash);
let packet = Packet::from_data(None, &transaction).unwrap();
DeserializedPacket::new(packet, None).unwrap()
DeserializedPacket::new(packet).unwrap()
};
let mut unprocessed_packet_batches: UnprocessedPacketBatches =
@ -4229,7 +4229,7 @@ mod tests {
packet_vector
.into_iter()
.map(|p| DeserializedPacket::new(p, None).unwrap())
.map(|p| DeserializedPacket::new(p).unwrap())
.collect()
}

View File

@ -1,7 +1,7 @@
use {
min_max_heap::MinMaxHeap,
solana_perf::packet::{limited_deserialize, Packet, PacketBatch},
solana_runtime::bank::Bank,
solana_program_runtime::compute_budget::ComputeBudget,
solana_sdk::{
hash::Hash,
message::{Message, SanitizedVersionedMessage},
@ -15,7 +15,6 @@ use {
collections::{hash_map::Entry, HashMap},
mem::size_of,
rc::Rc,
sync::Arc,
},
thiserror::Error,
};
@ -31,6 +30,8 @@ pub enum DeserializedPacketError {
SignatureOverflowed(usize),
#[error("packet failed sanitization {0}")]
SanitizeError(#[from] SanitizeError),
#[error("transaction failed prioritization")]
PrioritizationFailure,
}
#[derive(Debug, PartialEq, Eq)]
@ -39,7 +40,7 @@ pub struct ImmutableDeserializedPacket {
transaction: SanitizedVersionedTransaction,
message_hash: Hash,
is_simple_vote: bool,
fee_per_cu: u64,
priority: u64,
}
impl ImmutableDeserializedPacket {
@ -63,8 +64,8 @@ impl ImmutableDeserializedPacket {
self.is_simple_vote
}
pub fn fee_per_cu(&self) -> u64 {
self.fee_per_cu
pub fn priority(&self) -> u64 {
self.priority
}
}
@ -77,22 +78,18 @@ pub struct DeserializedPacket {
}
impl DeserializedPacket {
pub fn new(packet: Packet, bank: Option<&Arc<Bank>>) -> Result<Self, DeserializedPacketError> {
Self::new_internal(packet, bank, None)
pub fn new(packet: Packet) -> Result<Self, DeserializedPacketError> {
Self::new_internal(packet, None)
}
#[cfg(test)]
fn new_with_fee_per_cu(
packet: Packet,
fee_per_cu: u64,
) -> Result<Self, DeserializedPacketError> {
Self::new_internal(packet, None, Some(fee_per_cu))
fn new_with_priority(packet: Packet, priority: u64) -> Result<Self, DeserializedPacketError> {
Self::new_internal(packet, Some(priority))
}
pub fn new_internal(
packet: Packet,
bank: Option<&Arc<Bank>>,
fee_per_cu: Option<u64>,
priority: Option<u64>,
) -> Result<Self, DeserializedPacketError> {
let versioned_transaction: VersionedTransaction =
limited_deserialize(&packet.data[0..packet.meta.size])?;
@ -101,18 +98,18 @@ impl DeserializedPacket {
let message_hash = Message::hash_raw_message(message_bytes);
let is_simple_vote = packet.meta.is_simple_vote_tx();
let fee_per_cu = fee_per_cu.unwrap_or_else(|| {
bank.as_ref()
.map(|bank| compute_fee_per_cu(sanitized_transaction.get_message(), bank))
.unwrap_or(0)
});
// drop transaction if prioritization fails.
let priority = priority
.or_else(|| get_priority(sanitized_transaction.get_message()))
.ok_or(DeserializedPacketError::PrioritizationFailure)?;
Ok(Self {
immutable_section: Rc::new(ImmutableDeserializedPacket {
original_packet: packet,
transaction: sanitized_transaction,
message_hash,
is_simple_vote,
fee_per_cu,
priority,
}),
forwarded: false,
})
@ -133,8 +130,8 @@ impl Ord for DeserializedPacket {
fn cmp(&self, other: &Self) -> Ordering {
match self
.immutable_section()
.fee_per_cu()
.cmp(&other.immutable_section().fee_per_cu())
.priority()
.cmp(&other.immutable_section().priority())
{
Ordering::Equal => self
.immutable_section()
@ -153,7 +150,7 @@ impl PartialOrd for ImmutableDeserializedPacket {
impl Ord for ImmutableDeserializedPacket {
fn cmp(&self, other: &Self) -> Ordering {
match self.fee_per_cu().cmp(&other.fee_per_cu()) {
match self.priority().cmp(&other.priority()) {
Ordering::Equal => self.sender_stake().cmp(&other.sender_stake()),
ordering => ordering,
}
@ -193,8 +190,8 @@ impl UnprocessedPacketBatches {
self.message_hash_to_transaction.clear();
}
/// Insert new `deserizlized_packet_batch` into inner `MinMaxHeap<DeserializedPacket>`,
/// weighted first by the fee-per-cu, then the stake of the sender.
/// Insert new `deserialized_packet_batch` into inner `MinMaxHeap<DeserializedPacket>`,
/// weighted first by the tx priority, then the stake of the sender.
/// If buffer is at the max limit, the lowest weighted packet is dropped
///
/// Returns tuple of number of packets dropped
@ -351,10 +348,9 @@ impl UnprocessedPacketBatches {
pub fn deserialize_packets<'a>(
packet_batch: &'a PacketBatch,
packet_indexes: &'a [usize],
bank: Option<&'a Arc<Bank>>,
) -> impl Iterator<Item = DeserializedPacket> + 'a {
packet_indexes.iter().filter_map(move |packet_index| {
DeserializedPacket::new(packet_batch.packets[*packet_index].clone(), bank).ok()
DeserializedPacket::new(packet_batch.packets[*packet_index].clone()).ok()
})
}
@ -372,9 +368,17 @@ pub fn packet_message(packet: &Packet) -> Result<&[u8], DeserializedPacketError>
.ok_or(DeserializedPacketError::SignatureOverflowed(sig_size))
}
/// Computes `(addition_fee + base_fee / requested_cu)` for `deserialized_packet`
fn compute_fee_per_cu(_message: &SanitizedVersionedMessage, _bank: &Bank) -> u64 {
1
fn get_priority(message: &SanitizedVersionedMessage) -> Option<u64> {
let mut compute_budget = ComputeBudget::default();
let prioritization_fee_details = compute_budget
.process_instructions(
message.program_instructions_iter(),
false, // not request heap size
true, // use default units per instruction
true, // use changed prioritization fee
)
.ok()?;
Some(prioritization_fee_details.get_priority())
}
pub fn transactions_to_deserialized_packets(
@ -384,7 +388,7 @@ pub fn transactions_to_deserialized_packets(
.iter()
.map(|transaction| {
let packet = Packet::from_data(None, transaction)?;
DeserializedPacket::new(packet, None)
DeserializedPacket::new(packet)
})
.collect()
}
@ -409,10 +413,10 @@ mod tests {
if let Some(ip) = ip {
packet.meta.addr = ip;
}
DeserializedPacket::new(packet, None).unwrap()
DeserializedPacket::new(packet).unwrap()
}
fn packet_with_fee_per_cu(fee_per_cu: u64) -> DeserializedPacket {
fn packet_with_priority(priority: u64) -> DeserializedPacket {
let tx = system_transaction::transfer(
&Keypair::new(),
&solana_sdk::pubkey::new_rand(),
@ -420,7 +424,7 @@ mod tests {
Hash::new_unique(),
);
let packet = Packet::from_data(None, &tx).unwrap();
DeserializedPacket::new_with_fee_per_cu(packet, fee_per_cu).unwrap()
DeserializedPacket::new_with_priority(packet, priority).unwrap()
}
#[test]
@ -441,10 +445,10 @@ mod tests {
#[test]
fn test_unprocessed_packet_batches_insert_minimum_packet_over_capacity() {
let heavier_packet_weight = 2;
let heavier_packet = packet_with_fee_per_cu(heavier_packet_weight);
let heavier_packet = packet_with_priority(heavier_packet_weight);
let lesser_packet_weight = heavier_packet_weight - 1;
let lesser_packet = packet_with_fee_per_cu(lesser_packet_weight);
let lesser_packet = packet_with_priority(lesser_packet_weight);
// Test that the heavier packet is actually heavier
let mut unprocessed_packet_batches = UnprocessedPacketBatches::with_capacity(2);

View File

@ -101,11 +101,15 @@ At runtime a program may log how much of the compute budget remains. See
[debugging](developing/on-chain-programs/debugging.md#monitoring-compute-budget-consumption)
for more information.
A transaction may request a specific level of `max_units` it is allowed to
consume by including a
[``ComputeBudgetInstruction`](https://github.com/solana-labs/solana/blob/db32549c00a1b5370fcaf128981ad3323bbd9570/sdk/src/compute_budget.rs#L39).
Transaction prioritization depends on the fee/compute-unit ratio so transaction
should request the minimum amount of compute units required for them to process.
A transaction may set the maximum number of compute units it is allowed to
consume by including a "request units"
[`ComputeBudgetInstruction`](https://github.com/solana-labs/solana/blob/db32549c00a1b5370fcaf128981ad3323bbd9570/sdk/src/compute_budget.rs#L39).
Note that a transaction's prioritization fee is calculated from multiplying the
number of compute units requested by the compute unit price (measured in
micro-lamports) set by the transaction. So transactions should request the
minimum amount of compute units required for execution to minimize fees. Also
note that fees are not adjusted when the number of requested compute units
exceeds the number of compute units consumed by an executed transaction.
Compute Budget instructions don't require any accounts and don't consume any
compute units to process. Transactions can only contain one of each type of
@ -131,14 +135,22 @@ Budget](#compute-budget).
The transaction-wide compute budget applies the `max_units` cap to the entire
transaction rather than to each instruction within the transaction. The default
transaction-wide `max_units` will be calculated as the product of the number of
instructions in the transaction by the default per-instruction units, which is
currently 200k. During processing, the sum of the compute units used by each
instruction in the transaction must not exceed that value. This default value
attempts to retain existing behavior to avoid breaking clients. Transactions can
request a specific number of `max_units` via [Compute Budget](#compute-budget)
instructions. Clients should request only what they need; requesting the
minimum amount of units required to process the transaction will improve their
fee/compute-unit ratio, which transaction prioritization is based on.
instructions in the transaction (excluding [Compute Budget](#compute-budget)
instructions) by the default per-instruction units, which is currently 200k.
During processing, the sum of the compute units used by each instruction in the
transaction must not exceed that value. This default value attempts to retain
existing behavior to avoid breaking clients. Transactions can request a specific
number of `max_units` via [Compute Budget](#compute-budget) instructions.
Clients should request only what they need; requesting the minimum amount of
units required to process the transaction will reduce overall transaction cost,
which may include a prioritization-fee charged for every compute unit.
Transaction prioritization is determined by the transactions prioritization fee
which itself is the product of the transaction's compute unit budget and its
compute unit price (measured in micro-lamports). The compute unit budget and
compute unit fee can be set by adding instructions created by the
`ComputeBudgetInstruction::request_compute_units` and
`ComputeBudgetInstruction::set_compute_unit_price` function, respectively.
## New Features

View File

@ -1,10 +1,13 @@
use solana_sdk::{
borsh::try_from_slice_unchecked,
compute_budget::{self, ComputeBudgetInstruction},
entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES,
instruction::InstructionError,
message::SanitizedMessage,
transaction::TransactionError,
use {
crate::prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType},
solana_sdk::{
borsh::try_from_slice_unchecked,
compute_budget::{self, ComputeBudgetInstruction},
entrypoint::HEAP_LENGTH as MIN_HEAP_FRAME_BYTES,
instruction::{CompiledInstruction, InstructionError},
pubkey::Pubkey,
transaction::TransactionError,
},
};
pub const DEFAULT_UNITS: u32 = 200_000;
@ -122,24 +125,21 @@ impl ComputeBudget {
}
}
pub fn process_message(
pub fn process_instructions<'a>(
&mut self,
message: &SanitizedMessage,
instructions: impl Iterator<Item = (&'a Pubkey, &'a CompiledInstruction)>,
requestable_heap_size: bool,
default_units_per_instruction: bool,
prioritization_fee_type_change: bool,
) -> Result<u64, TransactionError> {
let mut num_instructions = message.instructions().len();
support_set_compute_unit_price_ix: bool,
) -> Result<PrioritizationFeeDetails, TransactionError> {
let mut num_non_compute_budget_instructions: usize = 0;
let mut requested_units = None;
let mut requested_heap_size = None;
let mut prioritization_fee = None;
for (i, (program_id, instruction)) in message.program_instructions_iter().enumerate() {
for (i, (program_id, instruction)) in instructions.enumerate() {
if compute_budget::check_id(program_id) {
// don't include request instructions in default max calc
num_instructions = num_instructions.saturating_sub(1);
if prioritization_fee_type_change {
if support_set_compute_unit_price_ix {
let invalid_instruction_data_error = TransactionError::InstructionError(
i as u8,
InstructionError::InvalidInstructionData,
@ -159,7 +159,8 @@ impl ComputeBudget {
return Err(duplicate_instruction_error);
}
requested_units = Some(units as u64);
prioritization_fee = Some(additional_fee as u64);
prioritization_fee =
Some(PrioritizationFeeType::Deprecated(additional_fee as u64));
}
Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => {
if requested_heap_size.is_some() {
@ -173,11 +174,12 @@ impl ComputeBudget {
}
requested_units = Some(units as u64);
}
Ok(ComputeBudgetInstruction::SetPrioritizationFee(fee)) => {
Ok(ComputeBudgetInstruction::SetComputeUnitPrice(micro_lamports)) => {
if prioritization_fee.is_some() {
return Err(duplicate_instruction_error);
}
prioritization_fee = Some(fee);
prioritization_fee =
Some(PrioritizationFeeType::ComputeUnitPrice(micro_lamports));
}
_ => return Err(invalid_instruction_data_error),
}
@ -188,7 +190,8 @@ impl ComputeBudget {
additional_fee,
}) => {
requested_units = Some(units as u64);
prioritization_fee = Some(additional_fee as u64);
prioritization_fee =
Some(PrioritizationFeeType::Deprecated(additional_fee as u64));
}
Ok(ComputeBudgetInstruction::RequestHeapFrame(bytes)) => {
requested_heap_size = Some((bytes, 0));
@ -201,6 +204,10 @@ impl ComputeBudget {
}
}
}
} else {
// only include non-request instructions in default max calc
num_non_compute_budget_instructions =
num_non_compute_budget_instructions.saturating_add(1);
}
}
@ -219,15 +226,21 @@ impl ComputeBudget {
}
self.max_units = if default_units_per_instruction {
requested_units
.or_else(|| Some(num_instructions.saturating_mul(DEFAULT_UNITS as usize) as u64))
requested_units.or_else(|| {
Some(
num_non_compute_budget_instructions.saturating_mul(DEFAULT_UNITS as usize)
as u64,
)
})
} else {
requested_units
}
.unwrap_or(MAX_UNITS as u64)
.min(MAX_UNITS as u64);
Ok(prioritization_fee.unwrap_or(0))
Ok(prioritization_fee
.map(|fee_type| PrioritizationFeeDetails::new(fee_type, self.max_units))
.unwrap_or_default())
}
}
@ -258,7 +271,7 @@ mod tests {
}
macro_rules! test {
( $instructions: expr, $expected_error: expr, $expected_budget: expr, $type_change: expr ) => {
( $instructions: expr, $expected_result: expr, $expected_budget: expr, $type_change: expr ) => {
let payer_keypair = Keypair::new();
let tx = SanitizedTransaction::from_transaction_for_tests(Transaction::new(
&[&payer_keypair],
@ -266,21 +279,26 @@ mod tests {
Hash::default(),
));
let mut compute_budget = ComputeBudget::default();
let result = compute_budget.process_message(&tx.message(), true, true, $type_change);
assert_eq!($expected_error, result);
let result = compute_budget.process_instructions(
tx.message().program_instructions_iter(),
true,
true,
$type_change,
);
assert_eq!($expected_result, result);
assert_eq!(compute_budget, $expected_budget);
};
( $instructions: expr, $expected_error: expr, $expected_budget: expr) => {
test!($instructions, $expected_error, $expected_budget, true);
( $instructions: expr, $expected_result: expr, $expected_budget: expr) => {
test!($instructions, $expected_result, $expected_budget, true);
};
}
#[test]
fn test_process_mesage() {
fn test_process_instructions() {
// Units
test!(
&[],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: 0,
..ComputeBudget::default()
@ -291,7 +309,7 @@ mod tests {
ComputeBudgetInstruction::request_units(1),
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: 1,
..ComputeBudget::default()
@ -302,7 +320,7 @@ mod tests {
ComputeBudgetInstruction::request_units(MAX_UNITS + 1),
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: MAX_UNITS as u64,
..ComputeBudget::default()
@ -313,7 +331,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_units(MAX_UNITS),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: MAX_UNITS as u64,
..ComputeBudget::default()
@ -326,7 +344,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_units(1),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: 1,
..ComputeBudget::default()
@ -340,7 +358,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_units(1), // ignored
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: DEFAULT_UNITS as u64 * 3,
..ComputeBudget::default()
@ -351,7 +369,10 @@ mod tests {
// Prioritization fee
test!(
&[request_units_deprecated(1, 42)],
Ok(42),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::Deprecated(42),
1,
)),
ComputeBudget {
max_units: 1,
..ComputeBudget::default()
@ -362,9 +383,12 @@ mod tests {
test!(
&[
ComputeBudgetInstruction::request_units(1),
ComputeBudgetInstruction::set_prioritization_fee(42)
ComputeBudgetInstruction::set_compute_unit_price(42)
],
Ok(42),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::ComputeUnitPrice(42),
1
)),
ComputeBudget {
max_units: 1,
..ComputeBudget::default()
@ -373,7 +397,10 @@ mod tests {
test!(
&[request_units_deprecated(1, u32::MAX)],
Ok(u32::MAX as u64),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::Deprecated(u32::MAX as u64),
1
)),
ComputeBudget {
max_units: 1,
..ComputeBudget::default()
@ -384,7 +411,7 @@ mod tests {
// HeapFrame
test!(
&[],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: 0,
..ComputeBudget::default()
@ -395,7 +422,7 @@ mod tests {
ComputeBudgetInstruction::request_heap_frame(40 * 1024),
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: DEFAULT_UNITS as u64,
heap_size: Some(40 * 1024),
@ -440,7 +467,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: DEFAULT_UNITS as u64,
heap_size: Some(MAX_HEAP_FRAME_BYTES as usize),
@ -472,7 +499,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
],
Ok(0),
Ok(PrioritizationFeeDetails::default()),
ComputeBudget {
max_units: DEFAULT_UNITS as u64 * 7,
..ComputeBudget::default()
@ -485,9 +512,12 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES),
ComputeBudgetInstruction::request_units(MAX_UNITS),
ComputeBudgetInstruction::set_prioritization_fee(u64::MAX),
ComputeBudgetInstruction::set_compute_unit_price(u64::MAX),
],
Ok(u64::MAX),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::ComputeUnitPrice(u64::MAX),
MAX_UNITS as u64,
)),
ComputeBudget {
max_units: MAX_UNITS as u64,
heap_size: Some(MAX_HEAP_FRAME_BYTES as usize),
@ -500,7 +530,7 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES),
ComputeBudgetInstruction::request_units(MAX_UNITS),
ComputeBudgetInstruction::set_prioritization_fee(u64::MAX),
ComputeBudgetInstruction::set_compute_unit_price(u64::MAX),
],
Err(TransactionError::InstructionError(
0,
@ -515,9 +545,12 @@ mod tests {
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::request_units(1),
ComputeBudgetInstruction::request_heap_frame(MAX_HEAP_FRAME_BYTES),
ComputeBudgetInstruction::set_prioritization_fee(u64::MAX),
ComputeBudgetInstruction::set_compute_unit_price(u64::MAX),
],
Ok(u64::MAX),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::ComputeUnitPrice(u64::MAX),
1
)),
ComputeBudget {
max_units: 1,
heap_size: Some(MAX_HEAP_FRAME_BYTES as usize),
@ -531,7 +564,10 @@ mod tests {
request_units_deprecated(MAX_UNITS, u32::MAX),
ComputeBudgetInstruction::request_heap_frame(MIN_HEAP_FRAME_BYTES as u32),
],
Ok(u32::MAX as u64),
Ok(PrioritizationFeeDetails::new(
PrioritizationFeeType::Deprecated(u32::MAX as u64),
MAX_UNITS as u64,
)),
ComputeBudget {
max_units: MAX_UNITS as u64,
heap_size: Some(MIN_HEAP_FRAME_BYTES as usize),
@ -564,8 +600,8 @@ mod tests {
test!(
&[
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
ComputeBudgetInstruction::set_prioritization_fee(0),
ComputeBudgetInstruction::set_prioritization_fee(u64::MAX),
ComputeBudgetInstruction::set_compute_unit_price(0),
ComputeBudgetInstruction::set_compute_unit_price(u64::MAX),
],
Err(TransactionError::DuplicateInstruction(2)),
ComputeBudget::default()

View File

@ -8,6 +8,7 @@ pub mod invoke_context;
pub mod log_collector;
pub mod neon_evm_program;
pub mod pre_account;
pub mod prioritization_fee;
pub mod stable_log;
pub mod sysvar_cache;
pub mod timings;

View File

@ -0,0 +1,204 @@
/// There are 10^6 micro-lamports in one lamport
const MICRO_LAMPORTS_PER_LAMPORT: u64 = 1_000_000;
type MicroLamports = u128;
pub enum PrioritizationFeeType {
ComputeUnitPrice(u64),
Deprecated(u64),
}
#[derive(Default, Debug, PartialEq)]
pub struct PrioritizationFeeDetails {
fee: u64,
priority: u64,
}
impl PrioritizationFeeDetails {
pub fn new(fee_type: PrioritizationFeeType, max_compute_units: u64) -> Self {
match fee_type {
PrioritizationFeeType::Deprecated(fee) => {
let priority = if max_compute_units == 0 {
0
} else {
let micro_lamport_fee: MicroLamports =
(fee as u128).saturating_mul(MICRO_LAMPORTS_PER_LAMPORT as u128);
let priority = micro_lamport_fee.saturating_div(max_compute_units as u128);
u64::try_from(priority).unwrap_or(u64::MAX)
};
Self { fee, priority }
}
PrioritizationFeeType::ComputeUnitPrice(cu_price) => {
let fee = {
let micro_lamport_fee: MicroLamports =
(cu_price as u128).saturating_mul(max_compute_units as u128);
let fee = micro_lamport_fee
.saturating_add(MICRO_LAMPORTS_PER_LAMPORT.saturating_sub(1) as u128)
.saturating_div(MICRO_LAMPORTS_PER_LAMPORT as u128);
u64::try_from(fee).unwrap_or(u64::MAX)
};
Self {
fee,
priority: cu_price,
}
}
}
}
pub fn get_fee(&self) -> u64 {
self.fee
}
pub fn get_priority(&self) -> u64 {
self.priority
}
}
#[cfg(test)]
mod test {
use super::{PrioritizationFeeDetails as FeeDetails, PrioritizationFeeType as FeeType, *};
#[test]
fn test_new_with_no_fee() {
for compute_units in [0, 1, MICRO_LAMPORTS_PER_LAMPORT, u64::MAX] {
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(0), compute_units),
FeeDetails::default(),
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(0), compute_units),
FeeDetails::default(),
);
}
}
#[test]
fn test_new_with_compute_unit_price() {
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(MICRO_LAMPORTS_PER_LAMPORT - 1), 1),
FeeDetails {
fee: 1,
priority: MICRO_LAMPORTS_PER_LAMPORT - 1,
},
"should round up (<1.0) lamport fee to 1 lamport"
);
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(MICRO_LAMPORTS_PER_LAMPORT), 1),
FeeDetails {
fee: 1,
priority: MICRO_LAMPORTS_PER_LAMPORT,
},
);
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(MICRO_LAMPORTS_PER_LAMPORT + 1), 1),
FeeDetails {
fee: 2,
priority: MICRO_LAMPORTS_PER_LAMPORT + 1,
},
"should round up (>1.0) lamport fee to 2 lamports"
);
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(200), 100_000),
FeeDetails {
fee: 20,
priority: 200,
},
);
assert_eq!(
FeeDetails::new(
FeeType::ComputeUnitPrice(MICRO_LAMPORTS_PER_LAMPORT),
u64::MAX
),
FeeDetails {
fee: u64::MAX,
priority: MICRO_LAMPORTS_PER_LAMPORT,
},
);
assert_eq!(
FeeDetails::new(FeeType::ComputeUnitPrice(u64::MAX), u64::MAX),
FeeDetails {
fee: u64::MAX,
priority: u64::MAX,
},
);
}
#[test]
fn test_new_with_deprecated_fee() {
assert_eq!(
FeeDetails::new(FeeType::Deprecated(1), MICRO_LAMPORTS_PER_LAMPORT / 2 - 1),
FeeDetails {
fee: 1,
priority: 2,
},
"should round down fee rate of (>2.0) to priority value 1"
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(1), MICRO_LAMPORTS_PER_LAMPORT / 2),
FeeDetails {
fee: 1,
priority: 2,
},
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(1), MICRO_LAMPORTS_PER_LAMPORT / 2 + 1),
FeeDetails {
fee: 1,
priority: 1,
},
"should round down fee rate of (<2.0) to priority value 1"
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(1), MICRO_LAMPORTS_PER_LAMPORT),
FeeDetails {
fee: 1,
priority: 1,
},
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(42), 42 * MICRO_LAMPORTS_PER_LAMPORT),
FeeDetails {
fee: 42,
priority: 1,
},
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(420), 42 * MICRO_LAMPORTS_PER_LAMPORT),
FeeDetails {
fee: 420,
priority: 10,
},
);
assert_eq!(
FeeDetails::new(
FeeType::Deprecated(u64::MAX),
2 * MICRO_LAMPORTS_PER_LAMPORT
),
FeeDetails {
fee: u64::MAX,
priority: u64::MAX / 2,
},
);
assert_eq!(
FeeDetails::new(FeeType::Deprecated(u64::MAX), u64::MAX),
FeeDetails {
fee: u64::MAX,
priority: MICRO_LAMPORTS_PER_LAMPORT,
},
);
}
}

View File

@ -3489,7 +3489,7 @@ fn test_program_fees() {
);
let sanitized_message = SanitizedMessage::try_from(message.clone()).unwrap();
let expected_max_fee = Bank::calculate_fee(
let expected_normal_fee = Bank::calculate_fee(
&sanitized_message,
congestion_multiplier,
&fee_structure,
@ -3500,32 +3500,31 @@ fn test_program_fees() {
.send_and_confirm_message(&[&mint_keypair], message)
.unwrap();
let post_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap();
assert_eq!(pre_balance - post_balance, expected_max_fee);
assert_eq!(pre_balance - post_balance, expected_normal_fee);
let pre_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap();
let message = Message::new(
&[
ComputeBudgetInstruction::request_units(100),
ComputeBudgetInstruction::set_prioritization_fee(42),
ComputeBudgetInstruction::set_compute_unit_price(1),
Instruction::new_with_bytes(program_id, &[], vec![]),
],
Some(&mint_keypair.pubkey()),
);
let sanitized_message = SanitizedMessage::try_from(message.clone()).unwrap();
let expected_min_fee = Bank::calculate_fee(
let expected_prioritized_fee = Bank::calculate_fee(
&sanitized_message,
congestion_multiplier,
&fee_structure,
true,
true,
);
assert!(expected_min_fee < expected_max_fee);
assert!(expected_normal_fee < expected_prioritized_fee);
bank_client
.send_and_confirm_message(&[&mint_keypair], message)
.unwrap();
let post_balance = bank_client.get_balance(&mint_keypair.pubkey()).unwrap();
assert_eq!(pre_balance - post_balance, expected_min_fee);
assert_eq!(pre_balance - post_balance, expected_prioritized_fee);
}
#[test]

View File

@ -31,7 +31,7 @@ use {
account_utils::StateMut,
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
clock::{BankId, Slot, INITIAL_RENT_EPOCH},
feature_set::{self, prioritization_fee_type_change, tx_wide_compute_cap, FeatureSet},
feature_set::{self, add_set_compute_unit_price_ix, tx_wide_compute_cap, FeatureSet},
fee::FeeStructure,
genesis_config::ClusterType,
hash::Hash,
@ -525,7 +525,7 @@ impl Accounts {
lamports_per_signature,
fee_structure,
feature_set.is_active(&tx_wide_compute_cap::id()),
feature_set.is_active(&prioritization_fee_type_change::id()),
feature_set.is_active(&add_set_compute_unit_price_ix::id()),
)
} else {
return (Err(TransactionError::BlockhashNotFound), None);

View File

@ -107,8 +107,9 @@ use {
epoch_schedule::EpochSchedule,
feature,
feature_set::{
self, default_units_per_instruction, disable_fee_calculator, nonce_must_be_writable,
prioritization_fee_type_change, requestable_heap_size, tx_wide_compute_cap, FeatureSet,
self, add_set_compute_unit_price_ix, default_units_per_instruction,
disable_fee_calculator, nonce_must_be_writable, requestable_heap_size,
tx_wide_compute_cap, FeatureSet,
},
fee::FeeStructure,
fee_calculator::{FeeCalculator, FeeRateGovernor},
@ -3638,7 +3639,7 @@ impl Bank {
&self.fee_structure,
self.feature_set.is_active(&tx_wide_compute_cap::id()),
self.feature_set
.is_active(&prioritization_fee_type_change::id()),
.is_active(&add_set_compute_unit_price_ix::id()),
))
}
@ -3653,7 +3654,7 @@ impl Bank {
&self.fee_structure,
self.feature_set.is_active(&tx_wide_compute_cap::id()),
self.feature_set
.is_active(&prioritization_fee_type_change::id()),
.is_active(&add_set_compute_unit_price_ix::id()),
)
}
@ -4389,11 +4390,11 @@ impl Bank {
if tx_wide_compute_cap {
let mut compute_budget_process_transaction_time =
Measure::start("compute_budget_process_transaction_time");
let process_transaction_result = compute_budget.process_message(
tx.message(),
let process_transaction_result = compute_budget.process_instructions(
tx.message().program_instructions_iter(),
feature_set.is_active(&requestable_heap_size::id()),
feature_set.is_active(&default_units_per_instruction::id()),
feature_set.is_active(&prioritization_fee_type_change::id()),
feature_set.is_active(&add_set_compute_unit_price_ix::id()),
);
compute_budget_process_transaction_time.stop();
saturating_add_assign!(
@ -4609,7 +4610,7 @@ impl Bank {
lamports_per_signature: u64,
fee_structure: &FeeStructure,
tx_wide_compute_cap: bool,
prioritization_fee_type_change: bool,
support_set_compute_unit_price_ix: bool,
) -> u64 {
if tx_wide_compute_cap {
// Fee based on compute units and signatures
@ -4622,9 +4623,15 @@ impl Bank {
};
let mut compute_budget = ComputeBudget::default();
let prioritization_fee = compute_budget
.process_message(message, false, false, prioritization_fee_type_change)
let prioritization_fee_details = compute_budget
.process_instructions(
message.program_instructions_iter(),
false,
false,
support_set_compute_unit_price_ix,
)
.unwrap_or_default();
let prioritization_fee = prioritization_fee_details.get_fee();
let signature_fee = Self::get_num_signatures_in_message(message)
.saturating_mul(fee_structure.lamports_per_signature);
let write_lock_fee = Self::get_num_write_locks_in_message(message)
@ -4691,7 +4698,7 @@ impl Bank {
&self.fee_structure,
self.feature_set.is_active(&tx_wide_compute_cap::id()),
self.feature_set
.is_active(&prioritization_fee_type_change::id()),
.is_active(&add_set_compute_unit_price_ix::id()),
);
// In case of instruction error, even though no accounts
@ -7242,7 +7249,11 @@ pub(crate) mod tests {
status_cache::MAX_CACHE_ENTRIES,
},
crossbeam_channel::{bounded, unbounded},
solana_program_runtime::invoke_context::InvokeContext,
solana_program_runtime::{
compute_budget::MAX_UNITS,
invoke_context::InvokeContext,
prioritization_fee::{PrioritizationFeeDetails, PrioritizationFeeType},
},
solana_sdk::{
account::Account,
bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable,
@ -16765,26 +16776,19 @@ pub(crate) mod tests {
// Explicit fee schedule
let expected_fee_structure = &[
// (units requested, fee in SOL),
(0, 0.0),
(5_000, 0.0),
(10_000, 0.0),
(100_000, 0.0),
(300_000, 0.0),
(500_000, 0.0),
(700_000, 0.0),
(900_000, 0.0),
(1_100_000, 0.0),
(1_300_000, 0.0),
(1_500_000, 0.0), // ComputeBudget capped
];
for pair in expected_fee_structure.iter() {
const PRIORITIZATION_FEE: u64 = 42;
for requested_compute_units in [
0, 5_000, 10_000, 100_000, 300_000, 500_000, 700_000, 900_000, 1_100_000, 1_300_000,
MAX_UNITS,
] {
const PRIORITIZATION_FEE_RATE: u64 = 42;
let prioritization_fee_details = PrioritizationFeeDetails::new(
PrioritizationFeeType::ComputeUnitPrice(PRIORITIZATION_FEE_RATE),
requested_compute_units as u64,
);
let message = SanitizedMessage::try_from(Message::new(
&[
ComputeBudgetInstruction::request_units(pair.0),
ComputeBudgetInstruction::set_prioritization_fee(PRIORITIZATION_FEE),
ComputeBudgetInstruction::request_units(requested_compute_units),
ComputeBudgetInstruction::set_compute_unit_price(PRIORITIZATION_FEE_RATE),
Instruction::new_with_bincode(Pubkey::new_unique(), &0, vec![]),
],
Some(&Pubkey::new_unique()),
@ -16793,7 +16797,7 @@ pub(crate) mod tests {
let fee = Bank::calculate_fee(&message, 1, &fee_structure, true, true);
assert_eq!(
fee,
sol_to_lamports(pair.1) + lamports_per_signature + PRIORITIZATION_FEE
lamports_per_signature + prioritization_fee_details.get_fee()
);
}
}

View File

@ -35,9 +35,9 @@ pub enum ComputeBudgetInstruction {
/// Request a specific maximum number of compute units the transaction is
/// allowed to consume and an additional fee to pay.
RequestUnits(u32),
/// Additional fee in lamports to charge the payer, used for transaction
/// prioritization
SetPrioritizationFee(u64),
/// Set a compute unit price in "micro-lamports" to pay a higher transaction
/// fee for higher transaction prioritization.
SetComputeUnitPrice(u64),
}
impl ComputeBudgetInstruction {
@ -51,8 +51,8 @@ impl ComputeBudgetInstruction {
Instruction::new_with_borsh(id(), &Self::RequestUnits(units), vec![])
}
/// Create a `ComputeBudgetInstruction::SetPrioritizationFee` `Instruction`
pub fn set_prioritization_fee(fee: u64) -> Instruction {
Instruction::new_with_borsh(id(), &Self::SetPrioritizationFee(fee), vec![])
/// Create a `ComputeBudgetInstruction::SetComputeUnitPrice` `Instruction`
pub fn set_compute_unit_price(micro_lamports: u64) -> Instruction {
Instruction::new_with_borsh(id(), &Self::SetComputeUnitPrice(micro_lamports), vec![])
}
}

View File

@ -396,10 +396,14 @@ pub mod stake_raise_minimum_delegation_to_1_sol {
solana_sdk::declare_id!("4xmyBuR2VCXzy9H6qYpH9ckfgnTuMDQFPFBfTs4eBCY1");
}
pub mod prioritization_fee_type_change {
pub mod add_set_compute_unit_price_ix {
solana_sdk::declare_id!("98std1NSHqXi9WYvFShfVepRdCoq1qvsp8fsR2XZtG8g");
}
pub mod disable_deploy_of_alloc_free_syscall {
solana_sdk::declare_id!("79HWsX9rpnnJBPcdNURVqygpMAfxdrAirzAGAVmf92im");
}
lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -492,7 +496,8 @@ lazy_static! {
(stake_allow_zero_undelegated_amount::id(), "Allow zero-lamport undelegated amount for initialized stakes #24670"),
(require_static_program_ids_in_transaction::id(), "require static program ids in versioned transactions"),
(stake_raise_minimum_delegation_to_1_sol::id(), "Raise minimum stake delegation to 1.0 SOL #24357"),
(prioritization_fee_type_change::id(), "Switch compute budget to prioritization fee"),
(add_set_compute_unit_price_ix::id(), "add compute budget ix for setting a compute unit price"),
(disable_deploy_of_alloc_free_syscall::id(), "disable new deployments of deprecated sol_alloc_free_ syscall"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()