Батчинг операций подписи и верификации

В криптографических системах на основе TweetNaCl.js и nacl.js каждая операция подписи обычно рассматривается как независимое действие: сообщение → подпись → проверка. При увеличении количества сообщений такой подход становится узким местом из-за повторяющихся затрат на сериализацию данных, вызовы криптографических функций и работу с буферами.

Батчинг операций подписи и верификации решает задачу групповой обработки, минимизируя накладные расходы JavaScript-уровня и позволяя более эффективно использовать криптографическое ядро библиотеки.


Модель данных и базовые операции подписи

В TweetNaCl.js используется Ed25519 для цифровых подписей. Основной API выглядит следующим образом:

const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');

const keyPair = nacl.sign.keyPair();

const message = nacl.util.decodeUTF8('Hello world');

const signature = nacl.sign.detached(message, keyPair.secretKey);

const isValid = nacl.sign.detached.verify(
  message,
  signature,
  keyPair.publicKey
);

Каждая операция detached создаёт независимую подпись, которая затем может быть проверена отдельно. При большом количестве сообщений (например, журналы событий, блоки транзакций, телеметрия) такой подход приводит к избыточным вызовам verify.


Проблема повторяющейся верификации

Если имеется массив сообщений:

const messages = [
  'event:login:user1',
  'event:transfer:500',
  'event:logout:user1',
  'event:login:user2'
];

и каждое сообщение подписано отдельно:

const signatures = messages.map(m =>
  nacl.sign.detached(nacl.util.decodeUTF8(m), keyPair.secretKey)
);

то на этапе проверки выполняется:

messages.forEach((m, i) => {
  const valid = nacl.sign.detached.verify(
    nacl.util.decodeUTF8(m),
    signatures[i],
    keyPair.publicKey
  );
});

Основная проблема — повторяющееся декодирование, обращение к криптографической функции и проверка каждой подписи отдельно.


Батчинг на уровне структуры данных

Батчинг в контексте TweetNaCl.js не реализован как встроенная функция, поэтому он строится поверх существующего API.

Ключевая идея — объединить операции на уровне JavaScript-логики:

  • группировка сообщений
  • предварительное кодирование
  • минимизация вызовов decodeUTF8
  • кэширование промежуточных данных
  • последовательная обработка без лишних аллокаций

Подготовка батча сообщений

Эффективный батчинг начинается с нормализации входных данных:

function prepareBatch(messages) {
  const encoded = new Array(messages.length);

  for (let i = 0; i < messages.length; i++) {
    encoded[i] = nacl.util.decodeUTF8(messages[i]);
  }

  return encoded;
}

Эта стадия убирает повторяющееся преобразование UTF-8 при каждой верификации.


Батчевая генерация подписей

Хотя криптографически подписи остаются индивидуальными, их генерация может быть оптимизирована через единый цикл без промежуточных структур:

function signBatch(encodedMessages, secretKey) {
  const signatures = new Array(encodedMessages.length);

  for (let i = 0; i < encodedMessages.length; i++) {
    signatures[i] = nacl.sign.detached(encodedMessages[i], secretKey);
  }

  return signatures;
}

Оптимизация достигается за счёт:

  • отсутствия map-обёрток
  • фиксированной длины массива
  • отсутствия лишних аллокаций внутри колбэков

Батчевая верификация подписей

Основной выигрыш даёт оптимизация проверки:

function verifyBatch(encodedMessages, signatures, publicKey) {
  const results = new Array(encodedMessages.length);

  for (let i = 0; i < encodedMessages.length; i++) {
    results[i] = nacl.sign.detached.verify(
      encodedMessages[i],
      signatures[i],
      publicKey
    );
  }

  return results;
}

В результате вместо множества разрозненных операций формируется единый линейный процесс обработки.


Снижение накладных расходов JavaScript-движка

В V8 и других движках основная стоимость таких операций складывается из:

  • создания замыканий (map/forEach)
  • динамического изменения типов
  • повторных аллокаций Uint8Array
  • переходов между JS и нативным кодом

