Weights and Fees
Because the resources available to a blockchain are limited, it’s important to manage how blocks consume them. The resources that need to be managed include:
- Memory usage
- Storage input and output
- Computation
- Transaction and block size
- State database size
Substrate provides block authors with several ways to manage access to resources and to prevent individual components of the chain from consuming too much of any single resource. Two of the most important mechanisms available to block authors are weights and transaction fees.
Weights are used to manage the time it takes to validate a block. In general, weights are used to characterize the time it takes to execute the extrinsic calls in the body of a block. By controlling the execution time that a block can consume, weights set limits on storage input and output and computation.
Weights are not used to restrict access to other resources, such as storage itself or memory footprint. Other mechanisms must be used for this.
Some of the weight allowed for a block is consumed as part of the block's initialization and finalization. The weight might also be used to execute mandatory inherent extrinsic calls. To help ensure blocks don’t consume too much execution time—and prevent malicious users from overloading the system with unnecessary calls—weights are used in combination with transaction fees.
Transaction fees are a key component of making the blockchain economically sustainable and are typically applied to transactions initiated by users and deducted before a transaction request is executed.
How fees are calculated
The final fee for a transaction is calculated using the following parameters:
base fee: This is the minimum amount a user pays for a transaction. It is declared as a base weight in the runtime and converted to a fee using
WeightToFee
.weight fee: A fee proportional to the execution time (input and output and computation) that a transaction consumes.
length fee: A fee proportional to the encoded length of the transaction.
tip: An optional tip to increase the priority of the transaction, giving it a higher chance to be included by the transaction queue.
The base fee and proportional weight and length fees constitute the inclusion fee. The inclusion fee is the minimum fee that must be available for a transaction to be included in a block.
Using the transaction payment pallet
The Transaction Payment pallet provides the basic logic for calculating the inclusion fee.
You can also use the Transaction Payment pallet to:
Convert a weight value into a deductible fee based on a currency type using
Config::WeightToFee
.Update the fee for the next block by defining a multiplier, based on the final state of the chain at the end of the previous block using
Config::FeeMultiplierUpdate
.Manage the withdrawal, refund, and deposit of transaction fees using
Config::OnChargeTransaction
.
You can learn more about these configuration traits in the Transaction Payment documentation.
You should note that transaction fees are withdrawn before the transaction is executed. After the transaction is executed, the transaction weight can be adjusted to reflect the actual resources the transaction used. If a transaction uses fewer resources than expected, the transaction fee is corrected and the adjusted transaction fee is deposited.
A closer look at the inclusion fee
The formula for calculating the final fee looks like this:
inclusion_fee = base_fee + length_fee + [targeted_fee_adjustment * weight_fee];
final_fee = inclusion_fee + tip;
In this formula, the targeted_fee_adjustment
is a multiplier that can tune the final fee based on the congestion of the network.
The
base_fee
derived from the base weight covers inclusion overhead like signature verification.The
length_fee
is a per-byte fee that is multiplied by the length of the encoded extrinsic.The
weight_fee
fee is calculated using two parameters:The
ExtrinsicBaseWeight
that is declared in the runtime and applies to all extrinsics.The
#[pallet::weight]
annotation that accounts for an extrinsic's complexity.
To convert the weight to Currency, the runtime must define a WeightToFee
struct that implements a conversion function, Convert<Weight,Balance>
.
Note that the extrinsic sender is charged the inclusion fee before the extrinsic is invoked. The fee is deducted from the sender's balance even if the transaction fails upon execution.
Accounts with an insufficient balance
If an account does not have a sufficient balance to pay the inclusion fee and remain alive—that is, enough to pay the inclusion fee and maintain the minimum existential deposit—then you should ensure the transaction is cancelled so that no fee is deducted and the transaction does not begin execution.
Substrate does not enforce this rollback behavior. However, this scenario would be a rare occurrence because the transaction queue and block-making logic perform checks to prevent it before adding an extrinsic to a block.
Fee multiplier
The inclusion fee formula always results in the same fee for the same input.
However, weight can be dynamic and—based on how
WeightToFee
is defined—the final fee can include some degree of variability.
To account for this variability, the Transaction Payment pallet provides the FeeMultiplierUpdate
configurable parameter.
The default update function is inspired by the Polkadot network and implements a targeted adjustment in which a target saturation level of block weight is defined. If the previous block is more saturated, then the fees are slightly increased. Similarly, if the previous block has fewer transactions than the target, fees are decreased by a small amount. For more information about fee multiplier adjustments, see the Web3 research page.
Transactions with special requirements
Inclusion fees must be computable prior to execution, and therefore can only represent fixed logic. Some transactions warrant limiting resources with other strategies. For example:
Bonds are a type of fee that might be returned or slashed after some on-chain event.
For example, you might want to require users to place a bond to participate in a vote. The bond might then be returned at the end of the referendum or slashed if the voter attempted malicious behavior.
Deposits are fees that might be returned later.
For example, you might require users to pay a deposit to execute an operation that uses storage. If a subsequent operation frees up storage, the user's deposit could be returned.
Burn operations are used to pay for a transaction based on its internal logic.
For example, a transaction might burn funds from the sender if the transaction creates new storage items to pay for the increased the state size.
Limits enable you to enforce constant or configurable limits on certain operations.
For example, the default Staking pallet only allows nominators to nominate 16 validators to limit the complexity of the validator election process.
It is important to note that if you query the chain for a transaction fee, it only returns the inclusion fee.
Default weight annotations
All dispatchable functions in Substrate must specify a weight. The way of doing that is using the annotation-based system that lets you combine fixed values for database read/write weight and/or fixed values based on benchmarks. The most basic example would look like this:
#[pallet::weight(100_000)]
fn my_dispatchable() {
// ...
}
Please note that the ExtrinsicBaseWeight
is automatically added to the declared weight in order to
account for the costs of simply including an empty extrinsic into a block.
Parameterizing over database accesses
In order to make weight annotations independent of the deployed database backend, they are defined as a constant and then used in the annotations when expressing database accesses performed by the dispatchable:
#[pallet::weight(T::DbWeight::get().reads_writes(1, 2) + 20_000)]
fn my_dispatchable() {
// ...
}
This dispatchable does one database read and two database writes in addition to other things that
add the additional 20,000. A database access is generally every time a value that is declared inside
the #[pallet::storage]
block is accessed. However, only unique accesses are counted because once a
value is accessed it is cached and accessing it again does not result in a database operation. That
is:
- Multiple reads of the same value count as one read.
- Multiple writes of the same value count as one write.
- Multiple reads of the same value, followed by a write to that value, count as one read and one write.
- A write followed by a read only counts as one write.
Dispatch classes
Dispatches are broken into three classes: Normal
, Operational
, and Mandatory
. When not defined
otherwise in the weight annotation, a dispatch is Normal
. The developer can specify that the
dispatchable uses another class like this:
#[pallet::weight(100_000, DispatchClass::Operational)]
fn my_dispatchable() {
// ...
}
This tuple notation also allows specifying a final argument that determines whether or not the user
is charged based on the annotated weight. When not defined otherwise, Pays::Yes
is assumed:
#[pallet::weight(100_000, DispatchClass::Normal, Pays::No)]
fn my_dispatchable() {
// ...
}
Normal dispatches
Dispatches in this class represent normal user-triggered transactions. These types of dispatches may
only consume a portion of a block's total weight limit; this portion can be found by examining the
AvailableBlockRatio
.
Normal dispatches are sent to the transaction pool.
Operational dispatches
As opposed to normal dispatches, which represent usage of network capabilities, operational
dispatches are those that provide network capabilities. These types of dispatches may consume the
entire weight limit of a block, which is to say that they are not bound by the
AvailableBlockRatio
.
Dispatches in this class are given maximum priority and are exempt from paying the length_fee
.
Mandatory dispatches
Mandatory dispatches will be included in a block even if they cause the block to surpass its weight limit. This dispatch class may only be applied to inherents and is intended to represent functions that are part of the block validation process. Since these kinds of dispatches are always included in a block regardless of the function weight, it is critical that the function's validation process prevents malicious validators from abusing the function in order to craft blocks that are valid but impossibly heavy. This can typically be accomplished by ensuring that the operation is always very light and can only be included in a block once. In order to make it more difficult for malicious validators to abuse these types of dispatches, they may not be included in blocks that return errors. This dispatch class exists to serve the assumption that it is better to allow an overweight block to be created than to not allow any block to be created at all.
Dynamic weights
In addition to purely fixed weights and constants, the weight calculation can consider the input arguments of a dispatchable. The weight should be trivially computable from the input arguments with some basic arithmetic:
#[pallet::weight(FunctionOf(
|args: (&Vec<User>,)| args.0.len().saturating_mul(10_000),
DispatchClass::Normal,
Pays::Yes,
))]
fn handle_users(origin, calls: Vec<User>) {
// Do something per user
}
Post dispatch weight correction
Depending on the execution logic, a dispatchable may consume less weight than was prescribed pre-dispatch. Why this is useful is explained in the weights article. In order to correct weight, the dispatchable declares a different return type and then returns its actual weight:
#[pallet::weight(10_000 + 500_000_000)]
fn expensive_or_cheap(input: u64) -> DispatchResultWithPostInfo {
let was_heavy = do_calculation(input);
if (was_heavy) {
// None means "no correction" from the weight annotation.
Ok(None.into())
} else {
// Return the actual weight consumed.
Ok(Some(10_000).into())
}
}
Custom fees
You can also define custom fee systems through custom weight functions or inclusion fee functions.
Custom weights
Instead of using the default weight annotations described above, one can create a custom weight calculation type. This type must implement the follow traits:
- [
WeighData<T>
]: To determine the weight of the dispatch. - [
ClassifyDispatch<T>
]: To determine the class of the dispatch. - [
PaysFee<T>
]: To determine whether the dispatchable's sender pays fees.
Substrate then bundles the output information of the two traits into the [DispatchInfo
] struct and
provides it by implementing the [GetDispatchInfo
] for all Call
variants and opaque extrinsic
types. This is used internally by the System and Executive modules; you probably won't use it.
ClassifyDispatch
, WeighData
, and PaysFee
are generic over T
, which gets resolved into the
tuple of all dispatch arguments except for the origin. To demonstrate, we will craft a struct that
calculates the weight as m * len(args)
where m
is a given multiplier and args
is the
concatenated tuple of all dispatch arguments. Further, the dispatch class is Operational
if the
transaction has more than 100 bytes of length in arguments and will pay fees if the encoded length
is greater than 10 bytes.
struct LenWeight(u32);
impl<T> WeighData<T> for LenWeight {
fn weigh_data(&self, target: T) -> Weight {
let multiplier = self.0;
let encoded_len = target.encode().len() as u32;
multiplier * encoded_len
}
}
impl<T> ClassifyDispatch<T> for LenWeight {
fn classify_dispatch(&self, target: T) -> DispatchClass {
let encoded_len = target.encode().len() as u32;
if encoded_len > 100 {
DispatchClass::Operational
} else {
DispatchClass::Normal
}
}
}
impl<T> PaysFee<T> {
fn pays_fee(&self, target: T) -> Pays {
let encoded_len = target.encode().len() as u32;
if encoded_len > 10 {
Pays::Yes
} else {
Pays::No
}
}
}
A weight calculator function can also be coerced to the final type of the argument, instead of
defining it as a vague type that is encodable. pallet-example
contains an example of how to do
this. Just note that, in that case, your code would roughly look like:
struct CustomWeight;
impl WeighData<(&u32, &u64)> for CustomWeight {
fn weigh_data(&self, target: (&u32, &u64)) -> Weight {
...
}
}
// given a dispatch:
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::weight(CustomWeight)]
fn foo(a: u32, b: u64) { ... }
}
This means that CustomWeight
can only be used in conjunction with a dispatch with a particular
signature (u32, u64)
, as opposed to LenWeight
, which can be used with anything because they
don't make any strict assumptions about <T>
.
Custom inclusion fee
This is an example of how to customize your inclusion fee. You must configure the appropriate associated types in the respective module.
// Assume this is the balance type
type Balance = u64;
// Assume we want all the weights to have a `100 + 2 * w` conversion to fees
struct CustomWeightToFee;
impl Convert<Weight, Balance> for CustomWeightToFee {
fn convert(w: Weight) -> Balance {
let a = Balance::from(100);
let b = Balance::from(2);
let w = Balance::from(w);
a + b * w
}
}
parameter_types! {
pub const ExtrinsicBaseWeight: Weight = 10_000_000;
}
impl frame_system::Config for Runtime {
type ExtrinsicBaseWeight = ExtrinsicBaseWeight;
}
parameter_types! {
pub const TransactionByteFee: Balance = 10;
}
impl transaction_payment::Config {
type TransactionByteFee = TransactionByteFee;
type WeightToFee = CustomWeightToFee;
type FeeMultiplierUpdate = TargetedFeeAdjustment<TargetBlockFullness>;
}
struct TargetedFeeAdjustment<T>(sp_std::marker::PhantomData<T>);
impl<T: Get<Perquintill>> Convert<Fixed128, Fixed128> for TargetedFeeAdjustment<T> {
fn convert(multiplier: Fixed128) -> Fixed128 {
// Don't change anything. Put any fee update info here.
multiplier
}
}
Next steps
The entire logic of fees is encapsulated in pallet-transaction-payment
via a SignedExtension
.
While this pallet provides a high degree of flexibility, a user can opt to build their custom
payment module drawing inspiration from Transaction Payment.
Given now you know what Substrate's weight system is, how it affects transaction fee computation, and how to specify
them for your dispatchables, the last question is how to find the right weights for your dispatchables.
That is what Substrate benchmarking is for. By writing benchmarking functions and running them, the system
(frame-benchmarking
) calls these functions repeatedly with different numerical parameters and empirically determine
the weight functions for dispatchables in their worst case scenarios, within a certain limit. For more information, see Benchmarking.
Examples
You can find examples of custom weights and fees in the following repositories: