Polkadart Logo
Substrate Metadata

Storage Hashers

Understanding and using Substrate storage hashers for generating storage keys

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).

Twox hashers are NOT cryptographically secure. Only use for non-user-controlled data.

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

HasherOutput SizeConcatSecureSpeedUse Case
Blake2_12816 bytesNoYesFastSecure hashing
Blake2_25632 bytesNoYesFastMore secure
Blake2_128Concat16 + dataYesYesFastIterable secure storage
Twox12816 bytesNoNoVery fastNon-user data
Twox25632 bytesNoNoVery fastNon-user data
Twox64Concat8 + dataYesNoVery fastIterable non-user data
IdentityData sizeYesNoInstantReadable 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