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

Update transaction-payment #1682

Merged
merged 6 commits into from
Dec 15, 2021
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
139 changes: 116 additions & 23 deletions modules/transaction-payment/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ use primitives::{Balance, CurrencyId, ReserveIdentifier};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{
Bounded, CheckedSub, Convert, DispatchInfoOf, One, PostDispatchInfoOf, SaturatedConversion, Saturating,
SignedExtension, UniqueSaturatedInto, Zero,
Bounded, CheckedDiv, CheckedSub, Convert, DispatchInfoOf, One, PostDispatchInfoOf, SaturatedConversion,
Saturating, SignedExtension, UniqueSaturatedInto, Zero,
},
transaction_validity::{
InvalidTransaction, TransactionPriority, TransactionValidity, TransactionValidityError, ValidTransaction,
Expand Down Expand Up @@ -248,6 +248,39 @@ pub mod module {
#[pallet::constant]
type TransactionByteFee: Get<PalletBalanceOf<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<u64>;

/// The step amount of tips required to effect transaction priority.
#[pallet::constant]
type TipPerWeightStep: Get<PalletBalanceOf<Self>>;

/// The maximum value of tips that affect the priority.
/// Set the maximum value of tips to prevent affecting the unsigned extrinsic.
#[pallet::constant]
type MaxTipsOfPriority: Get<PalletBalanceOf<Self>>;

/// Convert a weight value into a deductible fee based on the currency
/// type.
type WeightToFee: WeightToFeePolynomial<Balance = PalletBalanceOf<Self>>;
Expand Down Expand Up @@ -652,6 +685,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 + Send + Sync>(#[codec(compact)] pub PalletBalanceOf<T>);
Expand Down Expand Up @@ -707,32 +748,83 @@ where
}
}

/// 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.
/// 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.
fn get_priority(
len: usize,
info: &DispatchInfoOf<<T as frame_system::Config>::Call>,
len: usize,
tip: PalletBalanceOf<T>,
final_fee: PalletBalanceOf<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: PalletBalanceOf<T> = weight_saturation
.min(len_saturation)
// 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::<PalletBalanceOf<T>>();
final_fee
.saturating_mul(coefficient)
.saturated_into::<TransactionPriority>()
// tipPerWeight = tipPerWight / TipPerWeightStep * TipPerWeightStep
// = tip / bounded_{weight|length} / TipPerWeightStep * TipPerWeightStep
// priority = tipPerWeight * max_block_{weight|length}
// MaxTipsOfPriority = 10_000 KAR/ACA = 10^16.
// `MaxTipsOfPriority * max_block_{weight|length}` will overflow, so div `TipPerWeightStep` here.
let max_reward = |val: PalletBalanceOf<T>| {
val.checked_div(&T::TipPerWeightStep::get())
.expect("TipPerWeightStep is non-zero; qed")
.saturating_mul(max_tx_per_block)
};

// To distribute no-tip transactions a little bit, we increase the tip value by one.
// This means that given two transactions without a tip, smaller one will be preferred.
// Set the maximum value of tips to prevent affecting the unsigned extrinsic.
let tip = tip.saturating_add(One::one()).min(T::MaxTipsOfPriority::get());
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>()
}
}

Expand Down Expand Up @@ -763,9 +855,10 @@ where
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> TransactionValidity {
let (fee, _) = self.withdraw_fee(who, call, info, len)?;
let (final_fee, _) = self.withdraw_fee(who, call, info, len)?;
let tip = self.0;
Ok(ValidTransaction {
priority: Self::get_priority(len, info, fee),
priority: Self::get_priority(info, len, tip, final_fee),
..Default::default()
})
}
Expand Down
13 changes: 13 additions & 0 deletions modules/transaction-payment/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ impl module_dex::Config for Runtime {
parameter_types! {
pub MaxSwapSlippageCompareToOracle: Ratio = Ratio::saturating_from_rational(1, 2);
pub static TransactionByteFee: u128 = 1;
pub OperationalFeeMultiplier: u64 = 5;
pub static TipPerWeightStep: u128 = 1;
pub MaxTipsOfPriority: u128 = 1000;
pub DefaultFeeSwapPathList: Vec<Vec<CurrencyId>> = vec![vec![AUSD, ACA], vec![DOT, AUSD, ACA]];
}

Expand Down Expand Up @@ -226,6 +229,9 @@ impl Config for Runtime {
type MultiCurrency = Currencies;
type OnTransactionPayment = DealWithFees;
type TransactionByteFee = TransactionByteFee;
type OperationalFeeMultiplier = OperationalFeeMultiplier;
type TipPerWeightStep = TipPerWeightStep;
type MaxTipsOfPriority = MaxTipsOfPriority;
type WeightToFee = WeightToFee;
type FeeMultiplierUpdate = ();
type DEX = DEXModule;
Expand Down Expand Up @@ -276,6 +282,7 @@ pub struct ExtBuilder {
base_weight: u64,
byte_fee: u128,
weight_to_fee: u128,
tip_per_weight_step: u128,
native_balances: Vec<(AccountId, Balance)>,
}

Expand All @@ -286,6 +293,7 @@ impl Default for ExtBuilder {
base_weight: 0,
byte_fee: 2,
weight_to_fee: 1,
tip_per_weight_step: 1,
native_balances: vec![],
}
}
Expand All @@ -304,6 +312,10 @@ impl ExtBuilder {
self.weight_to_fee = weight_to_fee;
self
}
pub fn tip_per_weight_step(mut self, tip_per_weight_step: u128) -> Self {
self.tip_per_weight_step = tip_per_weight_step;
self
}
pub fn one_hundred_thousand_for_alice_n_charlie(mut self) -> Self {
self.native_balances = vec![(ALICE, 100000), (CHARLIE, 100000)];
self
Expand All @@ -312,6 +324,7 @@ impl ExtBuilder {
EXTRINSIC_BASE_WEIGHT.with(|v| *v.borrow_mut() = self.base_weight);
TRANSACTION_BYTE_FEE.with(|v| *v.borrow_mut() = self.byte_fee);
WEIGHT_TO_FEE.with(|v| *v.borrow_mut() = self.weight_to_fee);
TIP_PER_WEIGHT_STEP.with(|v| *v.borrow_mut() = self.tip_per_weight_step);
}
pub fn build(self) -> sp_io::TestExternalities {
self.set_constants();
Expand Down
Loading