Проблема: SHA-256 слишком медленный
SHA-256 — золотой стандарт криптографии. Его используют Bitcoin, TLS, Git. Но у него есть фундаментальная проблема для высоконагруженных систем: он медленный.
SHA-256 обрабатывает данные блоками по 64 байта, выполняя 64 раунда преобразований для каждого блока. На современном CPU это занимает ~700 ns на 1 KB данных. Для системы, которая верифицирует тысячи снапшотов в секунду, это становится узким местом.
Кроме того, SHA-256 не использует современные возможности процессоров:
- Нет SIMD — не использует векторные инструкции (AVX2, AVX-512)
- Последовательный — нельзя распараллелить вычисление хэша
- Фиксированный размер вывода — всегда 256 бит, нельзя получить 128 или 512 без отдельного алгоритма
При верификации 10 000 снапшотов в секунду (типичная нагрузка для MEMORIA), SHA-256 потребовал бы ~7 ms только на хэширование. Это 70% времени одного ядра CPU. BLAKE3 справляется с той же задачей за ~1 ms.
Что такое BLAKE3
BLAKE3 — это криптографическая хэш-функция, разработанная Жан-Филиппом Омоном (Jean-Philippe Aumasson) в 2020 году. Она основана на BLAKE2, который, в свою очередь, основан на ChaCha — потоковом шифре, созданном Дэниелом Бернштейном.
Ключевые особенности BLAKE3:
- Дерево Меркла — хэш вычисляется параллельно по блокам, затем объединяется
- SIMD-оптимизация — использует AVX2, AVX-512, NEON (ARM)
- Любой размер вывода — от 1 байта до бесконечности (в теории)
- Ключевой режим — может работать как MAC (message authentication code)
- DeriveKeys — может выводить подчинённые ключи
В Go используется библиотека lukechampine.com/blake3 — чистая реализация без зависимостей от C.
Архитектура BLAKE3
Главное отличие BLAKE3 от SHA-256 — деревья Меркла. Вместо последовательной обработки блоков, BLAKE3 разбивает данные на части, хэширует каждую часть параллельно, а затем объединяет результаты:
Это позволяет:
- Параллельное вычисление — каждый блок хэшируется независимо
- SIMD-оптимизация — несколько блоков обрабатываются одной векторной инструкцией
- Инкрементальность — можно добавить данные без пересчёта всего хэша
На CPU с AVX-512 BLAKE3 может обрабатывать 16 блоков по 1024 байта одновременно. Это даёт теоретическую скорость до 1 GB/s на ядро.
Подписи снапшотов в MEMORIA
Каждый снапшот в MEMORIA подписывается BLAKE3. Формат снапшота (128 байт):
─────────────────────────────────────────────────────────┐
│ Offset │ Size │ Field │
├──────────┼────────┼─────────────────────────────────────┤
│ 0-3 │ 4 │ Magic: "SNAP" │
│ 4-23 │ 20 │ PeerID (20 цифр) │
│ 24-31 │ 8 │ Balance (uint64) │
│ 32-63 │ 32 │ UserKey (BLAKE3 хэш) │
│ 64-95 │ 32 │ Padding (нули) │
│ 96-127 │ 32 │ Signature (BLAKE3 подпись) │
└──────────┴────────┴─────────────────────────────────────┘Формат снапшота
Подпись вычисляется так:
func buildSignature(peerID [20]byte, balance int64, userKey [32]byte) [32]byte {
// Формируем сообщение: peerID + balance + userKey + 32 нуля
msg := make([]byte, 0, 128)
msg = append(msg, peerID[:]...)
msg = binary.LittleEndian.AppendUint64(msg, uint64(balance))
msg = append(msg, userKey[:]...)
msg = append(msg, make([]byte, 32)...) // padding
// Вычисляем подпись: BLAKE3(snapshotKey, msg)
h := blake3.New(32, snapshotKey[:])
h.Write(msg)
sig := h.Sum(nil)
var result [32]byte
copy(result[:], sig)
return result
}Go
Обратите внимание: blake3.New(32, snapshotKey[:]) создаёт хэш-функцию с ключом. Это значит, что BLAKE3 работает в режиме MAC (message authentication code) — только тот, кто знает snapshotKey, может вычислить валидную подпись.
Верификация на сервере:
func verifySnapshotSignature(msg, sig []byte) bool {
if len(sig) != 32 {
return false
}
h := blake3.New(32, snapshotKey[:])
h.Write(msg)
expected := h.Sum(nil)
return bytes.Equal(expected, sig)
}Go
Простое сравнение двух 32-байтовых массивов. Если совпадают — снапшот валиден.
User-specific ключи
Каждый пользователь имеет свой уникальный userKey, который вычисляется из его peerID:
func deriveUserKey(peerID [20]byte) [32]byte {
h := blake3.New(32, snapshotKey[:])
h.Write(peerID[:])
var userKey [32]byte
copy(userKey[:], h.Sum(nil))
return userKey
}Go
Зачем это нужно?
- Изоляция пользователей — даже если злоумышленник знает
snapshotKey, он не может подделать снапшот другого пользователя без знания егоpeerID - Детерминированность —
userKeyвсегда одинаков для одногоpeerID, его не нужно хранить - Кэширование —
userKeyвычисляется один раз и кешируется вUserArena
В структуре UserArena есть поле для кэширования:
type UserArena struct {
// ...
userKey [32]byte
userKeySet bool
// ...
}Go
При первом обращении userKey вычисляется и сохраняется. Все последующие операции используют кэшированное значение — ноль дополнительных вычислений BLAKE3.
Кэширование верификации
Верификация подписи стоит ~100 ns. Если один и тот же снапшот приходит несколько раз (например, при повторной отправке), нет смысла вычислять подпись заново. MEMORIA использует кэш верификации:
type verifyCacheEntry struct {
key uint64 // xxhash от сообщения
value bool // true если подпись валидна
next uint32 // индекс следующего элемента
_ [4]byte
}
type verifyCacheShard struct {
arena [256]verifyCacheEntry
freeList uint32
head [256]uint32
mutex sync.RWMutex
count uint32
}
var verifyCaches [256]verifyCacheShardGo
Как это работает:
- Вычисляем
xxhashот сообщения (быстро, ~5 ns) - Ищем хэш в кэше
- Если нашли — возвращаем результат (0 ns)
- Если нет — вычисляем BLAKE3 (~100 ns) и сохраняем в кэш
func verifyAndApplySnapshot(arena *UserArena, data []byte) bool {
// ... парсинг снапшота ...
// Вычисляем хэш сообщения
msgHash := xxhash.Sum64(msg)
cacheKey := msgHash
// Проверяем кэш
shardIdx := cacheKey & SHARD_MASK
shard := &verifyCaches[shardIdx]
if shard.Get(cacheKey) {
// Кэш-хит! Подпись уже верифицирована
return true
}
// Кэш-мисс — вычисляем подпись
h := blake3.New(32, snapshotKey[:])
h.Write(msg)
expectedSig := h.Sum(nil)
if !bytes.Equal(expectedSig, parsed.sig[:]) {
return false // Подпись невалидна
}
// Сохраняем в кэш
shard.Set(cacheKey, true)
return true
}Go
При высокой нагрузке (один и тот же снапшот приходит от разных клиентов) кэш-хиты составляют ~80-90%. Это снижает среднее время верификации с 100 ns до ~20 ns.
AES-GCM для snapshot.key
Мастер-ключ snapshotKey (32 байта) хранится в файле snapshot.key в зашифрованном виде. Для шифрования используется AES-GCM (Galois/Counter Mode):
func initNodeKey() {
snapshotKeyFile := "snapshot.key"
password := os.Getenv("SNAPSHOT_KEY_PASSWORD")
if data, err := os.ReadFile(snapshotKeyFile); err == nil {
// Файл существует — расшифровываем
decrypted, err := decryptKey(data, []byte(password))
if err != nil {
log.Fatalf("❌ Failed to decrypt snapshot.key: %v", err)
}
copy(snapshotKey[:], decrypted)
return
}
// Файла нет — генерируем новый ключ
rand.Read(snapshotKey[:])
keyToSave, err := encryptKey(snapshotKey[:], []byte(password))
os.WriteFile(snapshotKeyFile, keyToSave, 0600)
}Go
Функции шифрования и расшифровки:
func encryptKey(plaintext, password []byte) ([]byte, error) {
block, _ := aes.NewCipher(deriveKey(string(password)))
gcm, _ := cipher.NewGCM(block)
nonce := make([]byte, gcm.NonceSize())
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func decryptKey(ciphertext, password []byte) ([]byte, error) {
block, _ := aes.NewCipher(deriveKey(string(password)))
gcm, _ := cipher.NewGCM(block)
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
return gcm.Open(nil, nonce, ciphertext, nil)
}Go
Почему AES-GCM?
- Аутентифицированное шифрование — GCM проверяет целостность данных
- Аппаратное ускорение — AES-NI инструкции на x86/x64
- Стандарт индустрии — используется в TLS 1.3, IPsec, SSH
Пароль для расшифровки snapshot.key передаётся через переменную окружения SNAPSHOT_KEY_PASSWORD. Файл создаётся с правами 0600 (только владелец может читать). Это не идеально (пароль в env vars виден в /proc), но достаточно для защиты от случайного доступа.
xxhash: когда криптография не нужна
Не все хэши в MEMORIA криптографические. Для кэширования, шардирования и rate limiting используется xxhash — некриптографическая хэш-функция:
import "github.com/cespare/xxhash/v2"
// Шардирование
shardIdx := xxhash.Sum64(peerID[:]) & 255
// Кэш верификации
cacheKey := xxhash.Sum64(msg)
// IP rate limiter
ipHash := xxhash.Sum64([]byte(addr.IP.String()))Go
Почему xxhash, а не BLAKE3?
- Скорость: ~100 ns/KB
- Безопасность: криптографическая
- Использование: подписи снапшотов
- Скорость: ~5 ns/KB
- Безопасность: не нужна
- Использование: шардирование, кэши
Для шардирования и кэширования криптографическая стойкость не нужна — важна только скорость и равномерное распределение. xxhash в 20 раз быстрее BLAKE3 и даёт отличное распределение хэшей.
Бенчмарки
Реальные цифры на Intel Core i7-4790:
BLAKE3 (32 байта вывода, 128 байт данных):
- Вычисление: ~100 ns
- Верификация: ~100 ns
- С кэшем (hit): ~5 ns (xxhash)
SHA-256 (для сравнения, 128 байт данных):
- Вычисление: ~700 ns
- Верификация: ~700 ns
xxhash (128 байт данных):
- Вычисление: ~5 nsОценка
Сравнение с другими алгоритмами:
- Скорость: 700 ns/128B
- SIMD: нет
- Параллелизм: нет
- Вывод: фиксированный 256 бит
- Скорость: 100 ns/128B
- SIMD: AVX2, AVX-512
- Параллелизм: дерево Меркла
- Вывод: любой размер
Разница: 7 раз быстрее. Для системы, которая верифицирует тысячи снапшотов в секунду, это критично.
Выводы
BLAKE3 — это не просто «быстрый SHA-256». Это качественно другой алгоритм, который использует современные возможности процессоров:
- Дерево Меркла — параллельное вычисление
- SIMD — векторные инструкции
- Ключевой режим — MAC без отдельного HMAC
- Любой размер вывода — гибкость
В MEMORIA BLAKE3 используется для:
- Подписей снапшотов (с
snapshotKey) - Derive user-specific ключей (из
peerID) - Подписей транзакций (с
userKey) - Подписей claim-запросов (с
userKey)
А xxhash — для некриптографических задач: шардирование, кэши, rate limiting.
Не все хэши одинаково полезны. Если вам нужна криптографическая стойкость — используйте BLAKE3 (быстрее SHA-256 в 6-8 раз). Если нужна только скорость и распределение — используйте xxhash (быстрее BLAKE3 в 20 раз). Правильный выбор алгоритма может дать ускорение в 100 раз для всей системы.
В следующей статье мы разберём, как double buffering через ping-pong буферы обеспечивает атомарные обновления без блокировок.