← Назад

BLAKE3 vs SHA-256: почему мы выбрали BLAKE3

Криптографические подписи снапшотов, user-specific ключи, кэширование верификации. Как MEMORIA использует BLAKE3 для защиты 15 миллионов пользователей и почему это быстрее SHA-256 в 6-8 раз.

6-8×
быстрее SHA-256
32B
подпись
100ns
верификация
0
аллокаций
Содержание
  1. Проблема: SHA-256 слишком медленный
  2. Что такое BLAKE3
  3. Архитектура BLAKE3
  4. Подписи снапшотов в MEMORIA
  5. User-specific ключи
  6. Кэширование верификации
  7. AES-GCM для snapshot.key
  8. xxhash: когда криптография не нужна
  9. Бенчмарки
  10. Выводы

Проблема: SHA-256 слишком медленный

SHA-256 — золотой стандарт криптографии. Его используют Bitcoin, TLS, Git. Но у него есть фундаментальная проблема для высоконагруженных систем: он медленный.

SHA-256 обрабатывает данные блоками по 64 байта, выполняя 64 раунда преобразований для каждого блока. На современном CPU это занимает ~700 ns на 1 KB данных. Для системы, которая верифицирует тысячи снапшотов в секунду, это становится узким местом.

Кроме того, SHA-256 не использует современные возможности процессоров:

Цифры говорят

При верификации 10 000 снапшотов в секунду (типичная нагрузка для MEMORIA), SHA-256 потребовал бы ~7 ms только на хэширование. Это 70% времени одного ядра CPU. BLAKE3 справляется с той же задачей за ~1 ms.

Что такое BLAKE3

BLAKE3 — это криптографическая хэш-функция, разработанная Жан-Филиппом Омоном (Jean-Philippe Aumasson) в 2020 году. Она основана на BLAKE2, который, в свою очередь, основан на ChaCha — потоковом шифре, созданном Дэниелом Бернштейном.

Ключевые особенности BLAKE3:

В Go используется библиотека lukechampine.com/blake3 — чистая реализация без зависимостей от C.

Архитектура BLAKE3

Главное отличие BLAKE3 от SHA-256 — деревья Меркла. Вместо последовательной обработки блоков, BLAKE3 разбивает данные на части, хэширует каждую часть параллельно, а затем объединяет результаты:

Данные: [блок 1] [блок 2] [блок 3] [блок 4] │ │ │ │ ▼ ▼ ▼ ▼ H(block1) H(block2) H(block3) H(block4) │ │ │ │ └────┬─────┘ └────┬───── ▼ ▼ H(left) H(right) │ │ └──────────┬───────────┘ ▼ BLAKE3 hash

Это позволяет:

  1. Параллельное вычисление — каждый блок хэшируется независимо
  2. SIMD-оптимизация — несколько блоков обрабатываются одной векторной инструкцией
  3. Инкрементальность — можно добавить данные без пересчёта всего хэша

На 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

Зачем это нужно?

  1. Изоляция пользователей — даже если злоумышленник знает snapshotKey, он не может подделать снапшот другого пользователя без знания его peerID
  2. ДетерминированностьuserKey всегда одинаков для одного peerID, его не нужно хранить
  3. Кэширование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

Как это работает:

  1. Вычисляем xxhash от сообщения (быстро, ~5 ns)
  2. Ищем хэш в кэше
  3. Если нашли — возвращаем результат (0 ns)
  4. Если нет — вычисляем 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?

Безопасность

Пароль для расшифровки 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?

✗ BLAKE3 для кэша
  • Скорость: ~100 ns/KB
  • Безопасность: криптографическая
  • Использование: подписи снапшотов
✓ xxhash для кэша
  • Скорость: ~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Оценка

Сравнение с другими алгоритмами:

✗ SHA-256
  • Скорость: 700 ns/128B
  • SIMD: нет
  • Параллелизм: нет
  • Вывод: фиксированный 256 бит
✓ BLAKE3
  • Скорость: 100 ns/128B
  • SIMD: AVX2, AVX-512
  • Параллелизм: дерево Меркла
  • Вывод: любой размер

Разница: 7 раз быстрее. Для системы, которая верифицирует тысячи снапшотов в секунду, это критично.

Выводы

BLAKE3 — это не просто «быстрый SHA-256». Это качественно другой алгоритм, который использует современные возможности процессоров:

В MEMORIA BLAKE3 используется для:

  1. Подписей снапшотов (с snapshotKey)
  2. Derive user-specific ключей (из peerID)
  3. Подписей транзакций (с userKey)
  4. Подписей claim-запросов (с userKey)

А xxhash — для некриптографических задач: шардирование, кэши, rate limiting.

Главный урок

Не все хэши одинаково полезны. Если вам нужна криптографическая стойкость — используйте BLAKE3 (быстрее SHA-256 в 6-8 раз). Если нужна только скорость и распределение — используйте xxhash (быстрее BLAKE3 в 20 раз). Правильный выбор алгоритма может дать ускорение в 100 раз для всей системы.

В следующей статье мы разберём, как double buffering через ping-pong буферы обеспечивает атомарные обновления без блокировок.