Rework Transaction Priority calculation (#9834)

* Add transaction validity docs.

* Re-work priority calculation.

* Fix tests.

* Update frame/transaction-payment/src/lib.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* cargo +nightly fmt --all

* Fix an obvious mistake :)

* Re-work again.

* Fix test.

* cargo +nightly fmt --all

* Make VirtualTip dependent on the transaction size.

* cargo +nightly fmt --all

* Update frame/transaction-payment/src/lib.rs

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>

* Fix compilation.

* Update bin/node/runtime/src/lib.rs

Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>

Co-authored-by: Alexander Popiak <alexander.popiak@parity.io>
Co-authored-by: Bastian Köcher <bkchr@users.noreply.github.com>
This commit is contained in:
Tomasz Drwięga
2021-10-04 16:25:18 +02:00
committed by GitHub
parent 886c42c5c8
commit 374fb6a921
15 changed files with 225 additions and 105 deletions
+175 -18
View File
@@ -264,6 +264,30 @@ pub mod pallet {
#[pallet::constant]
type TransactionByteFee: Get<BalanceOf<Self>>;
/// A fee mulitplier for `Operational` extrinsics to compute "virtual tip" to boost their
/// `priority`
///
/// This value is multipled by the `final_fee` to obtain a "virtual tip" that is later
/// added to a tip component in regular `priority` calculations.
/// It means that a `Normal` transaction can front-run a similarly-sized `Operational`
/// extrinsic (with no tip), by including a tip value greater than the virtual tip.
///
/// ```rust,ignore
/// // For `Normal`
/// let priority = priority_calc(tip);
///
/// // For `Operational`
/// let virtual_tip = (inclusion_fee + tip) * OperationalFeeMultiplier;
/// let priority = priority_calc(tip + virtual_tip);
/// ```
///
/// Note that since we use `final_fee` the multiplier applies also to the regular `tip`
/// sent with the transaction. So, not only does the transaction get a priority bump based
/// on the `inclusion_fee`, but we also amplify the impact of tips applied to `Operational`
/// transactions.
#[pallet::constant]
type OperationalFeeMultiplier: Get<u8>;
/// Convert a weight value into a deductible fee based on the currency type.
type WeightToFee: WeightToFeePolynomial<Balance = BalanceOf<Self>>;
@@ -525,6 +549,14 @@ where
/// Require the transactor pay for themselves and maybe include a tip to gain additional priority
/// in the queue.
///
/// # Transaction Validity
///
/// This extension sets the `priority` field of `TransactionValidity` depending on the amount
/// of tip being paid per weight unit.
///
/// Operational transactions will receive an additional priority bump, so that they are normally
/// considered before regular transactions.
#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
#[scale_info(skip_type_params(T))]
pub struct ChargeTransactionPayment<T: Config>(#[codec(compact)] BalanceOf<T>);
@@ -566,27 +598,73 @@ where
.map(|i| (fee, i))
}
/// Get an appropriate priority for a transaction with the given length and info.
/// Get an appropriate priority for a transaction with the given `DispatchInfo`, encoded length
/// and user-included tip.
///
/// This will try and optimise the `fee/weight` `fee/length`, whichever is consuming more of the
/// maximum corresponding limit.
/// The priority is based on the amount of `tip` the user is willing to pay per unit of either
/// `weight` or `length`, depending which one is more limitting. For `Operational` extrinsics
/// we add a "virtual tip" to the calculations.
///
/// For example, if a transaction consumed 1/4th of the block length and half of the weight, its
/// final priority is `fee * min(2, 4) = fee * 2`. If it consumed `1/4th` of the block length
/// and the entire block weight `(1/1)`, its priority is `fee * min(1, 4) = fee * 1`. This means
/// that the transaction which consumes more resources (either length or weight) with the same
/// `fee` ends up having lower priority.
fn get_priority(
len: usize,
/// The formula should simply be `tip / bounded_{weight|length}`, but since we are using
/// integer division, we have no guarantees it's going to give results in any reasonable
/// range (might simply end up being zero). Hence we use a scaling factor:
/// `tip * (max_block_{weight|length} / bounded_{weight|length})`, since given current
/// state of-the-art blockchains, number of per-block transactions is expected to be in a
/// range reasonable enough to not saturate the `Balance` type while multiplying by the tip.
pub fn get_priority(
info: &DispatchInfoOf<T::Call>,
len: usize,
tip: BalanceOf<T>,
final_fee: BalanceOf<T>,
) -> TransactionPriority {
let weight_saturation = T::BlockWeights::get().max_block / info.weight.max(1);
let max_block_length = *T::BlockLength::get().max.get(DispatchClass::Normal);
let len_saturation = max_block_length as u64 / (len as u64).max(1);
let coefficient: BalanceOf<T> =
weight_saturation.min(len_saturation).saturated_into::<BalanceOf<T>>();
final_fee.saturating_mul(coefficient).saturated_into::<TransactionPriority>()
// Calculate how many such extrinsics we could fit into an empty block and take
// the limitting factor.
let max_block_weight = T::BlockWeights::get().max_block;
let max_block_length = *T::BlockLength::get().max.get(info.class) as u64;
let bounded_weight = info.weight.max(1).min(max_block_weight);
let bounded_length = (len as u64).max(1).min(max_block_length);
let max_tx_per_block_weight = max_block_weight / bounded_weight;
let max_tx_per_block_length = max_block_length / bounded_length;
// Given our current knowledge this value is going to be in a reasonable range - i.e.
// less than 10^9 (2^30), so multiplying by the `tip` value is unlikely to overflow the
// balance type. We still use saturating ops obviously, but the point is to end up with some
// `priority` distribution instead of having all transactions saturate the priority.
let max_tx_per_block = max_tx_per_block_length
.min(max_tx_per_block_weight)
.saturated_into::<BalanceOf<T>>();
let max_reward = |val: BalanceOf<T>| val.saturating_mul(max_tx_per_block);
// To distribute no-tip transactions a little bit, we set the minimal tip as `1`.
// This means that given two transactions without a tip, smaller one will be preferred.
let tip = tip.max(1.saturated_into());
let scaled_tip = max_reward(tip);
match info.class {
DispatchClass::Normal => {
// For normal class we simply take the `tip_per_weight`.
scaled_tip
},
DispatchClass::Mandatory => {
// Mandatory extrinsics should be prohibited (e.g. by the [`CheckWeight`]
// extensions), but just to be safe let's return the same priority as `Normal` here.
scaled_tip
},
DispatchClass::Operational => {
// A "virtual tip" value added to an `Operational` extrinsic.
// This value should be kept high enough to allow `Operational` extrinsics
// to get in even during congestion period, but at the same time low
// enough to prevent a possible spam attack by sending invalid operational
// extrinsics which push away regular transactions from the pool.
let fee_multiplier = T::OperationalFeeMultiplier::get().saturated_into();
let virtual_tip = final_fee.saturating_mul(fee_multiplier);
let scaled_virtual_tip = max_reward(virtual_tip);
scaled_tip.saturating_add(scaled_virtual_tip)
},
}
.saturated_into::<TransactionPriority>()
}
}
@@ -629,8 +707,12 @@ where
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
Ok(ValidTransaction { priority: Self::get_priority(len, info, fee), ..Default::default() })
let (final_fee, _) = self.withdraw_fee(who, call, info, len)?;
let tip = self.0;
Ok(ValidTransaction {
priority: Self::get_priority(info, len, tip, final_fee),
..Default::default()
})
}
fn pre_dispatch(
@@ -743,6 +825,7 @@ mod tests {
pub const BlockHashCount: u64 = 250;
pub static TransactionByteFee: u64 = 1;
pub static WeightToFee: u64 = 1;
pub static OperationalFeeMultiplier: u8 = 5;
}
impl frame_system::Config for Runtime {
@@ -822,6 +905,7 @@ mod tests {
impl Config for Runtime {
type OnChargeTransaction = CurrencyAdapter<Balances, DealWithFees>;
type TransactionByteFee = TransactionByteFee;
type OperationalFeeMultiplier = OperationalFeeMultiplier;
type WeightToFee = WeightToFee;
type FeeMultiplierUpdate = ();
}
@@ -1335,6 +1419,79 @@ mod tests {
});
}
#[test]
fn should_alter_operational_priority() {
let tip = 5;
let len = 10;
ExtBuilder::default().balance_factor(100).build().execute_with(|| {
let normal =
DispatchInfo { weight: 100, class: DispatchClass::Normal, pays_fee: Pays::Yes };
let priority = ChargeTransactionPayment::<Runtime>(tip)
.validate(&2, CALL, &normal, len)
.unwrap()
.priority;
assert_eq!(priority, 50);
let priority = ChargeTransactionPayment::<Runtime>(2 * tip)
.validate(&2, CALL, &normal, len)
.unwrap()
.priority;
assert_eq!(priority, 100);
});
ExtBuilder::default().balance_factor(100).build().execute_with(|| {
let op = DispatchInfo {
weight: 100,
class: DispatchClass::Operational,
pays_fee: Pays::Yes,
};
let priority = ChargeTransactionPayment::<Runtime>(tip)
.validate(&2, CALL, &op, len)
.unwrap()
.priority;
assert_eq!(priority, 5800);
let priority = ChargeTransactionPayment::<Runtime>(2 * tip)
.validate(&2, CALL, &op, len)
.unwrap()
.priority;
assert_eq!(priority, 6100);
});
}
#[test]
fn no_tip_has_some_priority() {
let tip = 0;
let len = 10;
ExtBuilder::default().balance_factor(100).build().execute_with(|| {
let normal =
DispatchInfo { weight: 100, class: DispatchClass::Normal, pays_fee: Pays::Yes };
let priority = ChargeTransactionPayment::<Runtime>(tip)
.validate(&2, CALL, &normal, len)
.unwrap()
.priority;
assert_eq!(priority, 10);
});
ExtBuilder::default().balance_factor(100).build().execute_with(|| {
let op = DispatchInfo {
weight: 100,
class: DispatchClass::Operational,
pays_fee: Pays::Yes,
};
let priority = ChargeTransactionPayment::<Runtime>(tip)
.validate(&2, CALL, &op, len)
.unwrap()
.priority;
assert_eq!(priority, 5510);
});
}
#[test]
fn post_info_can_change_pays_fee() {
ExtBuilder::default()