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

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

Merged
merged 12 commits into from
May 16, 2022
33 changes: 19 additions & 14 deletions docs/src/developing/programming-model/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,16 @@ 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 overall cost include prioritization-fee for every 10K compute-units,
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 the number of
compute units requested if a transaction also sets a "prioritization fee rate".
The "prioritization fee rate" is measured in lamports per 10K requested compute
units so transactions should request the minimum amount of compute units
required for execution to minimize fees. 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
Expand All @@ -132,14 +136,15 @@ 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 reduce overall
transaction cost, which includes prioritization-fee for every 10K compute-units.
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 includes prioritization-fee for every 10K compute-units.

Prioritization_fee is what transaction prioritization based on, it can be set by
`ComputeBudgetInstruction::set_prioritization_fee` function.
Expand Down
13 changes: 9 additions & 4 deletions program-runtime/src/compute_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ impl ComputeBudget {
default_units_per_instruction: bool,
prioritization_fee_type_change: bool,
) -> Result<PrioritizationFeeDetails, TransactionError> {
let mut num_instructions: usize = 0;
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;
Expand Down Expand Up @@ -205,7 +205,8 @@ impl ComputeBudget {
}
} else {
// only include non-request instructions in default max calc
num_instructions = num_instructions.saturating_add(1);
num_non_compute_budget_instructions =
num_non_compute_budget_instructions.saturating_add(1);
}
}

Expand All @@ -224,8 +225,12 @@ 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
}
Expand Down
126 changes: 76 additions & 50 deletions program-runtime/src/prioritization_fee.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Prioritization fee rate is 1 lamports per 10K CUs
// Prioritization fee rate is measured in lamports per compute unit "ticks"
const COMPUTE_UNIT_TICK_SIZE: u64 = 10_000;

// `COMPUTE_UNIT_TICK_SIZE` tick lamports = 1 lamport
type TickLamports = u128;

