← Назад

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 буферы обеспечивает атомарные обновления без блокировок.