Skip to content

Accounts

Introduction

Accounts are essential for managing identity, transactions, and governance on the network in the Polkadot SDK. Understanding these components is critical for seamless development and operation on the network, whether you're building or interacting with Polkadot-based chains.

This page will guide you through the essential aspects of accounts, including their data structure, balance types, reference counters, and address formats. You’ll learn how accounts are managed within the runtime, how balances are categorized, and how addresses are encoded and validated.

Account Data Structure

Accounts are foundational to any blockchain, and the Polkadot SDK provides a flexible management system. This section explains how the Polkadot SDK defines accounts and manages their lifecycle through data structures within the runtime.

Account

The Account data type is a storage map within the System pallet that links an account ID to its corresponding data. This structure is fundamental for mapping account-related information within the chain.

The code snippet below shows how accounts are defined:

 /// The full account information for a particular account ID
 #[pallet::storage]
 #[pallet::getter(fn account)]
 pub type Account<T: Config> = StorageMap<
   _,
   Blake2_128Concat,
   T::AccountId,
   AccountInfo<T::Nonce, T::AccountData>,
   ValueQuery,
 >;

The preceding code block defines a storage map named Account. The StorageMap is a type of on-chain storage that maps keys to values. In the Account map, the key is an account ID, and the value is the account's information. Here, T represents the generic parameter for the runtime configuration, which is defined by the pallet's configuration trait (Config).

The StorageMap consists of the following parameters:

  • _ - used in macro expansion and acts as a placeholder for the storage prefix type. Tells the macro to insert the default prefix during expansion
  • Blake2_128Concat - the hashing function applied to keys in the storage map
  • T::AccountId - represents the key type, which corresponds to the account’s unique ID
  • AccountInfo<T::Nonce, T::AccountData> - the value type stored in the map. For each account ID, the map stores an AccountInfo struct containing:
    • T::Nonce - a nonce for the account, which is incremented with each transaction to ensure transaction uniqueness
    • T::AccountData - custom account data defined by the runtime configuration, which could include balances, locked funds, or other relevant information
  • ValueQuery - defines how queries to the storage map behave when no value is found; returns a default value instead of None
Additional information

For a detailed explanation of storage maps, refer to the StorageMap Rust docs.

Account Info

The AccountInfo structure is another key element within the System pallet, providing more granular details about each account's state. This structure tracks vital data, such as the number of transactions and the account’s relationships with other modules.

#[derive(Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode)]
pub struct AccountInfo<Nonce, AccountData> {
  pub nonce: Nonce,
  pub consumers: RefCount,
  pub providers: RefCount,
  pub sufficients: RefCount,
  pub data: AccountData,
}

The AccountInfo structure includes the following components:

  • nonce - tracks the number of transactions initiated by the account, which ensures transaction uniqueness and prevents replay attacks
  • consumers - counts how many other modules or pallets rely on this account’s existence. The account cannot be removed from the chain (reaped) until this count reaches zero
  • providers - tracks how many modules permit this account’s existence. An account can only be reaped once both providers and sufficients are zero
  • sufficients - represents the number of modules that allow the account to exist for internal purposes, independent of any other modules
  • AccountData - a flexible data structure that can be customized in the runtime configuration, usually containing balances or other user-specific data

This structure helps manage an account's state and prevents its premature removal while it is still referenced by other on-chain data or modules. The AccountInfo structure can vary as long as it satisfies the trait bounds defined by the AccountData associated type in the frame-system::pallet::Config trait.

Account Reference Counters

Polkadot SDK uses reference counters to track an account’s dependencies across different runtime modules. These counters ensure that accounts remain active while data is associated with them.

The reference counters include:

  • consumers - prevents account removal while other pallets still rely on the account
  • providers - ensures an account is active before other pallets store data related to it
  • sufficients - indicates the account’s independence, ensuring it can exist even without a native token balance, such as when holding sufficient alternative assets

Providers Reference Counters

The providers counter ensures that an account is ready to be depended upon by other runtime modules. For example, it is incremented when an account has a balance above the existential deposit, which marks the account as active.

The system requires this reference counter to be greater than zero for the consumers counter to be incremented, ensuring the account is stable before any dependencies are added.

Consumers Reference Counters

The consumers counter ensures that the account cannot be reaped until all references to it across the runtime have been removed. This check prevents the accidental deletion of accounts that still have active on-chain data.

It is the user’s responsibility to clear out any data from other runtime modules if they wish to remove their account and reclaim their existential deposit.

Sufficients Reference Counter

The sufficients counter tracks accounts that can exist independently without relying on a native account balance. This is useful for accounts holding other types of assets, like tokens, without needing a minimum balance in the native token.

