Квитанционная архитектура
Полный инженерный гайд — как построить ПО, в котором не накапливаются логи: каждое значимое действие производит криптографическую квитанцию вместо строки в файле. Жизненный цикл, анатомия, кворумные подписи, офлайн-верификация, паттерны для реальных программ.
Содержание
1. Основная идея
Большинство программ доказывают, что что-то сделали, записывая строку в лог-файл. Лог живёт на сервере оператора, растёт бесконечно, а доказать, что его не редактировали, можно только доверяя оператору. Квитанционное программирование переворачивает эту модель:
Основной принцип
Каждое значимое действие производит квитанцию — небольшой (~1,5 КБ) криптографический артефакт, подписанный независимыми сторонами — вместо изменяемой записи в логе. Квитанция переходит к пользователю. Доказательство больше не живёт на вашем сервере.
Содержимое действия никогда не покидает машину клиента. Вы вычисляете хеш локально — SHA-256, BLAKE3, SHA-3 или любая стандартная хеш-функция и отправляете только отпечаток. Сеть аттестует отпечаток — она никогда не видит, что вы сделали, только то, что вы сделали нечто в конкретный момент, и что хеш именно такой.
SHA-256 · BLAKE3 · SHA-3 · ваш выбор
только хеш уходит по сети
~1,5 КБ, сохранить
После выдачи квитанция принадлежит вам. Её может проверить кто угодно офлайн — без аккаунта, без обращения к нашим серверам — через открытый верификатор и публичные ключи операторов на GitHub. Даже если TimeLayer завтра закроется, квитанции, выданные сегодня, останутся действительными навсегда.
2. Жизненный цикл квитанции
Полный цикл от действия программы до верифицируемого доказательства, шаг за шагом:
Шаг 1 — Хеш действия локально
Определите, что считается «действием» (см. §6.1), и вычислите его хеш-отпечаток. Принимается любая стандартная хеш-функция (в примерах используется SHA-256; BLAKE3, SHA-3 и другие работают так же). Содержимое не покидает ваш процесс.
# Содержимое хешируется локально — в API не уходит CONTENT="счёт №4471 согласован · сумма 1200 EUR · 2026-01-15T09:00:00Z" ACTION_HEX=$(printf '%s' "$CONTENT" | sha256sum | cut -c1-64) # ACTION_HEX — 64-символьная hex-строка, только её увидит API
Что хешировать · Какой алгоритм
Хешируйте каноническую, детерминированную строку, которая однозначно идентифицирует действие: что произошло + кто сделал + когда + все значимые идентификаторы. Включайте достаточно полей, чтобы два разных действия всегда давали разные хеши. Точность метки времени до секунды обычно достаточна.
TimeLayer принимает любой hex-отпечаток — SHA-256, BLAKE3, SHA-3, Keccak или любой стандартный хеш. SHA-256 используется в примерах как наиболее повсеместно доступный; выбор хеш-функции полностью за вами.
Шаг 2 — POST хеша в /v1/notarize
curl -s -X POST https://api.timelayer-os.com/v1/notarize \ -H "Authorization: Bearer $ВАШ_ТОКЕН" \ -H "Content-Type: application/json" \ -d "{\"action_hex\":\"$ACTION_HEX\"}"
Шаг 3 — Получить квитанцию
API возвращает JSON. Главное поле — cert_hex — это и есть квитанция.
Сохраните её рядом с записью о действии или вместо строки лога.
{
"cert_hex": "a3f8…", ← квитанция, сохранить
"bundle_hex": "…", ← полный бандл (cert + доказательство)
"notarized_at": "2026-01-15T09:00:01Z"
}
Шаг 4 — Сохранить и передать квитанцию
В лог-ориентированной программе вы бы написали append(log_file, line).
В квитанционной вместо этого:
- Сохраните
cert_hexв базе данных рядом с записью о действии, или - Передайте файл квитанции напрямую пользователю — с этого момента доказательство у него, или
- Оба варианта: у вас копия для внутреннего аудита, у пользователя своя
Шаг 5 — Верифицировать в любой момент, офлайн
# Любой может проверить — без аккаунта, без обращения к TimeLayer timelayer-verifier verify cert.tlcert bundle.tlbundle # → VALID FINAL
3. Анатомия квитанции
Квитанция (cert_hex после декодирования) — самодостаточный документ. В ней есть
всё необходимое для верификации — никакого поиска при проверке не нужно.
{
/* что было нотаризовано */
"action_hex": "…64-символьный хеш…", ← SHA-256 · BLAKE3 · SHA-3 · любой стандартный хеш
"notarized_at": "2026-01-15T09:00:01Z",
/* кто подписал — каждая запись это один независимый оператор */
"signatures": [
{
"operator": "operator-1",
"sig_hex": "…Ed25519 подпись над телом квитанции…"
},
{
"operator": "operator-2",
"sig_hex": "…"
}
/* требуется кворум операторов */
],
/* метаданные сети */
"network_version": 2,
"quorum": "satisfied"
}
Ключевые поля
- action_hex — хеш-отпечаток, который вы отправили (в примерах SHA-256; принимается любой стандартный хеш). Квитанция привязана именно к этому значению.
- notarized_at — когда сеть заверила ваше действие.
- signatures — Ed25519 подписи от независимых операторов. Каждая покрывает полное тело квитанции (action_hex + notarized_at + метаданные сети). Изменение любого поля сломает все подписи.
Размер
Квитанция весит около 1,5 КБ (бинарные cert+bundle). Размер фиксированный — всё равно, что вы нотаризовали: один байт или 10 ГБ файл, квитанция та же.
| Термин | Что это | Размер |
|---|---|---|
Certificate (cert.tlcert) | минимальный подписанный proof | ~0,4 КБ |
Bundle (bundle.tlbundle) | данные для офлайн-проверки | ~1,1 КБ |
| Полный пакет проверки | certificate + bundle | ~1,5 КБ |
4. Кворумные подписи
Квитанция, подписанная одним ключом, ничего не доказывает о независимости: владелец ключа мог подписать что угодно. Модель доверия TimeLayer — независимая кворумная аттестация.
В сети независимые операторы, каждый со своим Ed25519 ключом подписи. Квитанция требует подписей кворума. Это имеет важные следствия:
- Нет единой точки фальсификации. Даже сам TimeLayer — как один из операторов — не может выдать валидную квитанцию в одиночку. Кворум независимых операторов должен независимо согласиться.
- Нет единой точки отказа. Если один оператор недоступен, оставшиеся операторы образуют кворум и квитанции продолжают выдаваться.
- Защита от подмены распределена между операторами. Атакующий, скомпрометировавший одного оператора, не может задним числом изменить квитанции — подписи остальных операторов не пройдут проверку.
Нотариус с одной подписью
Один ключ подписывает. Скомпрометируй ключ → подделай любую квитанцию. Оператор — единственная точка доверия.
Кворум (TimeLayer)
Нужно согласие двух независимых ключей. Компрометация одной стороны не ломает квитанции. Ни один оператор, включая нас, не может сфабриковать квитанцию.
Публичные ключи операторов
Публичный ключ каждого оператора опубликован открыто (GitHub). Верификатор загружает опубликованный набор один раз и дальше работает полностью офлайн — просто проверяет Ed25519 подписи против известных ключей. Список можно перекрестно проверить самостоятельно в любой момент; доверие к бинарнику верификатора не обязательно для того, чтобы знать, какие ключи активны.
5. Алгоритм офлайн-верификации
Верификация спроектирована так, чтобы не требовать ни одного сетевого запроса и нулевого доверия к любому работающему сервису. Что делает верификатор, шаг за шагом:
- Декодировать cert_hex — разобрать JSON-сертификат из hex-кодировки.
- Использовать встроенные ключи операторов — верификатор поставляется со встроенным каноническим набором ключей операторов (сеть не нужна). Каждому оператору сопоставлен Ed25519 публичный ключ; те же ключи опубликованы на GitHub для перекрёстной сверки.
- Восстановить подписанное сообщение — построить ту же каноническую последовательность байт из
action_hex + notarized_at + network_version + quorum. Точная сериализация детерминирована и задокументирована в открытом коде верификатора. - Проверить каждую подпись — для каждой записи в
signaturesзапустить Ed25519 verify:verify(public_key[operator], signed_message, sig_hex). - Проверить кворум — подсчитать валидные подписи. Если кворум известных операторов подписал — квитанция валидна.
- Вывод —
VALID FINALилиUNVERIFIABLEс причиной.
Что значит «валидная»
Результат VALID означает: этот конкретный action_hex был аттестован в момент notarized_at
кворумом независимых операторов сети, и сертификат не был изменён с момента выдачи.
Это не значит, что TimeLayer знает, каким было действие — была аттестована только хеш-сумма.
Что верификация не покрывает
Верификация подтверждает внутреннюю согласованность квитанции и реальность подписей сети.
Она не доказывает, что хеш в action_hex соответствует конкретному реальному событию —
эту связь вы поддерживаете сами (храните исходную строку действия рядом с квитанцией).
6. Паттерны проектирования
6.1 Граница действия
Самое сложное решение в квитанционном программировании — «что именно является действием?» Практическое правило: одно действие = одно обязательство, которое вы хотите доказывать независимо.
- Слишком крупно — «сегодняшний батч из 10 000 перемещений файлов» хешируется одним куском. Можно доказать батч, но не отдельное перемещение внутри.
- Слишком мелко — каждый вызов внутренней функции. Вы будете нотаризовать тысячи тривиальных событий, и квитанции превратятся в шум.
- Правильно — дискретное значимое событие: «счёт №4471 согласован», «файл X перемещён из A в B», «агент решил отправить письмо пользователю Y».
Практический тест: мог бы третий участник осмысленно оспорить или верифицировать это действие самостоятельно? Если да — оно заслуживает квитанции.
6.2 Fail-closed интеграция
Важнейший паттерн в квитанционных программах. В лог-ориентированном коде неудачная запись в лог обычно молча проглатывается — действие проходит, доказательство нет. В квитанционном коде нотаризация — это шлагбаум:
# Псевдокод — fail-closed паттерн # 1. Подготовить действие (валидировать, собрать изменения состояния) action_data = prepare_invoice_approval(invoice_id, amount, approver) action_hash = sha256(canonical(action_data)) # 2. Нотаризовать ДО коммита resp = notarize(action_hash) # бросает исключение, если сеть недоступна # 3. Коммит только при наличии квитанции db.commit(action_data, receipt=resp.cert_hex) return resp
Если notarize() бросает исключение — сеть недоступна, квота исчерпана, таймаут —
действие не коммитится. Система остаётся доказуемой: каждое зафиксированное действие имеет квитанцию,
ни одно зафиксированное действие никогда не останется без свидетелей.
Идемпотентность и повторы
Если нотаризация прошла, но коммит упал, у вас на руках квитанция для действия, которого не было. Решайте это транзакционно: либо оба, либо ни один. Квитанция для незафиксированного действия безвредна — она аттестует хеш, которого нет в базе.
6.3 Хранилище квитанций
Где держать квитанции зависит от того, кому их нужно проверять:
- Колонка в базе данных — проще всего. Добавьте
receipt_cert TEXTв таблицу действий. Проверяется любым, у кого есть доступ к БД. - Файловая система (один файл на квитанцию) — хорошо для файловых процессоров. Имя файла — по хешу действия:
{action_hex[:16]}.cert. Переносимо, без БД. - Передать пользователю по завершении — лучшее для пользовательских действий. Квитанция — это вложение «заказ подтверждён». Пользователь держит доказательство; ваш сервер может сгореть, а квитанция останется работать.
- Встроить в артефакт — для документов: приложите квитанцию к метаданным PDF или сайдкар-файлу
.cert. Доказательство путешествует с документом на протяжении всего его жизненного цикла.
6.4 Идемпотентность
Одно и то же хеш-значение всегда даёт согласованную, проверяемую квитанцию — но сеть выдаёт
новую квитанцию на каждый вызов нотаризации, с другим notarized_at. Если нужно ровно одна
квитанция на действие — применяйте это ограничение на уровне приложения: проверяйте, есть ли уже
квитанция для данного действия, перед вызовом API.
7. Демо двух программ
Лучший способ увидеть разницу — две программы, делающие одну работу: следят за папкой, хешируют входящие файлы, перекладывают их — построенные в двух разных архитектурных стилях.
log-driven/
Каждое перемещение файла дописывает строку в processor.log.
После 250 файлов: ~40 КБ лога, растёт.
Доказательство любого отдельного перемещения: прокрутить лог и поверить, что никто не редактировал.
receipt-driven/
Каждое перемещение вызывает notarize(sha256(move)).
После 250 файлов: 250 пар квитанций (cert.tlcert + bundle.tlbundle), ~1,5 КБ каждая.
Логов нет. Доказательство любого перемещения: timelayer-verifier verify cert.tlcert bundle.tlbundle → VALID FINAL, офлайн.
// receipt-driven/main.rs (упрощённо) fn process_file(path: &Path) -> Result<Receipt> { // 1. каноническая строка действия let action = format!( "move·{}·{}·{}", path.file_name(), sha256_file(path), utc_now() ); // 2. хеш локально — содержимое остаётся здесь let h = sha256(action.as_bytes()); // 3. нотаризовать хеш — только h уходит по сети let resp = client.notarize(&h)?; // fail-closed: ? пробрасывает ошибку // 4. записать квитанцию — два файла: нотариальный cert + bundle для гейта let dir = output_dir.join(&h[..16]); write(dir.join("cert.tlcert"), hex::decode(&resp.cert_hex)?)?; write(dir.join("bundle.tlbundle"), hex::decode(&resp.bundle_hex)?)?; // 5. только теперь переместить файл fs::rename(path, output_dir.join(path.file_name()))?; Ok(receipt) }
Ключевое наблюдение: функция возвращает Result — если нотаризация падает,
? пробрасывает ошибку и файл не перемещается. Квитанция предшествует действию
в точке коммита. Это fail-closed на уровне файловой системы.
После запуска демо в директории receipt-driven/ лежат 250 пар квитанций.
Возьмите любую:
timelayer-verifier verify output/a3f8b2c1/cert.tlcert output/a3f8b2c1/bundle.tlbundle VALID FINAL
8. Миграция с лог-ориентированного подхода
Миграция не должна быть всё-или-ничего. Практический путь:
- Определить границы действий — проверьте существующий формат логов. Каждое значимое событие (не отладочный шум) — кандидат в действие. Их, скорее всего, меньше, чем кажется.
- Добавить нотаризацию параллельно логированию — для каждого выбранного события добавьте вызов
notarize(hash(event))сразу после существующей записи в лог. (SHA-256 подходит; BLAKE3 или SHA-3 работают так же хорошо.) Сохраняйтеcert_hexв новую колонку БД. Запустите оба пути параллельно на спринт. - Верифицировать выборку — проверьте 20–30 квитанций через
timelayer-verifier. Убедитесь, что ваша каноническая строка детерминирована (одинаковые входы → одинаковый хеш → доказуемая квитанция). - Убрать запись в лог для мигрированных событий — когда квитанционному хранилищу можно доверять, удалите соответствующие вызовы
logger.info(…). Квитанция теперь и есть запись. - Перейти в fail-closed режим — перенесите нотаризацию перед коммитом в БД, чтобы неудача нотаризации предотвращала коммит действия. Это точка невозврата.
- Сохранить debug/trace логирование — квитанционный подход применяется к значимым бизнес-событиям, а не к каждому вызову функции. Отладочные логи — это шум; они принадлежат эфемерным инструментам наблюдаемости. Оставьте их как есть.
Не нотаризуйте всё подряд
Нотаризация HTTP health-check'ов, внутренних heartbeat'ов или отладочных трассировок тратит квоту и создаёт шум. Квитанция ценна своей намеренностью — одна на значимый коммит, а не одна на вызов функции.
9. FAQ для инженера
Как доказать, что квитанция соответствует конкретному реальному событию?
Храните каноническую строку действия, которая породила хеш, рядом с квитанцией. По строке
любой может пересчитать hash(строка) тем же алгоритмом и убедиться, что совпадает с action_hex
в квитанции. Квитанция тогда доказывает: эта точная строка была аттестована в это точное время кворумом независимых операторов. Связь строки с реальным событием — ваша ответственность задокументировать.
Должен ли хеш действия быть уникальным для всех пользователей?
Нет — два разных пользователя могут нотаризовать один и тот же хеш (например, хеш одного файла). Каждый получает свою квитанцию со своим временем. Уникальность важна внутри вашего собственного журнала: два разных действия должны давать разные хеши, поэтому включайте ID пользователя и метку времени в каноническую строку.
Что происходит, если API недоступен, когда программа пытается нотаризовать?
В fail-closed режиме действие не коммитится, вы повторяете позже. На практике стоит сделать небольшую локальную очередь: буферизуйте ожидающие нотаризации в локальной таблице БД, сбрасывайте её асинхронно. Действия коммитятся после получения квитанции при сбросе очереди. Это развязывает горячий путь от сетевой задержки, сохраняя гарантию fail-closed.
Можно ли нотаризовать несколько действий одной квитанцией?
Да — хешируйте дерево Меркла или каноническую конкатенацию всех хешей действий, нотаризуйте корень. Корневая квитанция доказывает весь батч. Компромисс: можно доказать только батч, но не отдельные действия внутри. Используйте батчинг для высоконагруженных конвейеров, где квитанции на каждое действие расточительны.
Как работает верификация квитанций в CI/CD?
Бинарник timelayer-verifier — единый статический исполняемый файл без зависимостей от рантайма.
Положите его в образ CI. Добавьте шаг верификации после интеграционных тестов: для каждого выходного
артефакта проверяйте его квитанцию. Ненулевой код возврата валит сборку.
Это «квитанции как оракул тестов» — CI становится проверщиком доказательств, а не просто гонщиком тестов.
Совместимо ли это с существующими фреймворками аудит-логирования?
Да — квитанции аддитивны. Оставьте существующий аудит-лог для compliance/отладки; добавьте нотаризацию для подмножества событий, которые хотите сделать внешне верифицируемыми. Оба механизма сосуществуют; квитанционный подход не замена всей наблюдаемости.
TimeLayer