pub enum PrioritizationFeeType {
Rate(u64),
Deprecated(u64),
Expand All @@ -14,21 +17,32 @@ pub struct PrioritizationFeeDetails {

impl PrioritizationFeeDetails {
pub fn new(fee_type: PrioritizationFeeType, max_compute_units: u64) -> Self {
let mut compute_ticks = max_compute_units
.saturating_div(COMPUTE_UNIT_TICK_SIZE)
.max(1);
match fee_type {
PrioritizationFeeType::Deprecated(fee) => Self {
fee,
priority: fee.saturating_div(compute_ticks),
},
PrioritizationFeeType::Deprecated(fee) => {
let priority = if max_compute_units == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got me thinking about the need for a minimum threshold that we can use to filter all of these from the buffer, i.e. return an error if less than this minimum

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking about that as well. I think that de-prioritizing those transactions is probably ok for now. But seems useful to have a min on the request units instructions as well.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got me thinking about the need for a minimum threshold that we can use to filter all of these from the buffer, i.e. return an error if less than this minimum

There might be some important lessons in EIP1559's github issue

"It is recommended that transactions with the same priority fee be sorted by time the transaction was received to protect the network from spamming attacks where the attacker throws a bunch of transactions into the pending pool in order to ensure that at least one lands in a favorable position."

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't exactly follow how that Ethereum issue translates to Solana, can you create a new issue and explain a bit more what you're suggesting?

0
} else {
let tick_lamport_fee: TickLamports =
(fee as u128).saturating_mul(COMPUTE_UNIT_TICK_SIZE as u128);
let priority = tick_lamport_fee.saturating_div(max_compute_units as u128);
u64::try_from(priority).unwrap_or(u64::MAX)
};

Self { fee, priority }
}
PrioritizationFeeType::Rate(fee_rate) => {
if compute_ticks.saturating_mul(COMPUTE_UNIT_TICK_SIZE) < max_compute_units {
compute_ticks = compute_ticks.saturating_add(1);
}
let fee = {
let tick_lamport_fee: TickLamports =
(fee_rate as u128).saturating_mul(max_compute_units as u128);
let mut fee = tick_lamport_fee.saturating_div(COMPUTE_UNIT_TICK_SIZE as u128);
if fee.saturating_mul(COMPUTE_UNIT_TICK_SIZE as u128) < tick_lamport_fee {
fee = fee.saturating_add(1);
}
u64::try_from(fee).unwrap_or(u64::MAX)
};

Self {
fee: fee_rate.saturating_mul(compute_ticks),
fee,
priority: fee_rate,
}
}
Expand Down Expand Up @@ -62,51 +76,53 @@ mod test {
}
}

#[test]
fn test_new_with_one_compute_tick() {
for compute_units in [
0, // zero compute units should be rounded up to 1 tick
1, // compute units are rounded up to the nearest tick
COMPUTE_UNIT_TICK_SIZE,
] {
for fee_type_value in [0, 1, 100, u64::MAX] {
let expected_details = FeeDetails {
fee: fee_type_value,
priority: fee_type_value,
};
assert_eq!(
FeeDetails::new(FeeType::Rate(fee_type_value), compute_units),
expected_details,
);

assert_eq!(
FeeDetails::new(FeeType::Deprecated(fee_type_value), compute_units),
expected_details,
);
}
}
}

#[test]
fn test_new_with_fee_rate() {
assert!(COMPUTE_UNIT_TICK_SIZE % 2 == 0);
assert_eq!(
FeeDetails::new(FeeType::Rate(2), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails::new(FeeType::Rate(2), COMPUTE_UNIT_TICK_SIZE / 2 - 1),
FeeDetails {
fee: 1,
priority: 2,
},
"should round up 2 * (<0.5) lamport fee to 1 lamport"
);

assert_eq!(
FeeDetails::new(FeeType::Rate(2), COMPUTE_UNIT_TICK_SIZE / 2),
FeeDetails {
fee: 2 * 42,
fee: 1,
priority: 2,
},
);

assert_eq!(
FeeDetails::new(FeeType::Rate(2), 42 * COMPUTE_UNIT_TICK_SIZE - 1),
FeeDetails::new(FeeType::Rate(2), COMPUTE_UNIT_TICK_SIZE / 2 + 1),
FeeDetails {
fee: 2 * 42,
fee: 2,
priority: 2,
},
"should round up 2 * (>0.5) lamport fee to 2 lamports"
);

assert_eq!(
FeeDetails::new(FeeType::Rate(u64::MAX), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails::new(FeeType::Rate(2), COMPUTE_UNIT_TICK_SIZE),
FeeDetails {
fee: 2,
priority: 2,
},
);

assert_eq!(
FeeDetails::new(FeeType::Rate(2), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails {
fee: 42 * 2,
priority: 2,
},
);

assert_eq!(
FeeDetails::new(FeeType::Rate(u64::MAX), COMPUTE_UNIT_TICK_SIZE),
FeeDetails {
fee: u64::MAX,
priority: u64::MAX,
Expand All @@ -125,31 +141,41 @@ mod test {
#[test]
fn test_new_with_deprecated_fee() {
assert_eq!(
FeeDetails::new(FeeType::Deprecated(2), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails::new(FeeType::Deprecated(1), COMPUTE_UNIT_TICK_SIZE / 2 - 1),
FeeDetails {
fee: 2,
priority: 0,
fee: 1,
priority: 2,
},
"should round down fee rate of (1 / (<0.5 compute ticks)) to priority value 2"
);

assert_eq!(
FeeDetails::new(FeeType::Deprecated(42), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails::new(FeeType::Deprecated(1), COMPUTE_UNIT_TICK_SIZE / 2),
FeeDetails {
fee: 42,
fee: 1,
priority: 2,
},
);

assert_eq!(
FeeDetails::new(FeeType::Deprecated(1), COMPUTE_UNIT_TICK_SIZE / 2 + 1),
FeeDetails {
fee: 1,
priority: 1,
},
"should round down fee rate of (1 / (>0.5 compute ticks)) to priority value 1"
);

assert_eq!(
FeeDetails::new(FeeType::Deprecated(42), 42 * COMPUTE_UNIT_TICK_SIZE - 1),
FeeDetails::new(FeeType::Deprecated(1), COMPUTE_UNIT_TICK_SIZE),
FeeDetails {
fee: 42,
fee: 1,
priority: 1,
},
);

assert_eq!(
FeeDetails::new(FeeType::Deprecated(42), 42 * COMPUTE_UNIT_TICK_SIZE + 1),
FeeDetails::new(FeeType::Deprecated(42), 42 * COMPUTE_UNIT_TICK_SIZE),
FeeDetails {
fee: 42,
priority: 1,
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/compute_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ 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),
/// Set a prioritization fee rate in "lamports per 10K CUs" to charge the payer,
/// used for transaction prioritization
/// Set a prioritization fee rate measured in "lamports per 10K CUs" to
jstarry marked this conversation as resolved.
Show resolved Hide resolved
/// pay a higher transaction fee for higher transaction prioritization.
SetPrioritizationFeeRate(u64),
}

Expand Down