Block weight

In Polkadot and Kusama parachains, block weight is a crucial concept in the context of blockchain performance and resource management. In Substrate, block weight refers to a metric that quantifies the computational and resource burden of executing transactions within a block. It’s an abstract measure used to represent the cost of transactions, not just in terms of computational steps, but also in terms of storage, bandwidth, and other resource usage.

The main purpose of the block weight system is to ensure that blocks are processed efficiently and within a predictable timeframe. This is critical for maintaining the performance and stability of substrate blockchains. By limiting the total weight of a block, Substrate ensures that blocks don’t become too heavy or complex to process within a reasonable timeframe.

Calculation and Limits

Each type of transaction in Substrate has an associated base weight, which represents the minimum resource cost to process that transaction. The total weight of a block is the sum of the weights of all transactions it contains. There is a maximum block weight limit, which ensures that blocks do not exceed the capacity of the network to process them in a timely manner.

The weight of a transaction can influence its processing priority and the fee required for it to be included in a block. Transactions with higher weights might require higher fees to be included, as they consume more resources.

Proper management of block weight is essential for maintaining the stability and security of the network. It helps in preventing spam and denial-of-service attacks by making it prohibitively expensive to flood the network with heavy transactions.

Since Substrate is a framework, different blockchain projects using Substrate can customize the block weight system according to their specific needs and use cases. This includes adjusting weight calculations and limits to optimize for different types of transactions or network conditions.

Transaction types

In Substrate, there are three different types on transactions for which block weights are calculated. The sum of the weight of each of these three transactions is the total weight of the block, as will be expanded on below.

Normal Transactions
Normal transactions are the standard type of transactions initiated by regular users. They typically include transfers of tokens, contract interactions, and other common blockchain operations. These transactions are subject to the standard block weight limits and fees. They compete for block space based on their offered fee and weight. Normal transactions are typically processed on a first-come, first-served basis, influenced by the transaction fee. Higher fees can lead to higher prioritization within this category.
Operational Transactions
Operational transactions are those that are crucial for the operation of the network. They are often initiated by network maintainers or validators. Examples include transactions related to consensus, governance, and emergency actions. Operational transactions are usually given a higher priority and may be allowed a larger portion of the block weight. This ensures that critical network operations are not hindered by congestion caused by normal transactions. These transactions are prioritized over normal transactions, as they are essential for the smooth functioning and maintenance of the network.
Mandatory Transactions
Mandatory transactions are those that must be processed irrespective of the block’s weight limitations. These are extremely critical transactions that are necessary for the network’s health and security, such as urgent security patches or updates. Mandatory transactions are exempt from the standard block weight limits. They will be included in the block even if it means exceeding the normal weight limit. These transactions have the highest level of prioritization. Their processing is mandatory, and they supersede all other transaction types in terms of inclusion in a block. The distinction between these types of transactions is crucial for maintaining a balance between efficient use of network resources and ensuring the reliability and security of the blockchain. By categorizing transactions in this manner, Substrate-based blockchains can prioritize essential operations, particularly in times of high demand or network stress, while still providing a fair and efficient system for normal user transactions.

py-substrate-interface

We will be using the py-substrate-interface to make RPC calls from python and decode data from substate blockchains. Specifically, we will look at how to derive public keys, ss58 addresses, and sovereign accounts. As an example, we will use data from the Composable Finance parachain to calculate the block weight of one of its blocks. First thing we will do is load the necessary Python packages.

from substrateinterface import SubstrateInterface
from substrateinterface.utils.ss58 import ss58_decode, ss58_encode
from scalecodec.base import ScaleBytes
from scalecodec.types import U32
from datetime import datetime
import pandas as pd
import pandas_gbq
pd.set_option('display.max_columns', None)
pd.options.display.max_colwidth = 100

Polkadot JS app

Before we get to the python code, we can see what the data looks like in the Polkadot JS app by choosing system.blockWeight. We see output for the three transactions types we discussed above: normal (line 4 below), operational (line 8 below), and mandatory (line 12 below).

system.blockWeight: FrameSupportDispatchPerDispatchClassWeight
{
  normal: {
    refTime: 0
    proofSize: 0
  }
  operational: {
    refTime: 0
    proofSize: 0
  }
  mandatory: {
    refTime: 500,000,000,000
    proofSize: 5,242,880
  }
}

The refTime is the measure that we are interested in. For this particular block all the refTime was allocated to mandatory transactions and zero was allocated to normal and operational transactions. There is a second piece of information that we need, which is the max refTime that is allocated to a block for this particular parachain. This information is contained in the metadata constants. In the JSON output below, the value we are looking for is the maxBlock.refTime (line 8 below):

const system.blockWeights: FrameSystemLimitsBlockWeights
{
  baseBlock: {
    refTime: 392,184,000
    proofSize: 0
  }
  maxBlock: {
    refTime: 500,000,000,000
    proofSize: 5,242,880
  }
  perClass: {
    normal: {
      baseExtrinsic: {
        refTime: 113,638,000
        proofSize: 0
      }
      maxExtrinsic: {
        refTime: 349,886,362,000
        proofSize: 3,670,016
      }
      maxTotal: {
        refTime: 375,000,000,000
        proofSize: 3,932,160
      }
      reserved: {
        refTime: 0
        proofSize: 0
      }
    }
    operational: {
      baseExtrinsic: {
        refTime: 113,638,000
        proofSize: 0
      }
      maxExtrinsic: {
        refTime: 474,886,362,000
        proofSize: 4,980,736
      }
      maxTotal: {
        refTime: 500,000,000,000
        proofSize: 5,242,880
      }
      reserved: {
        refTime: 125,000,000,000
        proofSize: 1,310,720
      }
    }
    mandatory: {
      baseExtrinsic: {
        refTime: 113,638,000
        proofSize: 0
      }
      maxExtrinsic: null
      maxTotal: null
      reserved: null
    }
  }
}

So since the refTime for mandatory transactions is 500,000,000,000 and the maxBlock.refTime is also 500,000,000,000, we can calculate that 100% of the block weight was due to mandatory transactions and zero for normal and operational transactions.

Python code

The code below shows an example of getting the block weight for each transaction type from the latest block for Composable Finance.

substrate = SubstrateInterface(url='wss://rpc.composable.finance')
head = substrate.get_chain_head()
block_number = substrate.get_block_number(head)
block_hash = substrate.get_block_hash(block_id = int(block_number))

consts = substrate.get_metadata_constants(block_hash)
weight_limit = pd.DataFrame(consts[0]).constant_value['max_block']
weight_limit = weight_limit['ref_time']

result = substrate.query("System", "BlockWeight", block_hash = block_hash).value
try:
  normal = result['normal']['ref_time']
  operational = result['operational']['ref_time']
  mandatory = result['mandatory']['ref_time']
except:
  normal = result['normal']
  operational = result['operational']
  mandatory = result['mandatory']

output = {'block':block_number,
        'normal':normal / weight_limit,
        'operational':operational / weight_limit,
        'mandatory':mandatory / weight_limit,
        'total':(normal + operational + mandatory) / weight_limit}

output
## {'block': 3675291, 'normal': 0.0, 'operational': 0.0, 'mandatory': 1.0, 'total': 1.0}