Сердце формирования детерминированной цепочки находится в файле alert_chaining.c.

Вся магия состоит из трех небольших, но фундаментальных функций:


1. Вычисление хэша содержимого (Слепое шифрование)

Сервер вообще не расшифровывает сообщение. Он берет «сырые» зашифрованные байты и превращает их в 64-битный хэш содержимого:

C
/* Единая точка вычисления content_hash.
 * Хэшируются только зашифрованные данные, поэтому сервер 
 * обеспечивает математическую честность, не имея ключей доступа. */
uint64_t alert_chain_compute_content(const Alert *a) {
    XXH3_state_t state;
    XXH3_64bits_reset(&state);
    
    /* 1. Зашифрованное тело алерта */
    XXH3_64bits_update(&state, a->text, a->text_len);
    
    /* 2. Зашифрованный AES-ключ */
    XXH3_64bits_update(&state, a->encrypted_key, a->encrypted_key_len);
    
    /* 3. Вектор инициализации (IV) */
    XXH3_64bits_update(&state, a->iv, a->iv_len);
    
    /* 4. Аутентификационный тэг GCM (16 байт) */
    XXH3_64bits_update(&state, a->tag, 16);
    
    return XXH3_64bits_digest(&state);
}

2. Математическое связывание звеньев (Криптографический замок)

Здесь текущий алерт намертво привязывается к своему порядковому ID, хэшу предыдущего алерта и своему содержимому:

C
/* Единая точка связывания звеньев цепи (Link Hash).
 * Если изменить хотя бы 1 бит в ID, в предыдущем хэше или в теле алерта —
 * на выходе получится совершенно другой curr_hash. */
uint64_t alert_chain_compute_link(uint64_t id, uint64_t prev_h, uint64_t cont_h) {
    /* Формируем неразрывную тройку: ID + Хэш предка + Хэш содержимого */
    uint64_t data[3] = { id, prev_h, cont_h };
    
    /* Вычисляем сверхбыстрый 64-битный хэш XXH3 с нулевым сидом */
    return XXH3_64bits_withSeed(data, sizeof(data), 0);
}

3. Главный конвейер: Вставка и самоисцеление цепи (Re-chaining)

Вот этот участок кода разруливает сплит-брейны, делает BACKFILL и на лету пересчитывает будущее, если в середину цепи пришел пропущенный алерт:

C
/* Главная функция сборки цепи.
 * Вызывается при добавлении любого алерта (от клиента или из репликации). */
void alert_chain_process_insertion(Recipient *rec, Alert *new_alert,
                                    uint64_t remote_prev_hash,
                                    uint64_t remote_curr_hash) {
    (void)remote_prev_hash;  /* Игнорируем сетевые подсказки: цепь строго детерминирована */
    (void)remote_curr_hash;

    /* ШАГ 1: Всегда локально считаем хэш содержимого нового алерта */
    new_alert->content_hash = alert_chain_compute_content(new_alert);

    /* ШАГ 2: Через бинарный поиск O(log N) находим точное хронологическое место алерта */
    int pos = find_insert_position(rec, new_alert->id);

    /* ШАГ 3: Привязываем новый алерт к предку */
    if (pos == 0) {
        /* Если это самый первый элемент в истории — у него нет предка (генезис) */
        new_alert->prev_hash = 0;
    } else {
        /* Иначе жестко берем хэш стоящего перед ним алевра (pos - 1) */
        new_alert->prev_hash = rec->alerts[pos - 1].curr_hash;
    }

    /* ШАГ 4: Запечатываем текущий алерт — вычисляем его собственный хэш */
    new_alert->curr_hash = alert_chain_compute_link(
        new_alert->id, new_alert->prev_hash, new_alert->content_hash);

    /* ШАГ 5: МАШИНА ВРЕМЕНИ (Каскадный Re-chaining)
     * Если алерт встал в середину массива (BACKFILL после сплит-брейна), 
     * то для всех последующих алертов родительский хэш изменился!
     * Мы последовательно обновляем цепочку вправо до самого конца окна: */
    uint64_t running_prev = new_alert->curr_hash;
    for (int i = pos; i < rec->count; i++) {
        Alert *cur = &rec->alerts[i];
        
        /* Переназначаем предка для следующего алерта */
        cur->prev_hash = running_prev;
        
        /* Мгновенно пересчитываем его хэш с новым предком */
        cur->curr_hash = alert_chain_compute_link(
            cur->id, cur->prev_hash, cur->content_hash);
            
        /* Двигаем волну обновления дальше в будущее */
        running_prev = cur->curr_hash;
    }

    /* ШАГ 6: Фиксируем актуальную верхушку цепи для получателя */
    if (rec->count > 0) {
        rec->last_hash = rec->alerts[rec->count - 1].curr_hash;
    } else {
        rec->last_hash = new_alert->curr_hash;
    }
}

В чем красота этой реализации:

  • Всего 3 строки на звено: Никаких тяжелых структур данных, указателей или связных списков — чистая прямая арифметика в плоском массиве памяти.
  • Неуязвимость: Невозможно подменить сообщение в прошлом, не сломав curr_hash у всех последующих элементов.
  • Мгновенная скорость: Хэш XXH3 считается со скоростью оперативной памяти (гигабайты в секунду), поэтому сдвиг и пересчет цепи на 1000 элементов занимают микросекунды, совершенно не нагружая CPU.

License

Author: Hugo Narrow

Link: http://192.168.1.170:1313/posts/alert-chaining/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.