Использование классического for-цикла вместо функциональных методов снижает давление на GC и уменьшает количество скрытых классовых переходов.


Частично векторизованный подход

Хотя TweetNaCl.js не поддерживает SIMD или нативную пакетную криптографию, возможна имитация батча через группировку операций:

function batchProcess(items, secretKey, publicKey) {
  const encoded = prepareBatch(items);

  const signatures = signBatch(encoded, secretKey);

  const results = verifyBatch(encoded, signatures, publicKey);

  return {
    signatures,
    results
  };
}

Такой подход объединяет весь pipeline в одну последовательность, уменьшая промежуточные состояния.


Кэширование и повторное использование буферов

Серьёзная оптимизация достигается при отказе от постоянного создания новых массивов:

const bufferCache = {
  encoded: [],
  signatures: [],
  results: []
};

function ensureCapacity(arr, size) {
  if (arr.length < size) {
    arr.length = size;
  }
}

Далее буферы переиспользуются:

function processBatch(messages, keyPair) {
  ensureCapacity(bufferCache.encoded, messages.length);
  ensureCapacity(bufferCache.signatures, messages.length);
  ensureCapacity(bufferCache.results, messages.length);

  for (let i = 0; i < messages.length; i++) {
    bufferCache.encoded[i] = nacl.util.decodeUTF8(messages[i]);
  }

  for (let i = 0; i < messages.length; i++) {
    bufferCache.signatures[i] =
      nacl.sign.detached(bufferCache.encoded[i], keyPair.secretKey);
  }

  for (let i = 0; i < messages.length; i++) {
    bufferCache.results[i] =
      nacl.sign.detached.verify(
        bufferCache.encoded[i],
        bufferCache.signatures[i],
        keyPair.publicKey
      );
  }

  return bufferCache.results;
}

Пакетная обработка потоковых данных

При работе с потоками событий батчинг приобретает дополнительный смысл: сообщения поступают непрерывно и группируются по окну времени или размеру.

class BatchProcessor {
  constructor(size, keyPair) {
    this.size = size;
    this.keyPair = keyPair;
    this.batch = [];
  }

  add(message) {
    this.batch.push(message);

    if (this.batch.length >= this.size) {
      return this.flush();
    }

    return null;
  }

  flush() {
    const encoded = prepareBatch(this.batch);
    const signatures = signBatch(encoded, this.keyPair.secretKey);
    const results = verifyBatch(encoded, signatures, this.keyPair.publicKey);

    this.batch.length = 0;

    return { encoded, signatures, results };
  }
}

Такой подход используется в системах логирования и телеметрии, где важно удерживать стабильную нагрузку.


Контроль целостности батча

При групповой обработке важно учитывать, что нарушение одного элемента не должно влиять на остальные. Поэтому результаты верификации возвращаются массивом:

[
  true,
  false,
  true,
  true
]

Каждый элемент батча остаётся независимым с точки зрения криптографической проверки, несмотря на общую обработку.


Ограничения подхода

Батчинг в TweetNaCl.js имеет архитектурные ограничения:

  • отсутствие нативной batch-verification Ed25519
  • невозможность объединения подписей в одну криптографическую операцию
  • линейная сложность O(n) остаётся неизменной
  • оптимизация затрагивает только JS-уровень, а не криптографическое ядро

Поэтому основная цель батчинга — снижение overhead, а не ускорение самой криптографии.


Практическая модель применения

Типичная схема использования батчинга выглядит как конвейер:

  1. Получение набора сообщений
  2. Преобразование в Uint8Array
  3. Подпись всех элементов
  4. Передача пакета
  5. Пакетная проверка получателем
const batch = processBatch(messages, keyPair);

Такой подход особенно эффективен в системах, где:

  • сообщения однотипны
  • высокая частота событий
  • критична производительность сериализации
  • допустима отложенная обработка