Substrate uses a namespaced composed key:Twox128(pallet) ++ Twox128(storage) ++ hash(key)This gives every storage slot a structured, readable address — and allows iteration over all keys in a map.
Interactive: Build a Substrate Storage Key
Change the pallet name, storage name, and key to see how the composed storage key is built.
Balances0x000000006ee02ec9000000006ee02ec9TwoX128 hash of the pallet name "Balances". This namespaces all storage under this pallet. Fast, non-cryptographic hash — pallet names are trusted.
Account0x000000001d0c220d000000001d0c220dAlice0x00000000152959e500000000152959e5...AliceBalances ++ Account ++ Alice0x000000006ee02ec9000000006ee02ec9000000...0x000000006ee02ec9000000006ee02ec9
Twox128(Account):
0x000000001d0c220d000000001d0c220d
Blake2_128Concat(Alice):
0x00000000152959e500000000152959e5416c696365...
vs. Ethereum
key = keccak256(address)
— can't decode key backkey = hash(pallet) ++ hash(storage) ++ hash(key) ++ key
— can iterate all keys!Real Substrate Key Example
1// Substrate Storage Key Formation2// TOTALLY different from Ethereum's keccak256(address)34// Every storage item is addressed by:5// Twox128(pallet_name) ++ Twox128(storage_name) ++ hash(key)67// Example: Balances pallet, Account storage map, Alice's key89Twox128("Balances") = 0x26aa394eea5630e07c48ae0c9558cef710Twox128("Account") = 0xb99d880ec681799c0cf30e8886371da911Blake2_128Concat(Alice_SS58)= 0xbe5ddb1579b72e84524fc29e78609e3c9fAlice_pubkey1213FINAL KEY = 0x26aa394eea5630e07c48ae0c9558cef714 ++ b99d880ec681799c0cf30e8886371da915 ++ be5ddb1579b72e84524fc29e78609e3c...1617// This is the key stored in the underlying trie / RocksDB18// Length: 16 + 16 + (16 + 32) = 80 bytes (320 hex chars)
Storage Types: Value, Map, DoubleMap
1// Different storage types in Substrate pallets23// 1. StorageValue — single value4#[pallet::storage]5pub type TotalIssuance<T> = StorageValue<_, u128>;6// Key = Twox128("Balances") ++ Twox128("TotalIssuance")7// No map key needed — it's a single global value89// 2. StorageMap — key → value map10#[pallet::storage]11pub type Account<T: Config> = StorageMap<12 _,13 Blake2_128Concat, // hash function for the key14 T::AccountId,15 AccountData<T::Balance>,16>;17// Key = Twox128(pallet) ++ Twox128("Account") ++ Blake2_128Concat(account_id)1819// 3. StorageDoubleMap — key1 + key2 → value20#[pallet::storage]21pub type Locks<T: Config> = StorageDoubleMap<22 _,23 Blake2_128Concat, T::AccountId, // first key24 Twox64Concat, LockIdentifier, // second key25 BoundedVec<BalanceLock<T::Balance>, MaxLocks>,26>;27// Key = Twox128(pallet) ++ Twox128("Locks") ++ hash1(key1) ++ hash2(key2)
3 Hash Functions — Choose Wisely
1// Substrate offers 3 hash functions for map keys2// Choose based on security needs:34// 1. Blake2_128Concat — SECURE (use for user-controlled keys)5// key_hash = blake2_128(key) ++ key6// - Resistant to HashDoS attacks7// - Key is appended → can iterate and decode8// - Use for: AccountId, user-provided data910// 2. Twox64Concat — FAST (use for trusted keys)11// key_hash = twox64(key) ++ key12// - NOT secure against adversarial input13// - Much faster than Blake214// - Use for: internal system keys (block numbers etc.)1516// 3. Identity — NO HASH (use for fixed-size trusted keys)17// key_hash = key (no hashing at all)18// - Fastest, but vulnerable to DoS19// - Only for fixed-size, trusted, non-adversarial keys2021// Why Blake2_128Concat vs Ethereum's keccak256?22// Ethereum: keccak256(key) → no way to reverse → can't enumerate23// Substrate: blake2_128(key) ++ key → can iterate all keys in a map!
Substrate vs Ethereum — Key Differences
1// Substrate Trie vs Ethereum MPT23// ETHEREUM:4key = keccak256(address) // single hash, 32 bytes5// → Traverse 64 nibbles in 16-way trie6// → RLP-encoded node stored as DB[keccak256(RLP(node))]7// → 3 node types: branch, extension, leaf89// SUBSTRATE:10key = Twox128(pallet) ++ Twox128(storage) ++ hash(key)11// → SAME trie traversal after key construction12// → But different encoding (not RLP — custom codec SCALE)13// → Optimized trie (more compact, fewer node types)1415// The KEY INSIGHT:16// ┌─────────────────────────────────────────────────┐17// │ BOTH end up in a trie stored as hash→node in DB│18// │ Difference is HOW the key is constructed │19// │ │20// │ Ethereum: one hash → trie │21// │ Substrate: namespaced hash → same trie │22// └─────────────────────────────────────────────────┘
Account Data Structure
1// Substrate AccountData (Balances pallet)2// Stored at: Twox128("Balances")++Twox128("Account")++Blake2_128Concat(who)34AccountData {5 free: u128, // freely transferable balance6 reserved: u128, // locked for staking/governance7 frozen: u128, // frozen by governance/system8 flags: ExtraFlags,9}1011// Compare with Ethereum:12// Ethereum account: { nonce, balance, storageRoot, codeHash }13// Substrate account: { free, reserved, frozen, flags }14// + nonce stored separately in System pallet:15// Twox128("System") ++ Twox128("Account") ++ Blake2_128Concat(who)
Pallet Architecture
Unlike Ethereum's single flat state, Substrate organizes state by pallets. Each pallet owns its storage.
Key Iteration — Major Advantage
Because Blake2_128Concat appends the raw key to the hash, you can scan a storage map and decode all keys:
for (key, value) in storage_map.iter() {
// key = blake2_128(k) ++ k → extract k
let account = decode(key[16..]);
}How Composed Keys → Capabilities
Twox128(pallet) prefixComplete namespace isolation. Pallets can never accidentally share storage keys. Add new pallets without collision risk.
Blake2_128Concat appends raw keyCan iterate all entries in a StorageMap — useful for migrations, queries. Ethereum (keccak256) can't do this.
Structured keys add length overheadKeys are 80+ bytes vs Ethereum's 32 bytes. More data per trie path.
Runtime upgrades with same storage layoutSubstrate supports forkless runtime upgrades because storage keys are deterministic from pallet/storage names.