For instance, the Assets pallet, may increment this counter for an account holding sufficient tokens.

Account Deactivation

In Polkadot SDK-based chains, an account is deactivated when its reference counters (such as providers, consumers, and sufficient) reach zero. These counters ensure the account remains active as long as other runtime modules or pallets reference it.

When all dependencies are cleared and the counters drop to zero, the account becomes deactivated and may be removed from the chain (reaped). This is particularly important in Polkadot SDK-based blockchains, where accounts with balances below the existential deposit threshold are pruned from storage to conserve state resources.

Each pallet that references an account has cleanup functions that decrement these counters when the pallet no longer depends on the account. Once these counters reach zero, the account is marked for deactivation.

Updating Counters

The Polkadot SDK provides runtime developers with various methods to manage account lifecycle events, such as deactivation or incrementing reference counters. These methods ensure that accounts cannot be reaped while still in use.

The following helper functions manage these counters:

  • inc_consumers() - increments the consumer reference counter for an account, signaling that another pallet depends on it
  • dec_consumers() - decrements the consumer reference counter, signaling that a pallet no longer relies on the account
  • inc_providers() - increments the provider reference counter, ensuring the account remains active
  • dec_providers() - decrements the provider reference counter, allowing for account deactivation when no longer in use
  • inc_sufficients() - increments the sufficient reference counter for accounts that hold sufficient assets
  • dec_sufficients() - decrements the sufficient reference counter

To ensure proper account cleanup and lifecycle management, a corresponding decrement should be made for each increment action.

The System pallet offers three query functions to assist developers in tracking account states:

  • can_inc_consumer() - checks if the account can safely increment the consumer reference
  • can_dec_provider() - ensures that no consumers exist before allowing the decrement of the provider counter
  • is_provider_required() - verifies whether the account still has any active consumer references

This modular and flexible system of reference counters tightly controls the lifecycle of accounts in Polkadot SDK-based blockchains, preventing the accidental removal or retention of unneeded accounts. You can refer to the System pallet Rust docs for more details.

Account Balance Types

In the Polkadot ecosystem, account balances are categorized into different types based on how the funds are utilized and their availability. These balance types determine the actions that can be performed, such as transferring tokens, paying transaction fees, or participating in governance activities. Understanding these balance types helps developers manage user accounts and implement balance-dependent logic.

A more efficient distribution of account balance types is in development

Soon, pallets in the Polkadot SDK will implement the Fungible trait (see the tracking issue for more details). For example, the transaction-storage pallet changed the implementation of the Currency trait (see the Refactor transaction storage pallet to use fungible traits PR for further details):

type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

To the Fungible trait:

type BalanceOf<T> = <<T as Config>::Currency as FnInspect<<T as frame_system::Config>::AccountId>>::Balance;

This update will enable more efficient use of account balances, allowing the free balance to be utilized for on-chain activities such as setting proxies and managing identities.

Balance Types

The five main balance types are:

  • Free balance - represents the total tokens available to the account for any on-chain activity, including staking, governance, and voting. However, it may not be fully spendable or transferrable if portions of it are locked or reserved
  • Locked balance - portions of the free balance that cannot be spent or transferred because they are tied up in specific activities like staking, vesting, or participating in governance. While the tokens remain part of the free balance, they are non-transferable for the duration of the lock
  • Reserved balance - funds locked by specific system actions, such as setting up an identity, creating proxies, or submitting deposits for governance proposals. These tokens are not part of the free balance and cannot be spent unless they are unreserved
  • Spendable balance - the portion of the free balance that is available for immediate spending or transfers. It is calculated by subtracting the maximum of locked or reserved amounts from the free balance, ensuring that existential deposit limits are met
  • Untouchable balance - funds that cannot be directly spent or transferred but may still be utilized for on-chain activities, such as governance participation or staking. These tokens are typically tied to certain actions or locked for a specific period

The spendable balance is calculated as follows:

spendable = free - max(locked - reserved, ED)

Here, free, locked, and reserved are defined above. The ED represents the existential deposit, the minimum balance required to keep an account active and prevent it from being reaped. You may find you can't see all balance types when looking at your account via a wallet. Wallet providers often display only spendable, locked, and reserved balances.

Locks

Locks are applied to an account's free balance, preventing that portion from being spent or transferred. Locks are automatically placed when an account participates in specific on-chain activities, such as staking or governance. Although multiple locks may be applied simultaneously, they do not stack. Instead, the largest lock determines the total amount of locked tokens.

Locks follow these basic rules:

  • If different locks apply to varying amounts, the largest lock amount takes precedence
  • If multiple locks apply to the same amount, the lock with the longest duration governs when the balance can be unlocked

Locks Example

