Storage Hashers
What are Storage Hashers?
In Substrate, storage keys are generated by hashing the pallet name, storage item name, and (for maps) the key. Different hashing algorithms provide different trade-offs between security, performance, and features.
The substrate_metadata package provides implementations of all Substrate storage hashers, allowing you to generate the correct storage keys for querying chain state.
Available Hashers
Blake2_128
128-bit Blake2 hash:
import 'package:substrate_metadata/substrate_hashers/substrate_hashers.dart';
final hasher = Hasher.blake2b128();
final hash = hasher.hash(data);
// Returns 16 bytes (128 bits)
Use case: Fast, secure hashing for storage keys.
Blake2_256
256-bit Blake2 hash:
final hasher = Hasher.blake2b256();
final hash = hasher.hash(data);
// Returns 32 bytes (256 bits)
Use case: More secure variant with larger output.
Blake2_128Concat
Blake2_128 with the original data concatenated:
final hasher = Hasher.blake2b128Concat();
final hash = hasher.hash(data);
// Returns: 16-byte hash + original data
Use case: Allows iterating over storage keys while maintaining hash security. The concatenated data allows decoding the key.
Twox128
128-bit xxHash:
final hasher = Hasher.twox128();
final hash = hasher.hash(data);
// Returns 16 bytes
Use case: Very fast hashing. Used for pallet and storage item names (which are not user-controlled).
Twox256
256-bit xxHash:
final hasher = Hasher.twox256();
final hash = hasher.hash(data);
// Returns 32 bytes
Use case: Faster alternative to Blake2_256 for non-user-controlled data.
Twox64Concat
64-bit xxHash with original data concatenated:
final hasher = Hasher.twox64Concat();
final hash = hasher.hash(data);
// Returns: 8-byte hash + original data
Use case: Fast iteration over storage keys. NOT cryptographically secure.
Identity
No hashing, returns the data as-is:
final hasher = Hasher.identity();
final hash = hasher.hash(data);
// Returns original data unchanged
Use case: When you want the key itself to be readable. Often used with Blake2 hashers for composite keys.
Generating Storage Keys
For Simple Storage Values
Simple values use a fixed storage key:
import 'package:substrate_metadata/substrate_hashers/substrate_hashers.dart';
// Generate key for a simple storage value
String generateStorageKey(String palletName, String storageName) {
final palletHash = Hasher.twox128().hashHex(palletName);
final storageHash = Hasher.twox128().hashHex(storageName);
return '0x$palletHash$storageHash';
}
// Example: System.Number
final key = generateStorageKey('System', 'Number');
For Storage Maps
Maps need the key to be hashed too:
import 'dart:typed_data';
import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';
String generateMapStorageKey({
required String palletName,
required String storageName,
required Hasher keyHasher,
required Uint8List encodedKey,
}) {
final palletHash = Hasher.twox128().hashHex(palletName);
final storageHash = Hasher.twox128().hashHex(storageName);
final keyHash = keyHasher.hashHex(encodedKey);
return '0x$palletHash$storageHash$keyHash';
}
// Example: System.Account (uses Blake2_128Concat)
final accountId = Uint8List(32); // Your account ID
final key = generateMapStorageKey(
palletName: 'System',
storageName: 'Account',
keyHasher: Hasher.blake2b128Concat(),
encodedKey: accountId,
);
For Double Maps
Some storage uses two keys:
String generateDoubleMapKey({
required String palletName,
required String storageName,
required Hasher keyHasher1,
required Uint8List encodedKey1,
required Hasher keyHasher2,
required Uint8List encodedKey2,
}) {
final palletHash = Hasher.twox128().hashHex(palletName);
final storageHash = Hasher.twox128().hashHex(storageName);
final keyHash1 = keyHasher1.hashHex(encodedKey1);
final keyHash2 = keyHasher2.hashHex(encodedKey2);
return '0x$palletHash$storageHash$keyHash1$keyHash2';
}
Using Storage Metadata
The metadata tells you which hashers to use:
import 'package:substrate_metadata/substrate_metadata.dart';
void generateKeyFromMetadata(
MetadataTypeRegistry registry,
String palletName,
String storageName,
List<Uint8List> keys,
) {
// Get storage metadata
final storage = registry.getStorageMetadata(palletName, storageName);
if (storage == null) {
throw Exception('Storage not found');
}
// Build the base key
final palletHash = Hasher.twox128().hash(utf8.encode(palletName));
final storageHash = Hasher.twox128().hash(utf8.encode(storageName));
final keyBytes = <int>[...palletHash, ...storageHash];
// Handle storage type
switch (storage.type) {
case StorageEntryTypePlain():
// No additional key needed
break;
case StorageEntryTypeMap(:final hashers):
// Apply hashers to keys
if (keys.length != hashers.length) {
throw Exception('Key count mismatch');
}
for (var i = 0; i < keys.length; i++) {
final hasher = _getHasher(hashers[i]);
final keyHash = hasher.hash(keys[i]);
keyBytes.addAll(keyHash);
}
break;
}
final storageKey = '0x${hex.encode(keyBytes)}';
print('Storage key: $storageKey');
}
Hasher _getHasher(StorageHasherEnum hasherEnum) {
return switch (hasherEnum) {
Blake2b128() => Hasher.blake2b128(),
Blake2b256() => Hasher.blake2b256(),
Blake2b128Concat() => Hasher.blake2b128Concat(),
Twox128() => Hasher.twox128(),
Twox256() => Hasher.twox256(),
Twox64Concat() => Hasher.twox64Concat(),
Identity() => Hasher.identity(),
};
}
Complete Storage Key Example
Here's a complete example for querying account balance:
import 'package:substrate_metadata/substrate_metadata.dart';
import 'package:polkadart_scale_codec/polkadart_scale_codec.dart';
class StorageKeyGenerator {
final MetadataTypeRegistry registry;
StorageKeyGenerator(this.registry);
/// Generate storage key for System.Account
String accountStorageKey(Uint8List accountId) {
return _generateMapKey(
palletName: 'System',
storageName: 'Account',
key: accountId,
);
}
/// Generate storage key for Balances.TotalIssuance
String totalIssuanceKey() {
return _generatePlainKey(
palletName: 'Balances',
storageName: 'TotalIssuance',
);
}
/// Generate a plain storage key (no map key)
String _generatePlainKey({
required String palletName,
required String storageName,
}) {
final palletHash = Hasher.twox128().hash(utf8.encode(palletName));
final storageHash = Hasher.twox128().hash(utf8.encode(storageName));
return '0x${hex.encode([...palletHash, ...storageHash])}';
}
/// Generate a map storage key
String _generateMapKey({
required String palletName,
required String storageName,
required Uint8List key,
}) {
// Get storage metadata to find the hasher
final storage = registry.getStorageMetadata(palletName, storageName);
if (storage == null) {
throw Exception('Storage $palletName.$storageName not found');
}
// Get the hasher
Hasher hasher;
if (storage.type case StorageEntryTypeMap(:final hashers)) {
hasher = _getHasher(hashers.first);
} else {
throw Exception('Not a map storage');
}
// Build the key
final palletHash = Hasher.twox128().hash(utf8.encode(palletName));
final storageHash = Hasher.twox128().hash(utf8.encode(storageName));
final keyHash = hasher.hash(key);
return '0x${hex.encode([...palletHash, ...storageHash, ...keyHash])}';
}
Hasher _getHasher(StorageHasherEnum hasherEnum) {
return switch (hasherEnum) {
Blake2b128() => Hasher.blake2b128(),
Blake2b256() => Hasher.blake2b256(),
Blake2b128Concat() => Hasher.blake2b128Concat(),
Twox128() => Hasher.twox128(),
Twox256() => Hasher.twox256(),
Twox64Concat() => Hasher.twox64Concat(),
Identity() => Hasher.identity(),
};
}
}
// Usage
void main() {
final chainInfo = ChainInfo.fromMetadata(metadataBytes);
final keyGen = StorageKeyGenerator(chainInfo.registry);
// Generate key for an account
final accountId = Uint8List(32); // Fill with actual account ID
final accountKey = keyGen.accountStorageKey(accountId);
print('Account storage key: $accountKey');
// Generate key for total issuance
final totalIssuanceKey = keyGen.totalIssuanceKey();
print('Total issuance key: $totalIssuanceKey');
}
Iterating Over Storage Keys
When using concat hashers (Blake2_128Concat, Twox64Concat, Identity), you can iterate and decode keys:
import 'dart:convert';
void iterateAccounts(List<String> storageKeys) {
for (final key in storageKeys) {
// Remove 0x prefix
final keyBytes = hex.decode(key.substring(2));
// Skip pallet hash (16 bytes) + storage hash (16 bytes)
final offset = 32;
// For Blake2_128Concat: next 16 bytes are hash, rest is account ID
final hashLength = 16;
final accountId = keyBytes.sublist(offset + hashLength);
print('Account ID: ${hex.encode(accountId)}');
}
}
Hasher Comparison
| Hasher | Output Size | Concat | Secure | Speed | Use Case |
|---|---|---|---|---|---|
| Blake2_128 | 16 bytes | No | Yes | Fast | Secure hashing |
| Blake2_256 | 32 bytes | No | Yes | Fast | More secure |
| Blake2_128Concat | 16 + data | Yes | Yes | Fast | Iterable secure storage |
| Twox128 | 16 bytes | No | No | Very fast | Non-user data |
| Twox256 | 32 bytes | No | No | Very fast | Non-user data |
| Twox64Concat | 8 + data | Yes | No | Very fast | Iterable non-user data |
| Identity | Data size | Yes | No | Instant | Readable keys |
Best Practices
Never Use Twox for User Data
Twox hashers are NOT cryptographically secure. Only use them for pallet/storage names or other non-user-controlled data.Use Metadata
Always get hashers from metadata rather than hardcoding them. Chains may change hashers in runtime upgrades.Cache Storage Keys
If you're querying the same storage repeatedly, cache the generated keys.Use Concat for Iteration
If you need to iterate over storage keys, use concat hashers (Blake2_128Concat, Twox64Concat).Next Steps
- Metadata Merkleization - Secure offline signing
- ChainInfo API - High-level API for common operations