Developers Home»Docs»Weights and Fees

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.

Note

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:

References

Last edit: on

Run into problems?
Let us Know