Consider an example where an account has 80 DOT locked for both staking and governance purposes like so:

  • 80 DOT is staked with a 28-day lock period
  • 24 DOT is locked for governance with a 1x conviction and a 7-day lock period
  • 4 DOT is locked for governance with a 6x conviction and a 224-day lock period

In this case, the total locked amount is 80 DOT because only the largest lock (80 DOT from staking) governs the locked balance. These 80 DOT will be released at different times based on the lock durations. In this example, the 24 DOT locked for governance will be released first since the shortest lock period is seven days. The 80 DOT stake with a 28-day lock period is released next. Now, all that remains locked is the 4 DOT for governance. After 224 days, all 80 DOT (minus the existential deposit) will be free and transferrable.

Illustration of Lock Example

Edge Cases for Locks

In scenarios where multiple convictions and lock periods are active, the lock duration and amount are determined by the longest period and largest amount. For example, if you delegate with different convictions and attempt to undelegate during an active lock period, the lock may be extended for the full amount of tokens. For a detailed discussion on edge case lock behavior, see this Stack Exchange post.

Balance Types on Polkadot.js

Polkadot.js provides a user-friendly interface for managing and visualizing various account balances on Polkadot and Kusama networks. When interacting with Polkadot.js, you will encounter multiple balance types that are critical for understanding how your funds are distributed and restricted. This section explains how different balances are displayed in the Polkadot.js UI and what each type represents.

The most common balance types displayed on Polkadot.js are:

  • Total balance - the total number of tokens available in the account. This includes all tokens, whether they are transferable, locked, reserved, or vested. However, the total balance does not always reflect what can be spent immediately. In this example, the total balance is 0.6274 KSM

  • Transferrable balance - shows how many tokens are immediately available for transfer. It is calculated by subtracting the locked and reserved balances from the total balance. For example, if an account has a total balance of 0.6274 KSM and a transferrable balance of 0.0106 KSM, only the latter amount can be sent or spent freely

  • Vested balance - tokens that allocated to the account but released according to a specific schedule. Vested tokens remain locked and cannot be transferred until fully vested. For example, an account with a vested balance of 0.2500 KSM means that this amount is owned but not yet transferable

  • Locked balance - tokens that are temporarily restricted from being transferred or spent. These locks typically result from participating in staking, governance, or vested transfers. In Polkadot.js, locked balances do not stack—only the largest lock is applied. For instance, if an account has 0.5500 KSM locked for governance and staking, the locked balance would display 0.5500 KSM, not the sum of all locked amounts

  • Reserved balance - refers to tokens locked for specific on-chain actions, such as setting an identity, creating a proxy, or making governance deposits. Reserved tokens are not part of the free balance, but can be freed by performing certain actions. For example, removing an identity would unreserve those funds

  • Bonded balance - the tokens locked for staking purposes. Bonded tokens are not transferrable until they are unbonded after the unbonding period

  • Redeemable balance - the number of tokens that have completed the unbonding period and are ready to be unlocked and transferred again. For example, if an account has a redeemable balance of 0.1000 KSM, those tokens are now available for spending

  • Democracy balance - reflects the number of tokens locked for governance activities, such as voting on referenda. These tokens are locked for the duration of the governance action and are only released after the lock period ends

By understanding these balance types and their implications, developers and users can better manage their funds and engage with on-chain activities more effectively.

Address Formats

The SS58 address format is a core component of the Polkadot SDK that enables accounts to be uniquely identified across Polkadot-based networks. This format is a modified version of Bitcoin's Base58Check encoding, specifically designed to accommodate the multi-chain nature of the Polkadot ecosystem. SS58 encoding allows each chain to define its own set of addresses while maintaining compatibility and checksum validation for security.

Basic Format

SS58 addresses consist of three main components:

base58encode(concat(<address-type>, <address>, <checksum>))
  • Address type - a byte or set of bytes that define the network (or chain) for which the address is intended. This ensures that addresses are unique across different Polkadot SDK-based chains
  • Address - the public key of the account encoded as bytes
  • Checksum - a hash-based checksum which ensures that addresses are valid and unaltered. The checksum is derived from the concatenated address type and address components, ensuring integrity

The encoding process transforms the concatenated components into a Base58 string, providing a compact and human-readable format that avoids easily confused characters (e.g., zero '0', capital 'O', lowercase 'l'). This encoding function (encode) is implemented exactly as defined in Bitcoin and IPFS specifications, using the same alphabet as both implementations.

Additional information

Refer to Ss58Codec for more details on the SS58 address format implementation.

Address Type

The address type defines how an address is interpreted and to which network it belongs. Polkadot SDK uses different prefixes to distinguish between various chains and address formats:

  • Address types 0-63 - simple addresses, commonly used for network identifiers
  • Address types 64-127 - full addresses that support a wider range of network identifiers
  • Address types 128-255 - reserved for future address format extensions

For example, Polkadot’s main network uses an address type of 0, while Kusama uses 2. This ensures that addresses can be used without confusion between networks.

The address type is always encoded as part of the SS58 address, making it easy to quickly identify the network. Refer to the SS58 registry for the canonical listing of all address type identifiers and how they map to Polkadot SDK-based networks.

Address Length

SS58 addresses can have different lengths depending on the specific format. Address lengths range from as short as 3 to 35 bytes, depending on the complexity of the address and network requirements. This flexibility allows SS58 addresses to adapt to different chains while providing a secure encoding mechanism.

Total Type Raw account Checksum
3 1 1 1
4 1 2 1
5 1 2 2
6 1 4 1
7 1 4 2
8 1 4 3
9 1 4 4
10 1 8 1
11 1 8 2
12 1 8 3
13 1 8 4
14 1 8 5
15 1 8 6
16 1 8 7
17 1 8 8
35 1 32 2

SS58 addresses also support different payload sizes, allowing a flexible range of account identifiers.

Checksum Types

A checksum is applied to validate SS58 addresses. Polkadot SDK uses a Blake2b-512 hash function to calculate the checksum, which is appended to the address before encoding. The checksum length can vary depending on the address format (e.g., 1-byte, 2-byte, or longer), providing varying levels of validation strength.

The checksum ensures that an address is not modified or corrupted, adding an extra layer of security for account management.

Validating Addresses

SS58 addresses can be validated using the subkey command-line interface or the Polkadot.js API. These tools help ensure an address is correctly formatted and valid for the intended network. The following sections will provide an overview of how validation works with these tools.

Using Subkey

Subkey is a CLI tool provided by Polkadot SDK for generating and managing keys. It can inspect and validate SS58 addresses.

The inspect command gets a public key and an SS58 address from the provided secret URI. The basic syntax for the subkey inspect command is:

subkey inspect [flags] [options] uri

For the uri command-line argument, you can specify the secret seed phrase, a hex-encoded private key, or an SS58 address. If the input is a valid address, the subkey program displays the corresponding hex-encoded public key, account identifier, and SS58 addresses.

For example, to inspect the public keys derived from a secret seed phrase, you can run a command similar to the following:

subkey inspect "caution juice atom organ advance problem want pledge someone senior holiday very"

The command displays output similar to the following:

subkey inspect "caution juice atom organ advance problem want pledge someone senior holiday very" Secret phrase caution juice atom organ advance problem want pledge someone senior holiday very is account: Secret seed: 0xc8fa03532fb22ee1f7f6908b9c02b4e72483f0dbd66e4cd456b8f34c6230b849 Public key (hex): 0xd6a3105d6768e956e9e5d41050ac29843f98561410d3a47f9dd5b3b227ab8746 Public key (SS58): 5Gv8YYFu8H1btvmrJy9FjjAWfb99wrhV3uhPFoNEr918utyR Account ID: 0xd6a3105d6768e956e9e5d41050ac29843f98561410d3a47f9dd5b3b227ab8746 SS58 Address: 5Gv8YYFu8H1btvmrJy9FjjAWfb99wrhV3uhPFoNEr918utyR

The subkey program assumes an address is based on a public/private key pair. If you inspect an address, the command returns the 32-byte account identifier.

However, not all addresses in Polkadot SDK-based networks are based on keys.

Depending on the command-line options you specify and the input you provided, the command output might also display the network for which the address has been encoded. For example:

subkey inspect "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU"

The command displays output similar to the following:

subkey inspect "12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU" Public Key URI 12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU is account: Network ID/Version: polkadot Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a Public key (SS58): 12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU SS58 Address: 12bzRJfh7arnnfPPUZHeJUaE62QLEwhK48QnH9LXeK2m1iZU

Using Polkadot.js API

To verify an address in JavaScript or TypeScript projects, you can use the functions built into the Polkadot.js API. For example:

// Import Polkadot.js API dependencies
const { decodeAddress, encodeAddress } = require('@polkadot/keyring');
const { hexToU8a, isHex } = require('@polkadot/util');

// Specify an address to test.
const address = 'INSERT_ADDRESS_TO_TEST';

// Check address
const isValidSubstrateAddress = () => {
  try {
    encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address));

    return true;
  } catch (error) {
    return false;
  }
};

// Query result
const isValid = isValidSubstrateAddress();
console.log(isValid);

If the function returns true, the specified address is a valid address.

Other SS58 Implementations

Support for encoding and decoding Polkadot SDK SS58 addresses has been implemented in several other languages and libraries.

Last update: December 4, 2024
| Created: October 16, 2024