Спецификация протокола qub
qub — это протокол криптографических временных обязательств: система, которая запечатывает слова к определённой дате в будущем и доказывает, по наступлении этой даты, что именно было сказано и когда.
Работу обеспечивают три примитива. drand — это децентрализованный маяк случайности — момент раскрытия обеспечивается физикой, а не доброй волей какой-либо стороны. Постоянное публичное хранилище — это защищённое от подделки публичное хранилище — никакая сторона не может изменить или удалить qub после того, как он был запечатан. ML-DSA-65 — это постквантовая цифровая подпись — каждый qub связан с парой ключей, секретная часть которой никогда не покидает устройство автора.
Вместе эти примитивы создают высказывание, защищённое временем, очевидно неподделанное и атрибутируемое — квитанцию, ценность которой растёт по мере того, как улучшается способность мира фальсифицировать прошлое.
Оставшаяся часть документа — это нормативная спецификация, необходимая для совместимых реализаций.
Спецификация протокола qub
| Поле | Значение |
|---|---|
| Версия | 1.0 (версия протокола 0x01, версия внешней обёртки 0x01) |
| Дата | 2026-05-01 |
| Статус | Черновик |
| Просмотрено до | 2026-05-01 |
Этот документ — нормативная спецификация протокола системы qub для запечатанных по времени обязательств. Он определяет структуры данных, правила сериализации, формулы вывода и процедуры верификации, необходимые для совместимых реализаций.
Область применения: уровень протокола намеренно нейтрален в отношении языка — тело qub представляет собой непрозрачный открытый текст / markdown / байты пакта, а локализованный рендеринг — это ответственность просмотрщика (веб-приложение qub.social, iframe <qub-embed>, MCP-клиенты и т. д.).
1. Нотация и соглашения
| Нотация | Значение |
|---|---|
u8, u64, i64 |
Беззнаковые/знаковые целые числа указанной разрядности |
[u8; N] |
Байтовый массив фиксированной длины из N байт |
Vec<u8> |
Байтовый массив переменной длины |
Option<T> |
Значение типа T или отсутствует |
String |
UTF-8 текстовая строка, нормализованная по NFC |
| ` | |
SHA3-256(x) |
Хеш NIST SHA3-256 байтовой строки x (FIPS 202) |
ceil(x) |
Функция округления вверх: наименьшее целое число ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Старший байт первым |
Все целые числа в конструкциях прообразов кодируются как байтовые массивы фиксированной ширины big-endian (i64 → 8 байт, u8 → 1 байт), если не указано иное.
Все временные метки — это секунды Unix в UTC.
2. Структуры данных
2.1 ComposeQub (состояние создателя в памяти)
Не сериализуется в CBOR. Не записывается в постоянное хранилище. Локально в приложении создателя.
ComposeQub {
draft_id: [u8; 16], // Random, generated locally
created_at: i64, // Unix seconds UTC
unlock_at: Option<i64>, // Unix seconds UTC; None while composing
visibility: u8, // 0x01 = public (only value in MVP)
content_type: u8, // 0x01 = text (only value in MVP)
plaintext: Vec<u8>, // UTF-8 qub body
sender_label: Option<String>, // Decorative display name; not authenticated
status: DraftStatus, // Composing | Sealed | Uploaded | Failed
}
2.2 QubEnvelope (расшифрованная полезная нагрузка)
Сериализуется с использованием канонического CBOR (§3). Зашифровано внутри SealedQub. Это структура, которая подтверждает целостность содержимого после расшифровки.
QubEnvelope {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Derived (see §4.1)
content_type: u8, // Content type registry (see §6)
created_at: i64, // Unix seconds UTC
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
sender_label: Option<String>, // Decorative; not authenticated in MVP
reply_to: Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
body: Vec<u8>, // Content payload (UTF-8 for text, CBOR for pact)
body_hash: [u8; 32], // SHA3-256(body) (see §4.2)
sig_alg: u8, // Signature algorithm (see §9.2)
author_signature: Option<Vec<u8>>, // Set when sig_alg != 0x00
author_pubkey: Option<Vec<u8>>, // Set when sig_alg != 0x00
cosigner_pubkey: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
cosigner_signature: Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}
Базовая конфигурация (неподписанный текстовый qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, все поля Option отсутствуют.
Другие конфигурации v1: content_type = 0x03 (тело пакта, см. §6.1); sig_alg = 0x01 (ML-DSA-65) с присутствующими author_signature и author_pubkey (см. §9.3); cosigner_pubkey и cosigner_signature присутствуют вместе для совместно подписанных пактов (см. §9.7); reply_to установлено в qub_id родительского qub для qub в цепочках ответов (см. §9.3 относительно последствий для области действия подписи).
2.3 SealedQub (канонический сетевой формат)
Сериализуется с использованием канонического CBOR (§3). Записывается в постоянное хранилище. Это on-chain артефакт.
SealedQub {
version: u8, // Protocol major version (0x01 for v1)
qub_id: [u8; 32], // Same as QubEnvelope.qub_id
visibility: u8, // 0x01 = public; v1 viewers reject other values
unlock_at: i64, // Unix seconds UTC
outcome_at: Option<i64>, // V1.1 — surfaced on the verdict-watch CTA
// before reveal; mirrors QubEnvelope.outcome_at;
// bound to qub_id via the §4.1 preimage.
drand_chain_id: String, // drand chain hash (hex string)
drand_round: u64, // Target drand round number
tlock_ciphertext: Vec<u8>, // tlock-encrypted QubEnvelope CBOR bytes
recipient_pubkey: Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
// but not interpreted by the v1 reference viewer
title: Option<String>, // Plaintext title surfaced on the viewer
// countdown before reveal. Bound to qub_id
// via title_hash (§4.1). 1..=100 NFC code
// points, no control characters.
}
2.4 RevealedQub (состояние приложения просмотрщика)
Не сериализуется в CBOR. Локально в приложении просмотрщика. Конструируется после успешной расшифровки и верификации.
RevealedQub {
qub_id: [u8; 32],
arweave_tx_id: String,
visibility: u8,
content_type: u8,
created_at: i64,
unlock_at: i64,
outcome_at: Option<i64>, // V1.1 — переносится из QubEnvelope.outcome_at / SealedQub.outcome_at; управляет блоком отслеживания вердикта на странице раскрытия (verdict-uplift-plan §5.1)
drand_chain_id: String,
drand_round: u64,
sender_label: Option<String>,
title: Option<String>, // Carried forward from SealedQub.title
reply_to: Option<[u8; 32]>,
body: Vec<u8>,
body_hash: [u8; 32],
body_hash_verified: bool,
author_signature: Option<Vec<u8>>,
author_pubkey: Option<Vec<u8>>,
signature_verified: Option<bool>,
cosigner_pubkey: Option<Vec<u8>>,
cosigner_signature: Option<Vec<u8>>,
cosigner_verified: Option<bool>,
}
3. Канонический профиль CBOR
Вся сериализация SealedQub и QubEnvelope ДОЛЖНА соответствовать этому профилю. Две реализации, получившие одинаковую логическую структуру, ДОЛЖНЫ производить идентичные байты.
3.1 Правила кодирования
| Правило | Спецификация |
|---|---|
| Стандарт | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Порядок ключей карты | Сортировка сначала по закодированной длине в байтах (более короткие перед более длинными), затем лексикографически (побайтово для кодировок одинаковой длины) |
| Кодирование целых чисел | Кратчайшая форма: 0–23 в начальном байте; 24–255 в 2 байтах; 256–65535 в 3 байтах; и т. д. |
| Кодирование длины | Только определённые длины. Никаких массивов, карт, байтовых или текстовых строк неопределённой длины (additional info = 31 запрещено). |
| Тэги | Никаких тэгов CBOR (major type 6 запрещён). |
| С плавающей точкой | Никаких float-значений (значения major type 7 0xF9–0xFB запрещены). |
| Текстовые строки | Кодированы в UTF-8, нормализованы по NFC (Unicode Normalization Form C). |
| Байтовые строки | Сырые байты. Никакого base64-кодирования на уровне CBOR. |
| Дублирующиеся ключи | Отклонять с ошибкой. Парсеры НЕ ДОЛЖНЫ молча принимать дублирующиеся ключи карты. |
| Неизвестные ключи | Отклонять с ошибкой. Парсеры НЕ ДОЛЖНЫ допускать ключи карты вне канонического набора ключей типа — две различные канонические байтовые строки никогда не должны декодироваться в одно и то же значение (encode(decode(x)) == x), а для подписанных полезных нагрузок лишний ключ был бы скрытым содержимым, которое фиксируют обе подписи. Эволюция схемы происходит через version, никогда — через дополнительные ключи. |
| Простые значения | Разрешены только true (0xF5), false (0xF4) и null (0xF6). |
| Необязательные поля | Отсутствующие необязательные поля полностью опускаются из CBOR-карты (не кодируются как null). Присутствующие необязательные поля включаются в отсортированном порядке ключей. |
3.2 Верифицированные канонические порядки ключей
Эти порядки ключей являются нормативными. Реализации ДОЛЖНЫ выводить ключи именно в этом порядке. Debug-assertions СЛЕДУЕТ проверять порядок в нерелизных сборках.
QubEnvelope (версия 0x01, неподписанная, все необязательные поля отсутствуют):
"body" (5 encoded bytes)
"qub_id" (7 encoded bytes)
"sig_alg" (8 encoded bytes)
"version" (8 encoded bytes)
"reply_to" (9 encoded bytes) ← only if present (reply chains)
"body_hash" (10 encoded bytes)
"unlock_at" (10 encoded bytes)
"created_at" (11 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"content_type" (13 encoded bytes)
"sender_label" (13 encoded bytes) ← only if present
"author_pubkey" (14 encoded bytes) ← only if present
"cosigner_pubkey" (16 encoded bytes) ← only if present (pact cosign)
"author_signature" (17 encoded bytes) ← only if present
"cosigner_signature" (19 encoded bytes) ← only if present (pact cosign)
Вывод порядка ключей QubEnvelope: каждый ключ — это текстовая строка CBOR. Закодированная длина = 1 байт заголовка + длина строки (для строк менее 24 байт). Сортируйте сначала по общей закодированной длине, затем лексикографически для ключей одинаковой длины.
SealedQub (версия 0x01, публичный, без получателя):
"title" (6 encoded bytes) ← only if present
"qub_id" (7 encoded bytes)
"version" (8 encoded bytes)
"unlock_at" (10 encoded bytes)
"outcome_at" (11 encoded bytes) ← only if present (V1.1 verdict mechanic)
"visibility" (11 encoded bytes)
"drand_round" (12 encoded bytes)
"drand_chain_id" (15 encoded bytes)
"recipient_pubkey" (17 encoded bytes) ← only if present
"tlock_ciphertext" (17 encoded bytes)
PactTerms (тело пакта, content_type 0x03):
"notes" (6 encoded bytes) ← only if present
"terms" (6 encoded bytes)
"title" (6 encoded bytes)
"party_a" (8 encoded bytes)
"party_b" (8 encoded bytes)
"pact_version" (13 encoded bytes)
PactTerm (строка массива terms):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (карта party_a / party_b):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 Справочник байтового кодирования
| Тип | Кодирование CBOR | Пример |
|---|---|---|
| Хеш SHA3-256 (32 байта) | 0x58 0x20 + 32 байта |
body_hash, qub_id |
| Временные метки (i64) | Major type 0 (положительные) или 1 (отрицательные), кратчайшее кодирование | Секунды Unix |
| Версия (u8, значение 1) | 0x01 (один байт) |
|
| Content type (u8, значение 1) | 0x01 (один байт) |
|
| sig_alg (u8, значение 0) | 0x00 (один байт) |
|
| Подпись ML-DSA-65 (3 309 байт) | 0x59 0x0C 0xED + 3 309 байт |
author_signature, cosigner_signature |
| Публичный ключ ML-DSA-65 (1 952 байта) | 0x59 0x07 0xA0 + 1 952 байта |
author_pubkey, cosigner_pubkey |
4. Нормативные выводы
4.1 qub_id
qub_id уникально идентифицирует qub и связывает QubEnvelope с SealedQub. Он выводится детерминированно из содержимого envelope.
qub_id = SHA3-256(
"QUB_ID_V2" || // domain separator: ASCII bytes [0x51 0x55 0x42 0x5F 0x49 0x44 0x5F 0x56 0x32] (9 bytes) + 0x00 padding (1 byte) = 10 bytes
version || // u8 (1 byte)
content_type || // u8 (1 byte)
created_at || // i64 big-endian (8 bytes)
unlock_at || // i64 big-endian (8 bytes)
outcome_at_or_zero || // i64 big-endian (8 bytes; 0 when outcome_at is absent)
drand_round || // u64 big-endian (8 bytes)
body_hash || // [u8; 32] (32 bytes)
title_hash // [u8; 32] (32 bytes; absent-sentinel = [0u8; 32])
)
// Total preimage: 108 bytes → 32-byte output
Кодирование domain separator: строка "QUB_ID_V2" — это 9 ASCII-байт. Один байт паддинга 0x00 добавляется, чтобы достичь 10 байт для выравнивания. Реализации ДОЛЖНЫ использовать ровно эти 10 байт: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Кодирование outcome_at: V1.1 расширил прообраз с 92 до 100 байт, чтобы свернуть необязательное поле outcome_at в привязку. Отсутствующее outcome_at кодируется как 8 нулевых байт; валидаторы протокола повсюду отклоняют outcome_at <= 0, поэтому этот сентинел не может совпасть с легитимным значением. См. §3.2 (сетевой формат) и внутрипроектный файл tasks/verdict-uplift-plan.md для механики вердикта, которая мотивирует это поле.
Кодирование drand_round: V1.2 расширил прообраз со 100 до 108 байт, чтобы свернуть drand_round (целевой раунд drand, §4.3) в привязку, и повысил domain separator до QUB_ID_V2. Это связывает раунд таймлока с идентичностью qub: шлюз не может перепривязать шифротекст к другому раунду (например, уже прошедшему), отличному от того, который подразумевает отображаемый unlock_at. Процедура разблокировки (§8) дополнительно проверяет, что раунд, запечённый в станзу tlock-шифротекста, совпадает с unlock_round(unlock_at), так что отображаемое время разблокировки доказуемо является тем раундом, который управляет расшифровкой.
Свойства:
- Изменение любого поля в QubEnvelope (body, временные метки, content type, версия) даёт другой qub_id.
- qub_id вычисляется до шифрования. И QubEnvelope, и SealedQub несут один и тот же qub_id. Просмотрщик проверяет их совпадение после расшифровки.
- qub_id не зависит от
sender_label,author_signatureилиauthor_pubkey. Это означает, что одно и то же содержимое, запечатанное в одно и то же время, даёт один и тот же qub_id независимо от того, кто его подписывает. - Изменение
titleв SealedQub (при неизменности всего остального) меняетqub_idчерезtitle_hash. Поэтому шлюз не может подменить отображаемый в обратном отсчёте открытый заголовок, не нарушив идентичность qub. - Изменение
outcome_atв SealedQub (при неизменности всего остального) меняетqub_idчерез прообраз. Шлюз не может подменить отображаемую в обратном отсчёте дату объявления вердикта до раскрытия, не нарушив идентичность qub. - Изменение
drand_round(при неизменности всего остального) меняетqub_idчерез прообраз. Шлюз не может перепривязать таймлок-шифротекст к другому раунду, не нарушив идентичность qub; в сочетании с проверкой раунда станзы при разблокировке из §8 отображаемыйunlock_atявляется тем раундом, который фактически управляет расшифровкой.
4.2 body_hash
body_hash = SHA3-256(body)
Где body — это сырая Vec<u8> полезная нагрузка содержимого. Для текстовых qub это UTF-8-кодированное тело qub.
4.2.1 title_hash
title_hash = SHA3-256(NFC(title).utf8_bytes) if title is present
title_hash = [0u8; 32] if title is absent
Где title — это необязательный открытый заголовок, отображаемый в обратном отсчёте просмотрщика до раскрытия (см. §3.2). Нормализация NFC выполняется во время хеширования, чтобы дайджест был стабилен для визуально эквивалентных последовательностей кодовых точек. Сентинел из нулей зарезервирован для случая отсутствия; пустая строка отклоняется на границе канонического CBOR как неканоническое кодирование «отсутствия» (каноническое кодирование полностью опускает поле).
4.3 Сопоставление раунда разблокировки
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Параметр | Источник | Пример |
|---|---|---|
unlock_at |
Выбранные пользователем секунды Unix UTC | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
drand chain info (genesis_time) |
1595431050 |
chain_period_seconds |
drand chain info (period) |
30 |
Операция ceil() выбирает первый раунд drand, чьё время раскрытия ≥ unlock_at. Это гарантирует, что qub не станет расшифровываемым до выбранного времени разблокировки.
Граничный случай: если (unlock_at - chain_genesis_time) точно делится на chain_period_seconds, результат — именно этот раунд — qub разблокируется ровно во время раскрытия этого раунда.
Валидация: unlock_at ДОЛЖЕН быть в будущем во время запечатывания. unlock_at НЕ ДОЛЖЕН быть более чем на 10 лет позже created_at (для ограничения риска долгосрочной зависимости от drand; интерфейс СЛЕДУЕТ предупреждать о датах разблокировки более чем через 2 года).
5. Newtypes сетевого формата
Newtypes сетевого формата обеспечивают защиту во время компиляции от смешивания байт CBOR с JSON, сырым открытым текстом или другими байтовыми кодировками.
| Тип | Содержит | Производится | Потребляется |
|---|---|---|---|
SealedQubCbor |
Канонический CBOR SealedQub | serialize_sealed_qub() |
Загрузка в постоянное хранилище, выборка просмотрщиком |
QubEnvelopeCbor |
Канонический CBOR QubEnvelope | serialize_qub_envelope() |
Вход в tlock-шифрование, выход из tlock-расшифровки |
5.1 Правила конструирования
// Production code — only through CBOR serialisers:
let sealed = SealedQubCbor::from_encoded(cbor_bytes);
// There is deliberately NO From<Vec<u8>> implementation.
// You cannot accidentally wrap arbitrary bytes in a wire format type.
// Accessing raw bytes:
let bytes: &[u8] = sealed.as_bytes();
let bytes: Vec<u8> = sealed.into_bytes();
5.2 Валидация при конструировании
from_encoded() СЛЕДУЕТ проверять, что вход начинается с действительного заголовка карты CBOR. Полная структурная валидация происходит во время парсинга, а не во время конструирования, чтобы избежать двойного парсинга.
6. Реестр типов содержимого
| Значение | Тип | Максимальный размер тела | Примечания |
|---|---|---|---|
0x00 |
Зарезервировано (недействительно) | — | НЕ ДОЛЖНО использоваться |
0x01 |
Простой текст (UTF-8, ограниченный Markdown) | 50 КБ платно / 10 КБ бесплатно | См. §10 для правил рендеринга. Разделение бесплатно / платно обеспечивается сервисом загрузки; жёсткий потолок на уровне протокола — 50 КБ. |
0x02 |
Зарезервировано (на будущее) | — | Выделено для будущего типа содержимого; недействительно в v1. Просмотрщики ДОЛЖНЫ отклонять согласно правилу ниже. |
0x03 |
Пакт (двустороннее соглашение, тело CBOR) | 100 КБ | Тело — канонический CBOR PactTerms (§6.1). Подпись соподписанта согласно §9.7. |
0x04 |
Вердикт (самооценка автора, тело CBOR) | 8 КБ | Тело — канонический CBOR VerdictBody (§6.2). Эмитируется только системным намерением verdict. Связь с родителем — на Arweave-теге Parent-Tx-Id, а не в теле. См. verdict-uplift-plan §3.4. |
Просмотрщики ДОЛЖНЫ отклонять неизвестные типы содержимого с чётко видимой пользователю ошибкой. Просмотрщики НЕ ДОЛЖНЫ пытаться рендерить неизвестные типы как текст.
6.1 Тело пакта (content_type = 0x03)
Тело пакта — это каноническое CBOR-кодирование значения PactTerms:
PactTerms {
pact_version: u8, // 0x01 for structured/v1
title: String, // ≤ 200 bytes, NFC
terms: Vec<PactTerm>, // ≤ 20 rows
party_a: PartyIdentifier, // initiator
party_b: PartyIdentifier, // counter-signer
notes: Option<String>, // ≤ 5,000 bytes, NFC; absent key if none
}
PactTerm { key: String (≤ 100), value: String (≤ 2,000) } // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }
Канонические порядки ключей CBOR для всех трёх карт приведены в §3.2. Общая сериализованная CBOR-форма пакта НЕ ДОЛЖНА превышать 100 КБ (соответствует §6).
Дискриминатор схемы. Первая строка в terms для пакта structured/v1 ДОЛЖНА быть { key: "pact_schema", value: "structured/v1" }. Строки без этого маркера являются «пользовательскими» пактами и не получают структурированной валидации или схемоориентированного рендеринга.
Замороженные слоты подтверждения. Пакты structured/v1 содержат ровно четыре строки подтверждения под этими ключами:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value для каждого — одна из восьми замороженных английских строк, выбираемых парой (role, kind), где role ∈ { seller, buyer, provider, client } и kind ∈ { standard, capacity }. Сами строки являются нормативными данными протокола — подписи ML-DSA-65 обеих сторон обязуются на точные байты через body_hash. Они НЕ локализуются; подписанное тело нейтрально в отношении языка. Любое изменение формулировки требует новой версии схемы (structured/v2).
Восемь строк, их поиск (acknowledgement_for(role, kind)) и обоснование каждой зафиксированы эталонной реализацией. Соответствующие реализации ДОЛЖНЫ выдавать побайтово идентичные значения подтверждения; тесты SHA3-256 body-hash с эталонными фикстурами, покрывающие все четыре комбинации ролей, обнаружат любой дрейф.
Порядок отображения в просмотрщике. Строки подтверждения содержат такие выражения, как «described above», которые предполагают, что строки описания / объёма рендерятся до подтверждений. Просмотрщики ДОЛЖНЫ рендерить массив terms в порядке CBOR; переупорядочивание нарушает текстовую семантику.
Контакт встречной стороны. Когда contact стороны B — действительный адрес электронной почты, сервис загрузки qub автоматически отправляет приглашение к просмотру / соподписанию на этапе стейджинга и связывает последующее соподписание с верификацией того же адреса (§9.7). Пакты, у которых контакт стороны B отсутствует, могут быть соподписаны, но только через out-of-band канал — сервис отклоняет запросы на соподписание, которые не могут предъявить соответствующий 15-минутный маркер email-верификации.
6.2 Тело вердикта (content_type = 0x04)
Тело вердикта — это каноническое CBOR-кодирование значения VerdictBody:
VerdictBody {
verdict_version: u8, // 0x01 for structured/v1
outcome: u8, // 1=Right · 2=Partial · 3=Wrong · 4=Unfalsifiable
reflection: Option<String>, // ≤ 2,000 bytes NFC; "what changed, what did you learn"
evidence_url: Option<String>, // ≤ 2,048 bytes; HTTPS only; absent key when omitted
}
Канонический порядок ключей CBOR:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
Общая сериализованная CBOR-форма вердикта НЕ ДОЛЖНА превышать 8 КБ (соответствует строке реестра выше).
Перечисление outcome. Байт на проводе нейтрален в отношении намерения; четыре корзины Right / Partial / Wrong / Unfalsifiable покрывают пространство исходов для всякого намерения, несущего вердикт. Метки по конкретному намерению («Called it» / «Kept it» / «Shipped» / «Confirmed» для Right и т. д.) — забота рендеринга на стороне просмотрщика, разрешаемая через намерение родительского qub; провод остаётся нейтральным в отношении языка и намерения. Значения вне 1..=4 ДОЛЖНЫ быть отклонены при декодировании.
Связь с родителем. qub вердикта НЕ несёт ссылку на родителя в своём теле. Идентификатор Arweave-транзакции родительского qub эмитируется как тег хранения Parent-Tx-Id во время загрузки (§7, слой тегов хранения). Это сохраняет тело как самодостаточное подписанное заявление о самооценке; цепочка аудита («о чём прав?») устанавливается через поиск по Arweave-тегу.
Безопасность evidence_url (нормативно). Когда evidence_url присутствует, валидаторы (на стороне компоновки, на проводе, на краю Worker) ДОЛЖНЫ обеспечить:
- Только HTTPS. Строка ДОЛЖНА начинаться с байтовой последовательности
https://. Любая другая схема —http,ftp,javascript,data,fileи т. д. — отклоняется. - Ограничение длины. ≤ 2 048 байт (практический предел URL в браузерах).
- NFC + проверка на враждебные кодпойнты. То же правило, что и для
titleиreflection— кодпойнты bidi-override / zero-width / tag-block / BOM / C0 / C1 отклоняются. Определение соответствует Rustcrate::handle::contains_hostile_text_codepointи TSworkers/api/src/utils/unicode.ts::isHostileCodepoint(держите их синхронизированными). - Без пробельных символов, без управляющих ASCII. Пробельные символы / DEL / байты ниже
0x20где-либо в URL отклоняются — закрывает вектор инъекции\n/\t, который не покрывает правило bidi. - Непустой сегмент хоста. Всё между
https://и первым/,?или#ДОЛЖНО быть непустым.
Без получения на стороне сервера. Worker НЕ ДОЛЖЕН проксировать, получать или предпросматривать URL. Протокол хранит строку; рендеринг происходит на стороне просмотрщика с rel="nofollow noopener noreferrer" target="_blank" и видимым хостом, отображаемым рядом с текстом ссылки.
Reflection. Необязательный написанный автором текст размышления («что изменилось, что вы узнали»). Та же валидация NFC + враждебных кодпойнтов, что и для title. Пустой ввод / ввод только из пробельных символов сводится к отсутствующему во время конструирования.
Версия схемы. v1 поддерживает только verdict_version = 0x01. Будущие ревизии схемы инкрементируют этот байт и выходят вместе с новой версией протокола согласно §12.
7. Протокол запечатывания
Полная последовательность запечатывания. Каждый шаг нормативен.
1. User composes plaintext and metadata in ComposeQub.
2. Validate:
a. body is non-empty.
b. body size ≤ max for content_type and user tier (see §6).
c. unlock_at is in the future.
d. unlock_at ≤ created_at + 10 years.
e. content_type is a known, supported value.
3. Compute body_hash = SHA3-256(body).
4. Set created_at = current Unix seconds UTC.
5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
compute drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
(Computed here, before qub_id, because drand_round is bound into the qub_id
preimage — §4.1, V1.2.)
6. Compute qub_id (see §4.1), folding in drand_round from step 5.
7. Construct QubEnvelope with all fields.
8. Serialise QubEnvelope using canonical CBOR → bytes B.
Assert: serialised output matches canonical profile (§3).
9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C, and matching qub_id, version,
unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
never the bare SealedQubCbor. K leaves the device only as the URL
fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
signs and uploads to permanent storage. The service is byte-blind to the inner
SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
`<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
when a short code is allocated). Browsers do not transmit URL fragments
to servers, so K is never observed by qub.social or any storage gateway.
Слой тэгов хранилища (out-of-band). Сервис загрузки qub прикрепляет намеренно небольшой набор тэгов транзакции хранилища вместе с обёрнутой полезной нагрузкой. Content-Type=application/octet-stream нормативно требуется. Эталонный сервис дополнительно прикрепляет три необязательных тэга, когда создатель решает их раскрыть: Intent (валидированное по allowlist намерение составления — например, quote, reply, commitment), Author (отпечаток публичного ключа §9.3 создателя в виде 64-символьной строчной шестнадцатеричной строки) и Parent-Tx-Id (идентификатор транзакции хранилища родительского qub для цепочек ответов, 43-символьный base64url).
Тэг Author — opt-in для каждого qub: эталонное приложение создателя прикрепляет его только тогда, когда пользователь явно включает публичную атрибуцию во время запечатывания. Когда переключатель выключен — по умолчанию — тэг Author не записывается, и qub остаётся не атрибутированным в цепи: ничто в постоянном хранилище не связывает загрузку с handle создателя, его email или другими qub. Когда переключатель включён, отпечаток Author разрешается в выбранный создателем @handle через цепочку аттестаций §9.5. Связи цепочек ответов и Intent не являются идентифицирующими. Внешняя обёртка (§13) защищает внутреннее тело от корреляции шифротекста — предотвращая возможность для сборщика распознавать и массово расшифровывать загрузки qub-формы после публикации их раунда drand.
Эталонный сервис намеренно НЕ прикрепляет тэги App-Name, App-Version или Type: любой такой фильтр с единственным значением возвращал бы весь корпус qub по GraphQL-запросу, что несовместимо с областью конфиденциальности обёртки, ограниченной только телом.
Соответствующий верификатор НЕ ДОЛЖЕН зависеть от какого-либо тэга хранилища для верификации сторонними лицами по §11; body hash / qub_id / подпись обязуются только на внутреннем CBOR, никогда — на наборе тэгов.
8. Протокол разблокировки
Полная последовательность разблокировки. Каждый шаг нормативен.
1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
K = base64url_decode(fragment) from the URL fragment. If the fragment
is absent or malformed → display "this URL is missing its decryption
key" and stop; the viewer MUST NOT contact the storage gateway
without K, since fetching wrapped bytes the viewer cannot decrypt
serves no purpose and only leaks the access attempt.
2. Check denylist. If tx_id is denylisted → display block message. Stop.
3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
`version` byte is `0x01`, and compute SealedQubCbor =
unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
swapped nonce) → display "this URL's decryption key does not match
the stored qub" and stop. Authentication failures are
indistinguishable to the viewer per §13.5.
4. Parse SealedQubCbor → SealedQub.
5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
6a. Round-binding check (V1.2). Recompute expected_round =
ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
Reject unless SealedQub.drand_round == expected_round AND the round baked
into the tlock ciphertext stanza (read via the age/tlock header, no signature
required) == expected_round. The stanza round is the one that actually gates
decryption; without this check a malicious creator could bind the ciphertext
to an already-past round while displaying a future countdown, so anyone
reading the stored bytes could decrypt before unlock_at. Implementations with
no chain identity (test mocks) skip this check.
7. Once current time ≥ SealedQub.unlock_at:
a. Fetch drand round signature for SealedQub.drand_round from drand network.
b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
8. Parse B → QubEnvelope.
9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
Fail → integrity error.
13. Verify: QubEnvelope.content_type is known and renderable.
Known values: 0x01 (text), 0x03 (pact). Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using appropriate renderer (see §10 for text, §6 for pact).
17. Construct RevealedQub for display.
9. Подпись авторства
9.1 Обоснование
qub хранятся в постоянном хранилище. Подписи авторства должны оставаться неподделываемыми бессрочно, поэтому v1.0 использует постквантовую схему ML-DSA-65 (FIPS 204) вместо классической схемы, безопасность которой может ухудшиться в течение постоянного срока жизни qub.
9.2 Реестр алгоритмов
sig_alg |
Схема | Размер ключа | Размер подписи |
|---|---|---|---|
0x00 |
Нет подписи (неподписанный) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 байта | 3 309 байт |
Просмотрщики ДОЛЖНЫ отклонять неизвестные значения sig_alg.
9.3 Конструирование подписываемого прообраза
Существовало две версии прообраза. Все подписи ДОЛЖНЫ использовать V2, а верификаторы ДОЛЖНЫ принимать только V2. Устаревший прообраз V1 (документированный ниже для исторической справки) принимался как резервный вариант только для верификации во время миграции на V2; этот резервный вариант выведен из употребления, и подпись, действительная только против V1, теперь отклоняется.
V2 (текущий — производится всеми новыми подписаниями автора, а также обеими подписями потока стейджинга / соподписания пакта):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V2" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 || // u8 (1 byte): MUST be 0x00 in v1.x
sender_label_hash || // [u8; 32]: SHA3-256(NFC(sender_label)),
// or 32 zero bytes when absent
reply_to_or_zero // [u8; 32]: parent qub_id, or 32 zero
// bytes when absent
)
// Total preimage: 155 bytes → 32-byte hash
signature = Sign(author_secret_key, sig_input)
sender_label_hash следует той же конвенции сентинела отсутствия, что и title_hash (§4.2.1): 32 нулевых байта не являются допустимым выводом SHA3-256, поэтому «отсутствие» никогда не может совпасть с присутствующим label. Все поля имеют фиксированную ширину, поэтому прообраз однозначен без префиксов длины.
V1 (устаревший — ВЫВЕДЕН ИЗ УПОТРЕБЛЕНИЯ; больше не производится и больше не принимается при верификации):
sig_input = SHA3-256(
"QUB_AUTHOR_SIG_V1" || // domain separator (17 bytes)
version || // u8 (1 byte)
qub_id || // [u8; 32] (32 bytes)
body_hash || // [u8; 32] (32 bytes)
unlock_at || // i64 big-endian (8 bytes)
0x00 // u8 (1 byte): MUST be 0x00 in v1.0
)
// Total preimage: 91 bytes → 32-byte hash
Прообраз V1 опускал sender_label и reply_to. Он принимался как резервный вариант только для верификации во время миграции на V2; с тех пор этот резервный вариант выведен из употребления — верификаторы ДОЛЖНЫ принимать только прообраз V2. Определение сохранено здесь для исторической справки и чтобы объяснить domain separator ниже. Подпись, которая верифицируется только против V1, ДОЛЖНА рассматриваться как провал верификации.
Domain separators: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" — по 17 ASCII-байт каждый ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Без паддинга. Различающийся сепаратор доменно разделяет две конструкции, так что подпись над одним прообразом никогда не сможет верифицироваться как другой.
Байт org_id_present: байт, следующий за unlock_at, ДОЛЖЕН быть 0x00. Эталонная реализация выставляет это как константу ORG_ID_PRESENT_INDIVIDUAL = 0x00 в crates/qub-core/src/signing.rs; просмотрщики, реконструирующие sig_input для верификации, ДОЛЖНЫ выводить тот же байт.
Область действия подписи — что покрыто и что не покрыто. sig_input версии V2 непосредственно обязуется на version, qub_id, body_hash, unlock_at, sender_label и reply_to (плюс фиксированный domain separator и байт org_id_present). qub_id сам выводится из version, content_type, created_at, unlock_at, outcome_at, drand_round и body_hash через прообраз §4.1, так что любое изменение этих полей производит другой qub_id и транзитивно инвалидирует подпись. Поэтому аутентифицированная поверхность такова:
| Поле | Аутентифицировано подписью | Каким образом |
|---|---|---|
version |
✓ | Прямой вход для sig_input |
qub_id |
✓ | Прямой вход |
body_hash |
✓ | Прямой вход |
unlock_at |
✓ | Прямой вход |
sender_label |
✓ | Прямой вход через sender_label_hash (прообраз V2 — единственная принимаемая форма) |
reply_to |
✓ | Прямой вход через reply_to_or_zero (прообраз V2 — единственная принимаемая форма) |
content_type |
✓ | Транзитивно, через прообраз qub_id |
created_at |
✓ | Транзитивно, через прообраз qub_id |
outcome_at |
✓ | Транзитивно, через прообраз qub_id |
drand_round |
✓ | Транзитивно, через прообраз qub_id (V1.2) |
body |
✓ | Транзитивно, через body_hash = SHA3-256(body) |
author_pubkey |
— (неявно) | Ключ, верифицировавший подпись, и есть автор по определению |
cosigner_pubkey / cosigner_signature |
— | Независимо подписаны над тем же sig_input (см. §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Поля внешнего SealedQub, вне envelope — покрыты собственными структурными инвариантами (согласованность раунда / цепочки), но не подписью автора. (drand_round теперь связан транзитивно через прообраз qub_id — см. выше.) |
Почему V2 — единственный принимаемый прообраз.
- При выведенном из употребления прообразе V1 сторона с доступом на запись к хранимым байтам могла бы подменить
sender_label(«Alice» → «Mallory») или перепривязатьreply_to— и повторно зашифровать после раунда — не инвалидируя подпись автора, поскольку ни одно из этих полей не входило в подписываемый прообраз. V2 покрывает оба, так что любое изменение любого из этих полей переводит верификацию в состояние «провалено». Поскольку верификаторы теперь принимают только V2, эта подмена закрыта для каждой подписи: подпись, не связывающая ни одно из полей (то есть верифицируемая только против V1), отклоняется напрямую, а не принимается через понижение до V1. author_pubkeyвнутри envelope остаётся истинным якорем идентичности — просмотрщики ДОЛЖНЫ выводить отображаемую идентичность изauthor_pubkey(через слой аттестаций §9.5), а не доверятьsender_label.
Реализации, отображающие sender_label или reply_to конечным пользователям, ДОЛЖНЫ выдвигать аутентифицированную идентичность (отпечаток публичного ключа, аттестацию) как основной сигнал идентичности, а не label.
9.4 Процедура верификации
1. Read sig_alg from QubEnvelope.
2. If sig_alg == 0x00 → unsigned. No verification. Display "unsigned qub."
3. If sig_alg is unknown → reject. Display "unrecognised signature scheme."
4. Extract author_signature and author_pubkey. If either is absent → integrity error.
5. Reconstruct sig_input using fields from QubEnvelope (V2 formula, §9.3).
6. Verify(author_pubkey, sig_input, author_signature). The V2 preimage is the
only accepted form — the legacy V1 fallback is retired (§9.3), so a
signature that does not verify against V2 fails, full stop.
7. If verification succeeds → display "signed by [key fingerprint]."
8. If verification fails → display "signature verification failed."
Верификация подписи — самая дорогая операция (особенно ML-DSA-65). Её СЛЕДУЕТ выполнять после того, как все более дешёвые проверки (hash, qub_id, unlock_at) пройдены.
9.5 Аттестации идентичности
Аттестации идентичности — отображение author_pubkey на распознаваемые человеком утверждения об идентичности, такие как qub-handle, email-адрес, social handle или passkey-credential — это прогрессивное улучшение на стороне просмотрщика и не требуются для верификации подписи. Просмотрщики, разрешающие аттестации в отображаемую идентичность, ДОЛЖНЫ применять приоритет:
handle > email > social > fingerprint
Резерв-отпечаток — это шестнадцатеричная строка в нижнем регистре от SHA3-256(author_pubkey); он всегда доступен для любого подписанного qub. Просмотрщики МОГУТ сокращать его для отображения — эталонный просмотрщик отображает qub:, за которым следуют первые и последние четыре байта (qub:<8 hex>…<8 hex>).
Соответствующий верификатор может завершить каждую проверку в §9.4 без обращения к API qub, без какой-либо сети, кроме постоянного хранилища и drand, и без какого-либо серверного поиска. Разрешение аттестаций — отдельный best-effort шаг, выполняемый только после успешной верификации подписи.
9.6 Влияние на размер
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Подпись | 64 байта | 3 309 байт |
| Публичный ключ | 32 байта | 1 952 байта |
| Всего на qub | 96 байт | 5 261 байт |
| Дельта стоимости хранилища (при ~$5/МБ) | ~$0,0005 | ~$0,026 |
Для текстового qub в 500–2 000 байт ML-DSA-65 примерно утраивает сохранённый размер. Абсолютная стоимость пренебрежимо мала.
9.7 Верификация соподписанта (двусторонние соглашения-пакты)
Для двусторонних соглашений (content_type = 0x03) второй слой подписи доказывает, что обе стороны согласились с одними и теми же условиями.
Поля envelope:
cosigner_pubkey: публичный ключ ML-DSA-65 встречного подписанта (сторона B).cosigner_signature: подпись над тем жеsig_input, что и у автора (§9.3).
Оба поля ДОЛЖНЫ присутствовать вместе или оба отсутствовать. Если присутствует ровно одно, просмотрщики ДОЛЖНЫ сообщить об ошибке целостности.
Процедура верификации:
1. If cosigner_pubkey absent and cosigner_signature absent → no cosigner. Done.
2. If exactly one is present → integrity error.
3. Verify cosigner_pubkey != author_pubkey (prevent self-cosigning).
Fail → display "cosigner pubkey must differ from author."
4. Reconstruct sig_input using the same formula as §9.3 (V2 only — the
legacy V1 fallback is retired; all pact clients produce V2 signatures).
5. Verify(cosigner_pubkey, sig_input, cosigner_signature).
6. Success → display "co-signed by [cosigner fingerprint]."
7. Failure → display "co-signature verification failed."
Свойства:
- Соподписант подписывает идентичный
sig_input, что и автор — обе стороны обязуются на один и тот жеqub_id,body_hashиunlock_at(а в V2 — на те жеsender_label_hashиreply_to_or_zero). - Чтобы позволить встречному подписанту реконструировать прообраз V2 без доступа к сырым байтам envelope, сервис стейджинга на этапе стейджинга обеспечивает, что
sender_labelenvelope пакта равенpact_terms.party_a.label, аreply_toотсутствует. Оба условия выполняются для каждого пакта эталонного клиента; нарушающие envelope отклоняются на стейджинге. - Вывод
qub_id(§4.1) НЕ включает поля соподписанта. Добавление соподписанта к существующему envelope не меняетqub_id. - Пакт может быть подписан только автором (одностороннее обязательство), только соподписантом (необычно) или обоими (полный двусторонний доказ).
Email-связывающий шлюз (операционный). Когда staged-пакт несёт email-контакт стороны B (§6.1), сервис загрузки qub ДОЛЖЕН отклонить запрос на соподписание, если не существует кратковременного маркера email-верификации, соответствующего как staging id, так и хешу нормализованного email этого контакта. Маркер записывается /api/v1/auth/verify, когда токен magic-link несёт staging_id и верифицированный адрес совпадает с SHA-256(normalise_email(party_b.contact)) — где normalise_email(addr) сохраняет регистр локальной части и приводит к нижнему регистру только доменную часть (согласно RFC 5321 §2.3.11), а SHA-256 здесь — это хеш NIST FIPS 180-4 (отличный от SHA3-256, используемого в выводах §4) — и истекает через 900 секунд (15 минут) после выпуска. Это операционный анти-имитационный шлюз, НЕ часть on-chain доказательства qub — стороннему верификатору, воспроизводящему §11, нужны только постоянное хранилище и drand, без какого-либо серверного поиска. Маркер существует только на стороне сервера и никогда не является частью подписанного тела.
Влияние на размер (ML-DSA-65 автор + соподписант):
| Компонент | Размер |
|---|---|
| Подпись автора | 3 309 байт |
| Публичный ключ автора | 1 952 байта |
| Подпись соподписанта | 3 309 байт |
| Публичный ключ соподписанта | 1 952 байта |
| Общие криптографические накладные расходы | 10 522 байта |
| Дельта стоимости хранилища | ~$0,05 |
10. Рендеринг и санитизация Markdown
Этот раздел критичен для безопасности. Просмотрщик рендерит текстовые qub (content_type = 0x01), используя ограниченное подмножество Markdown.
10.1 Разрешённые элементы
- Заголовки: от
#до####(без#####или######) - Выделение: жирный (
**), курсив (*), зачёркнутый (~~) - Списки: упорядоченные (
1.) и неупорядоченные (-,*) - Цитаты блоком (
>) - Код: встроенные фрагменты (```) и огороженные блоки (`````)
- Горизонтальные линейки (
---) - Переносы строк (два завершающих пробела или пустая строка)
- Абзацы
10.2 Запрещённые элементы
| Элемент | Обработка |
|---|---|
Сырой HTML (<div>, <script> и т. д.) |
Полностью вырезается. Никакой HTML не пропускается. |
Изображения () |
Вырезаются. Синтаксис изображения удаляется из вывода. |
Ссылки ([text](url)) |
URL рендерится как видимый простой текст. Не делается автоссылкой. Не кликабелен без явного действия пользователя. |
| Опасные схемы URL | javascript:, data:, vbscript:, file: — вырезаются. |
| Iframes, embeds, objects | Вырезаются. |
| HTML-сущности | Декодируются в отображаемые символы только если это безопасно. |
10.3 Реализация
Реализации ДОЛЖНЫ использовать строгий парсер на основе allowlist, а не blocklist. Рекомендуемый подход:
- Парсить Markdown с помощью
pulldown-cmark(или эквивалента). - Обойти AST и отбросить любой узел, не входящий в allowlist (§10.1).
- Для узлов ссылок: выдавать URL как видимый текст, а не как кликабельный элемент
<a>. - Преобразовать отфильтрованный AST в типизированное промежуточное представление (например, enum
MarkdownNodeтолько с безопасными вариантами). Сырой HTML структурно непредставим в этом IR. - Рендерить из типизированного IR в целевой слой представления (например, реактивные view-компоненты, узлы DOM). Никакой конкатенации HTML-строк или
innerHTMLни на каком этапе.
Подходы на основе blocklist хрупки, так как новые расширения Markdown или особенности парсера могут вводить нефильтруемые элементы. Подход с типизированным AST делает XSS структурно невозможным — нет варианта, который мог бы нести произвольный HTML.
10.4 Ограничения размера и структуры
- Максимальная глубина отрендеренного заголовка:
####(H4).#####и глубже рендерятся как жирный текст. - Нет ограничения на количество абзацев (ограничения размера тела в §6 являются ограничением).
- Огороженные блоки кода: без подсветки синтаксиса в MVP. Рендерятся как моноширинный преформатированный текст.
11. Сторонняя верификация
Любая третья сторона может верифицировать публичный qub без сотрудничества qub. Процедура верификации:
1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.
Что доказывает верификация:
| Доказательство | Что устанавливает |
|---|---|
| Обязательство | Шифротекст существовал на момент временной метки блока хранилища. |
| Целостность | Тело в открытом виде соответствует зафиксированному хешу и не было изменено. |
| Время | Содержимое было нечитаемо до раунда drand, который соответствует выбранному времени разблокировки (с учётом допущений безопасности tlock и drand). |
Что верификация НЕ доказывает:
| Не-доказательство | Почему |
|---|---|
| Авторство | sender_label декоративен. Без sig_alg ≥ 0x01 любой мог запечатать это содержимое. |
| Намерение | qub доказывает содержимое и время, а не то, что создатель субъективно имел в виду. |
| Время до события | Включение в блок хранилища может отставать от фактической загрузки на минуты. Временная метка обязательства — это время блока, а не момент, когда пользователь нажал «запечатать». |
12. Версионирование
12.1 Версия протокола
Поле version (u8) как в SealedQub, так и в QubEnvelope идентифицирует мажорную версию протокола.
- Просмотрщики ДОЛЖНЫ отклонять неизвестные мажорные версии с чёткой ошибкой.
- В пределах известной мажорной версии декодеры ДОЛЖНЫ отклонять неизвестные ключи карты (§3.1) — эволюция схемы происходит путём введения нового
version, а не добавления ключей, которые существующие декодеры пропускали бы. (Ранние редакции этой спецификации допускали неизвестные необязательные поля; это положение отозвано — оно делало отображениеencode(decode(x))неинъективным и открывало вектор скрытого подписанного содержимого в полезных нагрузках пактов.) - Типы содержимого (
content_type) и схемы подписи (sig_alg) привязаны к версии: новые значения могут вводиться только вместе с новой версией протокола или явным обновлением реестра.
12.2 История версий
| Версия | Значение | Описание |
|---|---|---|
| v1 | 0x01 |
Публичные текстовые qub (content_type 0x01), двусторонние соглашения-пакты (0x03, схема structured/v1, ML-DSA-65 автор + соподписант), tlock, SHA3-256 |
12.3 Прямая совместимость
Просмотрщик v1, встречающий QubEnvelope с неизвестными ключами карты CBOR (ключами, не входящими в канонический порядок §3.2), ДОЛЖЕН отклонить его с ошибкой декодирования (§3.1). Прямая совместимость держится на поле version, а не на терпимости к ключам: будущие добавления — даже небольшие метаданные — поставляются под новым значением version, которое просмотрщик v1 отклоняет с чёткой ошибкой «более новый протокол», а не молча отбрасывает содержимое, которое фиксируют подписи.
Просмотрщик v1, встречающий sig_alg = 0x01 (ML-DSA-65), но без поддержки верификации ML-DSA-65, СЛЕДУЕТ отображать содержимое qub с уведомлением «подпись присутствует, но не верифицируема», а не отклонять qub целиком. Эталонная реализация сегодня отклоняет любое значение sig_alg, кроме 0x00 и 0x01, потому что реестр v1 не содержит другого действительного алгоритма — строгое отклонение и soft-fail наблюдательно идентичны, пока не зарегистрирован третий алгоритм. Описанное выше поведение soft-fail становится несущим, как только §9.2 допустит новую запись, и эталонный просмотрщик будет обновлён на soft-fail в этот момент.
12.4 Версия внешней обёртки
OuterWrapper, описанный в §13, несёт собственный байт version, независимый от SealedQub.version и QubEnvelope.version. Эти два пространства версий развиваются раздельно: будущая постквантово-безопасная симметричная замена увеличивает байт обёртки, не затрагивая внутреннюю версию протокола, а будущее добавление на уровне протокола (например, новое поле envelope) увеличивает внутреннюю версию, не затрагивая байт обёртки.
OUTER_WRAPPER_VERSION_* |
Значение | Алгоритм | Статус |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM с 12-байтовым nonce, 16-байтовым тэгом аутентификации, AAD привязан к qub_id |
По умолчанию v1 |
| — | 0x02–0xFF |
Зарезервировано | Будущее |
Просмотрщики ДОЛЖНЫ отклонять неизвестные версии обёртки с чёткой ошибкой. Протокол намеренно сохраняет пространство версий обёртки узким, пока не появится конкретный драйвер миграции (например, рекомендация NIST в пользу другого AEAD); слот 0x02 будет выделен в той же ревизии, которая вводит алгоритм.
13. Внешняя обёртка шифрования
13.1 Обоснование
Слои протокола (QubEnvelope → tlock → SealedQub) делают запечатанный qub запечатанным по времени: тело нечитаемо до unlock_at и до публикации подписи раунда drand. Однако после разблокировки подпись раунда становится публичной, а каноническая CBOR-форма SealedQub распознаваема, поэтому сборщик, индексировавший транзакции постоянного хранилища, мог бы массово расшифровать весь корпус qub.
Внешняя обёртка шифрования закрывает этот канал, вставляя дополнительный симметричный AEAD-слой между каноническим SealedQubCbor и байтами, загружаемыми в постоянное хранилище. 256-битный ключ K живёт только во фрагменте URL ссылки доставки и на устройствах пользователей; браузеры не передают фрагменты URL серверам, поэтому qub.social, каждый шлюз хранилища и каждый CDN перед любым из них наблюдательно слепы к K. Поэтому каждый qub в постоянном хранилище — это непрозрачный шифротекст, открытый текст которого невосстановим без URL, который создатель решил поделиться.
Чистый эффект:
- Иммунитет к перечислению по умолчанию. Обёрнутые байты в постоянном хранилище байт-неотличимы от произвольного шифротекста. Стратегия сборщика «GraphQL-запрос загрузок qub-формы, массовая расшифровка публичными подписями drand» не завершается открытым текстом.
- Поза приватности с криптошреддингом. qub.social буквально не может расшифровать собственный корпус. Повестки достигают шифротекста, а не открытого текста.
- Двухуровневая лестница конфиденциальности. По умолчанию = доступ, контролируемый ссылкой (этот раздел). Приватные qub с шифрованием на получателя (зарезервированная функция фазы 2, пока не специфицирована) накладываются сверху в качестве второго уровня.
13.2 Многослойность
plaintext body ← QubEnvelope.body (§2.2)
↓ canonical CBOR (§3)
envelope CBOR
↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
↓ canonical CBOR (§3)
SealedQubCbor bytes ← inner wire artifact
↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes ← uploaded to permanent storage (§7 step 15)
Запечатывание и разблокировка на уровне протокола (§7, §8) остаются неизменными ниже границы обёртки; обёртка прикрепляется в точке вызова seal() и снимается в точке вызова unlock().
13.3 Структура данных OuterWrapper
struct OuterWrapper {
version: u8, // 0x01, see §12.4
qub_id: [u8; 32], // copied from inner SealedQub; AEAD AAD
nonce: [u8; 12], // 96-bit AEAD nonce
ciphertext: Vec<u8>, // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}
Инварианты полей.
versionДОЛЖЕН быть равен0x01для байт обёртки v1.0.qub_idДОЛЖЕН быть равен полюqub_idSealedQub, восстановленного после распаковки. Шаг распаковки не обеспечивает это напрямую (привязка AEAD AAD делает побайтовую подделку невозможной), но слой разблокировки проверяет связь транзитивно: если создатель оборачиваетSealedQubCbor, внутреннийqub_idкоторого не совпадает сqub_idобёртки, §8 шаг 11 не проходит.nonceДОЛЖЕН быть 96 бит (12 байт), сгенерированных свежим CSPRNG для каждой операции wrap. Повторное использование nonce под одним ключом допускает атаки повторного использования nonce AEAD, восстанавливающие открытый текст; производители ДОЛЖНЫ обращаться с парами (key,nonce) как с одноразовыми.ciphertext— это выход AES-256-GCM: байты шифротекста, конкатенированные с 16-байтовым тэгом аутентификации. Ровноciphertext.len() == SealedQubCbor.len() + 16.
Кодирование CBOR. Канонический CBOR согласно §3, с тем же правилом упорядочивания ключей (сортировка по закодированной длине в байтах по возрастанию, затем лексикографически). Четыре ключа:
| Ключ | Закодированных байт | Порядок |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Первый байт CBOR OuterWrapper, следовательно, является заголовком карты определённой длины для карты из 4 записей (0xA4).
13.4 Привязка AAD к qub_id
Обёртка привязывает qub_id как дополнительные аутентифицированные данные AEAD. Это несущая структурная защита от трёх классов атак:
| Атака | Защита |
|---|---|
Переместить шифротекст под другое поле qub_id в обёртке |
Несоответствие AAD → аутентификация AEAD не проходит |
| Смешать фрагмент URL qub A с байтами хранилища qub B | Несоответствие AAD → аутентификация AEAD не проходит |
Изменить поле qub_id обёртки после загрузки |
Несоответствие AAD → аутентификация AEAD не проходит |
Перенос qub_id в открытом тексте обёртки не ослабляет иммунитет к перечислению существенно — qub_id сам по себе является хешем SHA3-256 от прообраза §4.1 без восстановимого прообраза из дайджеста, и перечислитель, уже собравший байты обёртки, не узнаёт из видимого qub_id ничего, что не мог бы вывести из самого существования загрузки.
13.5 Алгоритмы Wrap и Unwrap
wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
require K.len() == 32 and N.len() == 12 and Q.len() == 32
C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
// C includes the 16-byte authentication tag at the end
return canonical_cbor_encode(OuterWrapper{
version: 0x01,
qub_id: Q,
nonce: N,
ciphertext: C,
})
unwrap_sealed_qub(OuterWrapper bytes W, key K):
require K.len() == 32
O := canonical_cbor_decode(W) as OuterWrapper
require O.version == 0x01 // §12.4
P := AES_256_GCM_decrypt(
key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
)
// any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
return P // P is the inner SealedQubCbor
Схлопывание режимов сбоя. Неверный K, неверный nonce, несоответствие AAD и подделанный шифротекст — все производят одну и ту же ошибку DECRYPT_FAILED. Это намеренное свойство AEAD: различение режима сбоя создало бы боковой канал, который удалённый атакующий мог бы прозондировать, отправляя сформированные неправильно обёртки и измеряя время ответа. Эталонные реализации ДОЛЖНЫ схлопывать все сбои AEAD в единую форму ошибки.
13.6 Ключевой материал и распространение
Ключ обёртки K — это 256-битное равномерное случайное значение, генерируемое для каждого qub с помощью CSPRNG. Эталонные реализации получают его из:
- WASM-создатель:
getrandom(WebCrypto под бэкендомwasm_js). - Вызывающий серверный API запечатывания: его локальный CSPRNG; вызывающий предоставляет и сохраняет
Kв видеwrapper_key_b64url. Worker используетKв памяти для обёртки, но НЕ ДОЛЖЕН его сохранять. Это позволяет идемпотентному повтору восстановить отредактированный ответ, используя сохранённую вызывающим возможность, вместо зависимости от одноразового секрета, сгенерированного сервером.
Распространение: K ДОЛЖЕН быть закодирован как URL-безопасный base64 (RFC 4648 §5, без паддинга) и добавлен к ссылке доставки как компонент фрагмента:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Фрагмент никогда не передаётся ни на какой сервер соответствующим браузером. Каналы восстановления (серверный индекс истории, опт-ин email auto-send), которые персистируют полную ссылку доставки — включая фрагмент — за пределы устройства пользователя, являются явным компромиссом против стандартной криптошреддинговой позы и ДОЛЖНЫ быть привязаны к явному согласию пользователя.
Потеря фрагмента. Если пользователь теряет фрагмент URL и не имеет канала восстановления, qub нечитаем. Это несущий компромисс дизайна и ДОЛЖЕН раскрываться пользователю во время запечатывания. MVP усиливает раскрытие во время запечатывания явным текстом «сохраните этот URL» и каналом восстановления через верифицированную почту для пользователей, которые соглашаются.
13.7 Вне области применения этого раздела
- Подпись авторства (§9) неизменна: подписи вычисляются внутри внутреннего
QubEnvelopeи восстанавливаются после Unwrap → tlock-расшифровка → парсинг CBOR. - Приватные qub с шифрованием на получателя (зарезервированная функция фазы 2, пока не специфицирована) компонуются поверх этой обёртки как второй уровень конфиденциальности; оба уровня могут быть активны одновременно.
- Пакты (§6, content_type
0x03) оборачиваются точно так же, как текстовые qub; обёртка байт-слепа к внутреннему типу содержимого.
13.8 Публичные qub (опускание обёртки)
Внешняя обёртка необязательна на уровне доставки. Создатель может запечатать qub как публичный, и в этом случае канонический SealedQubCbor записывается в постоянное хранилище напрямую, без слоя OuterWrapper и без ключа K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
Публичный qub запечатан по времени, но не ограничен ссылкой: он остаётся нечитаемым до публикации своего раунда drand (слой tlock неизменен), но после разблокировки любой, у кого есть arweave_tx_id, может его расшифровать — фрагмент URL не требуется, поскольку ключа K нет. Это намеренный компромисс для поверхностей, которые должен обслуживать сервер: email-уведомления о раскрытии, сторонние встраивания и более богатое SEO после раскрытия — всем им нужна ссылка, работающая без секрета, которого у сервера никогда нет (§13.6).
Последствия, которые производитель ДОЛЖЕН учитывать:
- Нет иммунитета к перечислению. Публичные qub по построению отказываются от свойства иммунитета к перечислению из §13.1. Эталонный сервис загрузки штампует на них (и только на них) тэг постоянного хранилища
Visibility: public, чтобы они были намеренно обнаружимы; приватные qub не несут такого тэга и сохраняют свою байт-неотличимость. - Открытый заголовок раскрыт во время запечатывания. Поле
titleиз §3.2 — это открытый текст внутриSealedQubCbor. Под обёрткой оно скрыто до тех пор, пока просмотрщик не предоставитK; без обёртки оно доступно для чтения всем в постоянном хранилище с момента загрузки, до разблокировки. Соответствующие приложения создателя ДОЛЖНЫ раскрывать это во время запечатывания. - Обнаружение структурно. Соответствующий просмотрщик/встраивание различает две формы парсингом: байты, парсящиеся как
OuterWrapper, идут по пути распаковки сK; байты, парсящиеся как голыйSealedQubCbor, принимаются напрямую. Никакой флаг на проводе не требуется, иqub_idне связывает видимость — одно и то же содержимое байт-идентично на уровнеSealedQubнезависимо от того, запечатано ли оно публично или приватно.
Приватный (обёрнутый) остаётся по умолчанию; публичный — это явный выбор создателя для каждого qub.
14. Тестовые векторы
14.1 Вывод qub_id
Input:
version = 0x01
content_type = 0x01
created_at = 1735689600 (2025-01-01 00:00:00 UTC)
unlock_at = 1736294400 (2025-01-08 00:00:00 UTC)
outcome_at = absent
drand_round = 4695445 (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
body = "Hello, future." (UTF-8, 14 bytes)
title = absent
Intermediate:
body_hash = SHA3-256("Hello, future.")
= 76ab8b3f843c6ed4f2d0fd75b9f457b4
ad49dd4450f9c22723ae430e3af3211d
title_hash = [0u8; 32] (title absent — §4.2.1 sentinel)
Domain separator (10 bytes):
[0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]
Preimage (108 bytes — V1.2):
domain_separator || // 10 bytes
0x01 || // version
0x01 || // content_type
0x0000000067748580 || // created_at as i64 big-endian (1735689600)
0x00000000677DC000 || // unlock_at as i64 big-endian (1736294400)
0x0000000000000000 || // outcome_at_or_zero (outcome_at absent)
0x000000000047A595 || // drand_round as u64 big-endian (4695445)
body_hash || // 32 bytes
title_hash // 32 bytes (all-zeros sentinel; title absent)
Expected output:
qub_id = SHA3-256(preimage)
= 3a9fcb31b750d985c262fada6d4f777f
d6a28be831d941d85c131f5a4bbaf8a4
Реализации ДОЛЖНЫ выдавать идентичные значения body_hash и qub_id для этого входа. Этот тестовый вектор СЛЕДУЕТ сделать первым написанным юнит-тестом. Канонические значения выше были вычислены эталонной реализацией и ДОЛЖНЫ совпадать побитово. Исторические схемы прообраза (до запуска — ни один действующий qub от них не зависел): 92-байтовый qub_id V1.0 был 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; 100-байтовый qub_id V1.1 (после сворачивания outcome_at_or_zero) был b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 сворачивает drand_round и повышает domain separator до QUB_ID_V2.
14.2 Сопоставление раунда разблокировки
Input:
unlock_at = 1735689600
chain_genesis_time = 1595431050
chain_period_seconds = 30
Calculation:
(1735689600 - 1595431050) / 30 = 4675285.0
ceil(4675285.0) = 4675285
drand_round = 4675285
14.3 Round-trip канонического CBOR
Реализации ДОЛЖНЫ проверить, что serialize(parse(serialize(qub))) == serialize(qub) для всех допустимых входов. Это property-тест, а не одиночный вектор.
14.4 PactTerms CBOR (content_type 0x03)
Input:
pact_version = 1
title = "Scooter deposit"
terms = [
{ key: "Item", value: "Honda Metropolitan scooter" },
{ key: "Price", value: "$100" },
{ key: "Deposit", value: "$10" }
]
party_a = { label: "Alice" }
party_b = { label: "Bob", contact: "bob@example.com" }
notes = absent
Canonical CBOR key order (PactTerms):
"notes"(6) < "terms"(6) < "title"(6) < "party_a"(8) < "party_b"(8) < "pact_version"(13)
Canonical CBOR key order (PactTerm):
"key"(4) < "value"(6)
Canonical CBOR key order (PartyIdentifier):
"label"(6) < "contact"(8)
Канонические байты CBOR и body_hash SHA3-256 вычисляются эталонной реализацией. Реализации ДОЛЖНЫ выдавать побайтово идентичный CBOR для этого входа.
Реализации ДОЛЖНЫ также проверить, что serialize(parse(serialize(pact))) == serialize(pact) для всех допустимых входов PactTerms (property-тест).
14.5 Кросс-языковые векторы внешней обёртки
Внешняя обёртка (§13) имеет отдельную каноническую фикстуру в crates/qub-core/tests/vectors/wrapper_v1.json. Каждый случай фиксирует кортеж (key, nonce, qub_id, sealed_cbor) как непрозрачные шестнадцатеричные входы и утверждает конкретный выход expected_wrapper_hex. Обе эталонные реализации потребляют один и тот же JSON-файл:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
Фикстура в настоящее время закрепляет три случая:
| Случай | Покрытие |
|---|---|
basic-text-public |
Наименьшая реалистичная форма SealedQub; без необязательных полей. Устанавливает каноническую форму обёртки для qub, типичного для v1.0. |
with-recipient-pubkey |
SealedQub с установленным recipient_pubkey (путь Phase 2). Другой набор внутренних ключей CBOR, другой qub_id. |
longer-body |
Тело ~4 KiB — упражняет многобайтовые префиксы длины CBOR как во внутреннем envelope, так и во внешнем шифротексте. |
Реализации ДОЛЖНЫ выдавать побайтово идентичный expected_wrapper_hex для записанных входов. Регенерация фикстуры требует QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors и зарезервирована для намеренных изменений формата.
15. Управление криптографическим профилем (будущее)
Этот раздел является информативным для v1 и становится нормативным в тот момент, когда второй алгоритм впервые войдёт в любую из криптографических примитивов qub.
15.1 Текущая позиция
Протокол v1 привязывает ровно один алгоритм на примитив:
- Подпись: ML-DSA-65 (
sig_alg = 0x01; публичный ключ 1952 байта, подпись 3309 байт) и без подписи (sig_alg = 0x00). Реестр §9.2 не определяет других значений; верификатор v1 ДОЛЖЕН отклонять каждыйsig_algвне{0x00, 0x01}. Будущая запись Ed25519 предвидится (§15.3), но не выделяется в v1. - Timelock: только drand quicknet — хеш chain, публичный ключ, время генезиса и период являются фиксированными сетевыми параметрами, переносимыми эталонной реализацией
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) иconfig/drand-endpoints.json. - Внешняя обёртка: только AES-256-GCM v1 (§13).
Верификаторы в настоящее время жёстко кодируют длины ключа и подписи на примитив. Формат передачи не предоставляет никакой поверхности гибкости.
15.2 Предполагаемая форма
Когда второй алгоритм войдёт в протокол, верификатор будет настроен на именованный CryptoProfile (например, ExqubV1), перечисляющий точный набор разрешённых значений на примитив — sig_algs, chains drand, версии обёртки, типы контента. Профиль фиксируется во время верификации, никогда не согласуется по основному каналу. Любое значение вне активного профиля отклоняется.
Это гарантирует, что добавление ML-DSA-87 или активация Ed25519 не может задним числом ослабить существующие конфигурации верификаторов: верификатор v1 остаётся верификатором v1 даже после публикации профиля v2.
15.3 Условия активации
Повышайте §15 до нормативного статуса, когда предлагается любое из следующего:
- Второй байт
sig_alg(активация Ed25519, ML-DSA-87 или любая новая запись в реестре §9). - Вторая chain drand в продакшен-использовании.
- Вторая версия внешней обёртки.
До тех пор §15 является заглушкой, которая фиксирует форму миграции, чтобы будущие PR приземлялись против известной цели, а не пере-обсуждали поверхность согласования с нуля.
16. Журнал прозрачности и уровни долговечности (дизайн — обзор завершён)
Статус. Этот раздел является спецификацией дизайна. Сетевые форматы, хеширование и модель доверия ниже нормативны для реализации, но код журнала прозрачности пока не выпущен. Внешний обзор W5 завершён: §16.15 фиксирует принятые решения и обязательные ограничения запуска, вытекающие из него. Реализация может продолжаться в рамках этих ограничений. §16 остаётся ориентированным на будущее в том же смысле, что и §15 — он фиксирует цель, чтобы реализация попала в устоявшийся дизайн, а не переводила модель доверия в коде ревью заново. Он строго аддитивен — каждый существующий qub сохраняет свою индивидуальную транзакцию Arweave, и сетевой формат
SealedQub/QubEnvelopeне изменяется.
16.1 Обоснование и уровни долговечности
Сегодня долговечность qub и его временное обязательство опираются на единую транзакцию Arweave для каждого qub (§11). Это связывает задержку запечатывания с финальностью Arweave, делает загрузку каждого qub продуктовым потолком стоимости (ARWEAVE_DAILY_CEILING) и не даёт защищённого от подделки упорядочивания между qub. Журнал прозрачности добавляет два слоя под и вокруг этого единственного уровня:
| Уровень | Название | Гарантия | Когда |
|---|---|---|---|
| T1 | Синхронное подтверждение R2-first | Минимум долговечности — запечатанные байты записываются в долговечное хранилище до возврата запечатывания (< 300 ms p95). |
Каждый qub, синхронно (§16.10). |
| T2 | Пакетное включение в журнал прозрачности | Универсальное, только пополняемое, защищённое от подделки обязательство + полное упорядочивание, привязанное к Arweave. | Каждый qub, отложенно + пакетно (§16.5–16.7). |
| T3 | Постоянство Arweave для каждого qub | Индивидуальная транзакция Arweave для qub. | Платная допродажа и резерв при недоступности Arweave (§16.8). |
T2 делает Arweave-для-каждого-qub выбором (T3), а не единственным путём долговечности. ARWEAVE_DAILY_CEILING снимается как продуктовый потолок и понижается до предохранителя только на выделенном кошельке привязки (§16.7); запечатывания пользователей никогда не отклоняются за его превышение.
Честность долговечности (решено — §16.15 Q6). Долговечность не регрессирует: запись T1 в R2 синхронна и однократна, поэтому qub бесплатного уровня, не купивший T3, полностью долговечен в тот момент, когда запечатывание возвращается. Грубеет проверяемое верхнее ограничение времени обязательства: для бесплатного qub оно становится временем блока привязки, а не временем блока транзакции для каждого qub. При низком объёме — реалистичное состояние раннего запуска и непиковых часов — полная суточная каденция является типичным минимумом, а не редким краевым случаем. Поэтому продуктовая формулировка — это верхняя граница без зафиксированной числовой задержки — «запечатано и долговечно сейчас; независимая публичная метка времени добавляется при следующей привязке журнала (обычно ежедневно)» — а доказательство обязательства с точностью до часа является платным свойством T3, раскрываемым на поверхности сравнения уровней и в условиях (§16.11, §16.15 Q6). Любое временное ограничение является только внутренним SLO, никогда не маркетинговым SLA.
16.2 Структура LogLeaf (две зафиксированные формы)
Запись журнала — это LogLeaf, кодируемая как рукописный канонический CBOR по профилю §3.1 (определённая длина, без тегов, без чисел с плавающей точкой, целые в кратчайшей форме, текст NFC, необязательные поля опускаются при отсутствии, ключи упорядочены по закодированной длине байт по возрастанию, затем побайтово). Канонический страж §3.1 parse → re-encode → compare применяется на пути кодирования до хеширования (а не только при декодировании), поэтому две реализации не могут разойтись в байтах листа из-за разницы в ширине целого или порядке ключей. Все целые — u8 / u64 / i64; все дайджесты — 32-байтовые байтовые строки (bstr[32]). Хранимый идентификатор транзакции Arweave — это сырой 32-байтовый дайджест SHA-256, переносимый как bstr[32], никогда не как текстовая строка base64url (соответствует §3.3).
Лист имеет две формы, выбираемые байтом kind, потому что на пути загрузки по умолчанию Worker байт-слеп: POST /api/v1/upload получает только qub_id и unlock_at как недоверенные клиентские утверждения — body_hash, drand_round, created_at и drand_chain_version все запечатаны внутри внешней обёртки §13, ключа которой Worker никогда не держит. Только путь серверного запечатывания (POST /api/v1/seal) выводит body_hash / drand_round из открытого текста. Единая форма листа, несущая body_hash + drand_round, поэтому фиксировала бы значения, которые оператор никогда не верифицировал для большинства реальных qub. Разделение сохраняет каждое зафиксированное значение честным:
| Ключ | Закодированных байт | Тип | Присутствие | Значение |
|---|---|---|---|---|
seq |
4 | u64 |
обязательно | Глобальный 0-based индекс листа; позиция, к которой привязывается доказательство включения. |
kind |
5 | u8 |
обязательно | 0x01 засвидетельствованный (серверное запечатывание) или 0x02 заявленный (клиентское запечатывание / байт-слепая загрузка). |
ref |
4 | bstr[32] |
обязательно | Идентификатор-ссылка листа. Засвидетельствованный → сырой qub_id. Заявленный → ослеплённый идентификатор SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
обязательно | Адрес содержимого SHA3-256(stored_bytes) — единственная привязка к содержимому, которую Worker всегда может честно вычислить, на обоих путях. |
unlock_at |
10 | i64 |
обязательно | Скопировано (засвидетельствованный) или заявлено (заявленный); валидируется > 0 до того, как попадает в лист. |
received_at |
12 | i64 |
обязательно | Настенные часы Worker на момент R2-подтверждения. Не-доказательное (утверждается оператором; §16.6). Присутствует для самоописания, никогда не доказательство. Валидируется > 0. |
body_hash |
10 | bstr[32] |
только kind=0x01 |
Опускается на 0x02 — у Worker его нет по §13. |
drand_round |
12 | u64 |
только kind=0x01 |
Опускается на 0x02. |
Лист kind=0x02 намеренно не фиксирует ни body_hash, ни drand_round: он свидетельствует обязательство и упорядочивание непрозрачного шифротекста по адресу содержимого chash, заявляя qub_id и unlock_at — а не его открытый текст или раунд. Ветви открытого текста / раунда для заявленного qub берутся из существующей верификации .qub-бандла §11, а не из журнала (§16.11). drand_chain_version отсутствует в листе (он внутри обёртки на пути по умолчанию); гранулярность chain живёт на привязке (§16.7). Дисциплина кодировщика: отклонять полностью нулевой ref или chash и отклонять неположительные unlock_at / received_at, зеркаля страж-сентинель outcome_at > 0 в cbor.rs.
16.2.1 Ослепление приватного qub
Журнал не должен стать оракулом перечисления, который внешняя обёртка §13 существует, чтобы предотвратить (§13.1). Для приватного (обёрнутого) qub asserted-лист фиксирует ослеплённый идентификатор SHA3-256(qub_id ‖ log_blind_secret), где log_blind_secret — это секрет, хранимый сервером, и опускает body_hash. Третья сторона не может связать такой лист с конкретным qub_id; держатель qub, у которого есть ссылка доставки и, следовательно, qub_id, может пересчитать ослепление, чтобы подтвердить собственное включение. Публичный qub (уже перечислимый, уже несущий тэг Arweave Visibility: public по §13.8) фиксирует сырой qub_id. Это единственное место, где самостоятельная верифицируемость намеренно уступает несущему инварианту приватности; самостоятельная привязка для приватных qub — это chash (§16.9).
Хранение log_blind_secret (решено — §16.15 Q4). Ослепление защищает несвязываемость листов, а не конфиденциальность открытого текста (обёртка §13 держит её независимо). При компрометации log_blind_secret для любого qub_id, который противник уже держит или может реконструировать (каждый qub, бандл/URL которого у него есть, плюс любой qub_id с низкой энтропией или публичный), он пересчитывает ref листа за один хеш и связывает его — это прямая связка известной популяции, а не перебор по неизвестному пространству. Классифицируйте log_blind_secret как секрет уровня корреляции/Sybil в том же уровне хранения, что и другие серверные секреты, и ротируйте только вперёд (ротация заново ослепляет будущие листы; она не может задним числом развязать уже привязанные).
16.3 Хеширование листов и узлов
Хеширование с разделением доменов по RFC 6962 §2.1 с заменой SHA-256 на SHA3-256:
leaf_hash = SHA3-256(0x00 || canonical_cbor(LogLeaf))
node_hash(l,r) = SHA3-256(0x01 || l || r)
empty tree = SHA3-256("") // defined but never anchored
Байты доменных префиксов 0x02 (цепочка записей, §16.4) и 0x03 (хеш STH, §16.6) зарезервированы и не пересекаются с ними. Это одиночные байты и поэтому не могут коллизировать с существующими 10-байтовыми ASCII-разделителями доменов (QUB_ID_V2 и др.). Дерево — это RFC 6962 левополное несбалансированное дерево (каждое внутреннее разбиение на наибольшей степени двойки строго меньшей числа листьев поддерева), что позволяет доказательствам включения и согласованности использовать один алгоритм аудиторского пути. Эталонная спецификация несёт явный псевдокод вывода left/right и закрепляет тестовый вектор не степени двойки (5 листьев), чтобы случай продвижения правого края — который вектор из 4 листьев скрывает — был упражнён.
16.4 Цепочка хешей (внутренняя)
LogDO поддерживает внутреннюю цепочку записей только для crash-консистентности. Она никогда не публикуется и не обращена к верификатору:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
Публикуемая только-пополняемая авторитетная инстанция — это кумулятивный корень Меркла + его привязка (§16.5–16.6), а никогда не сырой порядок, в котором оператор случайно обслуживает листья: цепочка пересчитывается для любого обслуженного порядка, поэтому только привязанный корень закрепляет каноническую позицию.
16.5 Кумулятивное дерево Меркла и пакетирование
Существует одно постоянно растущее дерево RFC 6962 над всеми листьями в порядке seq — а не изолированные деревья на каждый пакет. (Конструкция с пакетными деревьями, сцепленными перенос-листом, была отклонена: это не истинное отношение префикса, поэтому её «доказательства согласованности» несостоятельны.) Кумулятивное дерево даёт настоящие доказательства согласованности RFC 9162 и позволяет одной недавней привязке доказать включение для любого более старого qub.
Durable Object LogDO является единственным писателем (blockConcurrencyWhile, зеркаля QuotaDO / EntitlementDO) — добавление в общий журнал является чтением-модификацией-записью разделяемого состояния и поэтому ДОЛЖНО проходить через DO, никогда через KV. Он кэширует фронтир правого края дерева (O(log n) хешей), поэтому закрытие пакета — это O(batch). Пакет — это набор листьев, привязываемых вместе; его триггеры конфигурируемы, а не заморожены в протоколе: продвижение tree_size хотя бы на LOG_BATCH_MAX_LEAVES (по умолчанию 4096), или достижение возрастом каденции привязки, или принудительный сброс при поступлении платного запечатывания T3. root_i — это кумулятивный Merkle Tree Hash над листьями 0 .. tree_size_i.
16.6 Подписанная вершина дерева через привязку к Arweave
Транзакция привязки Arweave является подписанной вершиной дерева (Signed Tree Head) и заменяет подпись оператора для самой вершины дерева: суточная привязка не нуждается в qub-ключе, потому что owner транзакции Arweave и есть подпись. Тезис рва сохраняется — несущим для привязанного корня является неизменный субстрат, а не секрет, хранимый qub.
В дизайне есть ровно один тёплый qub-ключ подписи, и он закреплён: ключ квитанции на каждое запечатывание (§16.10). Его публичный ключ фиксируется в LogProfile (распространяется с верификатором) и кросс-подписывается anchor_owner, поэтому верификатор валидирует квитанцию против того же закреплённого корня, что и привязку. Это решение §16.15 Q2 — незакреплённый, ротируемый оператором ключ квитанции был бы отрицаем (оператор мог бы отрицать, что ключ его), что свело бы на нет ценность подотчётности квитанции против противника уровня оператора, ради сдерживания которого квитанция и существует. Итак: qub не держит незакреплённого ключа подписи журнала; ключ квитанции закреплён и кросс-подписан anchor_owner.
SignedTreeHead — это канонический CBOR (ключи по закодированной длине): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (предыдущий sth_hash; генезис = 32 нулевых байта), log_id:bstr[32], first_seq:u64, anchored_at:i64. Его хеш — это sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Закреплённый корень доверия. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). Соответствующий верификатор ДОЛЖЕН требовать anchor_tx.owner == LogProfile.anchor_owner, где anchor_owner (и публичный ключ ключа квитанции) запечён в qub_core как LogProfile — наряду с константами quicknet, уже находящимися в DrandTimelockProvider::quicknet() — и распространяется с бинарником верификатора. Верификатор ДОЛЖЕН также верифицировать привязку данных → tx_id транзакции Arweave локально, а не доверять ответу /raw/ шлюза. Это закрывает дыру раздвоения с мошенническим кошельком: «привязано на Arweave» бессмысленно, пока верификатор не закрепит, какой именно кошелёк.
Ротация — это расширение управления §15, а не повторное использование (решено — §16.15 Q3). Поверхность профиля §15.2 в настоящее время перечисляет только sig_algs / chains drand / версии обёртки / типы содержимого, а триггеры §15.3 не перечисляют ни одного из них — LogProfile / anchor_owner ещё не входит в поверхность §15. Поэтому управление ротацией должно быть построено: §15.3 расширяется (ниже), чтобы добавить триггер LogProfile, а ротация — это подписанное повышение LogProfile, поставляемое в обновлении верификатора. Плановая ротация несёт кросс-подпись исходящий → входящий; ротация, вызванная компрометацией, не может (исходящий ключ недоверен/недоступен именно тогда) и откатывается к управляемому §15 повышению, при этом проверка форка по предыдущей привязке (ниже) ограничивает ущерб в промежутке.
Окно раздвоения (первоклассный параметр доверия). Лист устойчив к раздвоению только после того, как его покрывающая привязка подтверждена на Arweave. Окно — это received_at → подтверждение привязки (≤ каденция + финальность Arweave). Внутри него единственные гарантии — это закреплённая квитанция запечатывания (§16.10) и операционная целостность qub. Три артефакта подотчётности делают это честным, а не голословным (модель свидетеля — это решение §16.15 Q2):
- Закреплённая подписанная квитанция запечатывания — аналог SCT, возвращаемый в ответе загрузки (§16.10), подписанный закреплённым, кросс-подписанным
anchor_ownerключом квитанции. Лист, отброшенный до его привязки, оставляет жертве неотрекаемую квитанцию для публикации, закрывая дыру тихого пропуска. - Опубликованная методология мониторинга + обход цепочки prev — цепочка
prevпривязки обходится от вершины к генезису; форк (две привязки на одномsizeс разнымrootили сломанныйprev) является публикуемым доказательством злоупотребления. Обнаружение раздвоения — это заявленное операционное обязательство, а не молчаливое допущение. - Двойные самопубликуемые вершины — каждая новая вершина
{sth_hash, tree_size}публикуется в выделенный, принадлежащий qub публичный, только-пополняемый репозиторий GitHub (несущая защищённая от подделки ветвь самопубликации), при этом пост в соцсети — это только best-effort подтверждение. Неудачная публикация ДОЛЖНА вызвать пейджинг (а не тихо упасть).
Граница честности (обязательное ограничение). Поскольку qub контролирует обе поверхности публикации, это самопубликация, а не независимое засвидетельствование. Никакая продуктовая, маркетинговая или юридическая поверхность не может утверждать, что журнал «независимо засвидетельствован»; разрешённое утверждение — что раздвоение обнаружимо и оставляет неотрекаемую квитанцию. Настоящий независимый сторонний свидетель отложен до будущего повышения управления §15.
received_at утверждается оператором, и ни одно утверждение не может опираться на него — оно никогда не подаётся как доказательство или как подтверждение в споре ни на какой продуктовой / юридической / API / доказательственной поверхности. Время блока привязки Arweave T — единственная бездоверительная метка времени (верхняя граница «залогировано до»). Любая проверка вменяемости мониторинга по received_at ДОЛЖНА сравнивать с T, а не с контролируемым оператором полем STH anchored_at; такая проверка — это страж только против ошибки часов честного оператора, а не контроль подотчётности против злонамеренного оператора (§16.15 Q5).
16.7 Формат транзакции привязки и каденция
AnchorBundle — это тело транзакции Arweave в каноническом CBOR, записываемое через бандлер §16.8: ver:u8, sth:bstr (канонические байты SignedTreeHead), prev_anchor:bstr (сырые байты id предыдущей транзакции привязки; опускается в генезисе), chain_hash:tstr (действующая chain drand — quicknet) и поток листьев-CBOR пакета в порядке seq, чтобы привязка была самодостаточной: монитор заново выводит root из тела с нулевой зависимостью от qub. (Если поток листьев становится большим при высоком объёме, будущая ревизия может фиксировать только диапазон листьев по ссылке; отмечено, не принято в v1.)
Тэги Arweave намеренно перечислимы — журнал предназначен для нахождения, в отличие от приватных qub: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Тэги — это недоверенные подсказки; тело CBOR — единственная авторитетная инстанция.
Каденция: ежедневно по умолчанию, пересматривается с объёмом (триггер размера авто-сокращает эффективную каденцию под нагрузкой); платное запечатывание T3 форсирует привязку, чтобы платящие клиенты никогда не ждали сутки. Кошелёк привязки выделен и низкоскоростной, отделён от кошелька загрузки — он ДОЛЖЕН быть собственным JWK (отдельный ключ, а не логическая роль на кошельке загрузки), чтобы компрометация кошелька загрузки не могла подделать привязки — с жёстким суточным бюджетом транзакций привязки (понижённый ARWEAVE_DAILY_CEILING). Поза хранения изложена прямо: узкоохватный тёплый ключ с тугим предохранителем и низким балансом, а не «холодный» — кошелёк, авто-подписывающий ежедневно, не может быть холодным, и спецификация не делает вид, что это так.
16.8 Бандлер ANS-104
Собственный кодировщик DataItem ANS-104 и подписант deep-hash, примерно 300 строк, только Web Crypto, ноль npm-зависимостей (оба Turbo SDK не проходят supply-chain страж npm ci --ignore-scripts). Байтовая раскладка DataItem:
signatureType (2, LE) || raw_signature || owner || target(flag+0|32) || anchor(flag+0|32) || num_tags(8, LE) || tags_len(8, LE) || avro_tags || data
Подписание — это Arweave deepHash — рекурсивный дайджест SHA-384 (требование Arweave на проводе, crypto.subtle.digest("SHA-384")) над ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — затем RSA-PSS над deep hash с JWK кошелька через crypto.subtle; id = base64url(SHA-256(signature)). SHA-384 здесь карантинирован как примитив только-для-провода-Arweave, никогда не примитив доверия qub (§15 фиксирует ограждение; хеширование доверия qub — это SHA3-256 повсюду).
Один путь кода обслуживает трёх потребителей: платное постоянство T3 для каждого qub, резерв при недоступности Arweave (поставить DataItem в очередь, вернуть подтверждение R2-first в любом случае — это закрывает текущий тупик ARWEAVE_UNAVAILABLE 503) и запись AnchorBundle. Схема подписи (решено — §16.15 Q8): v1 подписывает RSA-PSS (тип подписи 1), переиспользуя существующий механизм JWK кошелька Arweave (ноль нового хранения долгоживущих ключей, обслуживая тезис «на один секрет меньше»); Ed25519 отложен на путь PQ-миграции §15.
Рукописный deep hash — это код наивысшего риска и наименьшего естественного покрытия в W5, поэтому его контроль не подлежит обсуждению (§16.15 Q8):
- Кросс-языковая фикстура
tlog_v1.json(Rust + TS, паттернwrapper_v1.json§14.5) покрывает deep-hash, байты DataItem + id, хеши листьев, корень из 5 листьев + аудиторский путь, хеш STH, доказательство включения и доказательство согласованности — в обоих направлениях, подписи и верификации (направление верификации важно, потому что локальная проверка tx → tx_id §16.6 втягивает deep hash в каждый самостоятельный верификатор, а не только в писателя). - Однократный interop round-trip через эталонный бандлер ANS-104, потребляемый как только статические тестовые данные — никогда npm-зависимость времени выполнения (поза только-Web-Crypto / без install-скриптов сохраняется).
- Путь deep-hash + RSA-PSS должен round-trip через те же примитивы
crypto.subtle, что использует продакшен, чтобы собственный кодировщик был байт-совместим. - Постоянный пост-бандл монитор приёмки подтверждает, что каждый DataItem привязки / резерва действительно достигает приёмки Arweave, с тревогой + предохранителем — потому что deep hash также обслуживает очередь резерва при недоступности Arweave, поэтому тихая регрессия заполнила бы эту очередь отклонёнными сетью элементами именно во время того сбоя, для покрытия которого она существует.
16.9 Доказательства включения и согласованности
Оба — RFC 9162, SHA3-256, обслуживаются как канонический CBOR.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (точный CBOR листа — верификатор сам пересчитывает leaf_hash и никогда не доверяет предоставленному хешу), index:u64, size:u64, audit:[bstr[32]], root:bstr[32], anchor:{ txid:bstr[32], batch:u64, sth:bstr[32], log_id:bstr[32], block_height?:u64, anchored_at?:i64 }.
ConsistencyProof — GET /api/v1/log/consistency?first=<size_a>&second=<size_b>: ver:u8, first_size:u64, second_size:u64, first_root:bstr[32], second_root:bstr[32], nodes:[bstr[32]], first_anchor, second_anchor. Единый однозначный список ключей, закреплённый тестовым вектором.
Самостоятельная верификация (без сервера qub, расширяет §11):
1. Parse .qub bundle → SealedQub; recompute qub_id (§4.1).
2. Read leaf.kind.
3a. kind=0x01 (attested):
assert leaf.ref == qub_id
assert leaf.body_hash == SHA3-256(body)
assert leaf.drand_round == unlock_round(unlock_at)
3b. kind=0x02 (asserted):
assert leaf.ref == SHA3-256(qub_id || blind) // holder supplies blind
OR treat ref as opaque and bind via leaf.chash == SHA3-256(stored_bytes)
4. Recompute leaf_hash = SHA3-256(0x00 || leaf); fold `audit` per RFC 6962
using index/size; require derived root == proof.root.
5. Fetch anchor.txid from any gateway; verify the tx data → tx_id binding
(do not trust a gateway /raw/ response); REQUIRE anchor_tx.owner ==
LogProfile.anchor_owner.
6. Parse AnchorBundle; require committed root == proof.root and size ==
proof.size; read the Arweave block time T.
7. Emit the claim scoped by leaf.kind (§16.11).
Хранилище для обслуживания доказательств ДОЛЖНО быть с координатными ключами (решено — §16.15 Q7, блокирующая предпосылка). Генерация доказательств для холодного листа нейтральна к корректности только если аудиторский материал R2 является постоянным хранилищем узлов Меркла с ключом по абсолютной координате дерева (level, index) — а не дельтами узлов на каждый batch. С хранилищем по координатам любой аудиторский путь (leaf i, size N) — это набор из O(log N) прямых R2 GET без пересчёта через границы пакетов; с хранилищем по batch это не так, и именно этот пробел в раскладке хранения закрывает это решение. Тела листьев точно так же адресуемы по содержимому через seq. Тестовый вектор W5 ДОЛЖЕН доказать холодный лист эпохи генезиса против гораздо более позднего корня, используя только R2 + Arweave при стёртом хранилище LogDO, чтобы заявление о безопасности рекламации в §16.13 было подкреплено, а не голословно. Последовательные R2 GET O(log N) принадлежат только асинхронному эндпоинту доказательств — никогда горячему пути запечатывания (§16.10) или крону на каждый тик.
16.10 Упорядочивание подтверждения R2-First
Последовательность POST /api/v1/upload становится:
- Гейты передней половины (auth, валидация, ключ шарда идемпотентности) — без изменений.
- Синхронно
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— минимум долговечности; также закрывает гонку прекеша W1 (ранееctx.waitUntilпосле сабмита Arweave). - Синхронно
await LogDO.append(leaf)— один in-colo RPC DO; единственный писатель назначаетseq, расширяет цепочку записей и обновляет фронтир. (RMW над разделяемым состоянием → DO, никогда KV.) RPCappendделает только это — работа закрытия пакета МерклаO(batch)идёт вне этого RPC на тревоге LogDO, иначе p95appendскачет на каждомLOG_BATCH_MAX_LEAVES-м запечатывании. - Вернуть подтверждение сейчас — с квитанцией запечатывания (подписанной закреплённым ключом квитанции, §16.6) и
{ tx_id, log_seq, anchor_status: "pending" }. Многосекундный fan-out Arweave убран из критического пути. - Один
ctx.waitUntilставит в очередь отложенную работу: сабмит Arweave для каждого qub (теперь best-effort / платный; при сбое маршрутизируется в очередь резерва бандлера, а не 503-ит пользователя) плюс существующие записи провизорных метаданных. Закрытие пакета и привязка идут независимо от тревоги LogDO и суточного крона привязки. Никакогоctx.waitUntilвнутри цикла; существующий ключ шарда идемпотентности сохраняется.
Бюджет задержки (решено — §16.15 Q7). Цель < 300 ms p95 — это измеряемый гейт запуска, а не допущение. Честный критический путь — это чтения KV передней половины + один R2 PUT + два сериализованных Durable Objects — существующий дебет квоты запечатывания QuotaDO и новый append LogDO — поэтому бюджет должен учитывать два in-colo DO round-trip, а не один. Поставьте тревогу задержки LogDO, зеркалящую QuotaDO, и считайте регрессию p95 блокером релиза.
16.11 Модель доверия — точное утверждение, ограниченное видом листа
Для kind=0x01 (засвидетельствованный): «Это содержимое — тело, соответствующее body_hash, идентифицированное qub_id — было зафиксировано в только-пополняемом журнале qub на позиции seq и существовало не позднее времени блока Arweave T; оно было криптографически нечитаемо до раунда drand R = unlock_round(unlock_at).» Это полная тройка {привязка раунда tlock + включение Меркла + привязанный корень}.
Для kind=0x02 (заявленный, по умолчанию): «Непрозрачный шифротекст с адресом содержимого chash, заявляющий qub_id и unlock_at, был зафиксирован в только-пополняемом журнале на позиции seq и существовал не позднее времени блока Arweave T.» Ветви раунда и тела поставляются существующей верификацией .qub-бандла §11 (qub_core::unlock), а не журналом; то, что журнал добавляет поверх голой транзакции для каждого qub, — это защищённое от подделки упорядочивание, бездоверительное верхне-граничное время обязательства и устойчивость к раздвоению.
Оба утверждения исключают, по §11: авторство без sig_alg ≥ 0x01, намерение и тайминг с гранулярностью тоньше привязки. Ни одно не позволяет утверждению опираться на received_at.
Потолок утверждения (обязательное ограничение запуска — решено §16.15 Q1). Для бесплатных / по-умолчанию (kind=0x02) qub ограниченное kind=0x02 утверждение выше является потолком того, что любая продуктовая, маркетинговая, договорная или доказательственная поверхность может утверждать. Никакая поверхность не может заявлять или подразумевать, что журнал доказывает содержимое или раунд разблокировки qub по умолчанию — журнал доказывает упорядочивание + бездоверительное верхне-граничное время обязательства непрозрачного шифротекста. Доказательство содержимого и раунда исходит исключительно из существующей верификации .qub-бандла §11, которая независима от журнала. Это жёсткий блокер запуска для копирайта, а не стилистическое предпочтение; это решение сохраняет байт-слепой путь по умолчанию честным.
16.12 Версионирование и координация с W3
Нет повышения сетевого формата SealedQub и поэтому нет повышения версии протокола (§12.1): журнал — это сайдкар, который фиксирует существующие поля и байты, поэтому он не входит в историю версий протокола §12.2. Необязательный drand_chain_version из W3 не затронут и остаётся единственным необязательным полем SealedQub. Вместо этого журнал вводит собственные независимые пространства версий — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — зеркаля независимость версии обёртки §12.4 (обёртка несёт байт версии, независимый от версии протокола, и версии журнала следуют тому же разделению).
Доставка доказательства по умолчанию запрашивается, с необязательным попутчиком. Доказательство не может существовать во время запечатывания (привязка ещё не записана), поэтому .qub-бандл времени запечатывания остаётся без доказательства. Верификатор W7 запрашивает GET …/proof единожды, или в полностью офлайн-режиме реконструирует доказательство из публичного AnchorBundle через запрос Arweave по Log-Id. .qub-бандл (W7) резервирует необязательный член inclusion_proof — отсутствует при запечатывании, заполняется пост-привязочным ре-экспортом для холодного архивирования — следуя тому же паттерну «необязательно, опускается по умолчанию, аддитивно», что и drand_chain_version из W3.
16.13 Хранение
Окна хранения для открытого хвоста LogDO, субстрата обслуживания доказательств R2, счётчиков предохранителя привязки и очереди резерва бандлера специфицированы в docs/DATA-RETENTION.md. Принцип: горячее хранение журнала на каждую запись (LogDO) рекламируемо после привязки; его аудиторский материал — хранилище узлов Меркла с ключом по координате (level, index) + тела листьев с адресацией по seq (§16.9) + привязки Arweave — постоянен. Рекламация холодного листа из DO никогда не аннулирует выданное доказательство, потому что доказательство разрешается против этого постоянного хранилища узлов R2 и привязки Arweave, а не против DO (и тестовый вектор стёртого DO §16.9 это доказывает).
16.14 Тестовые векторы
W5 поставляет кросс-языковую фикстуру tlog_v1.json (§16.8) плюс проработанные векторы: лист kind=0x01 и лист kind=0x02 → leaf_hash; кумулятивный корень из 5 листьев; одно доказательство включения; одно доказательство согласованности; один AnchorBundle; и один id DataItem. Они живут рядом с векторами внешней обёртки §14.5 и упражняются обеими реализациями — Rust (qub-core) и TypeScript (Worker).
16.15 Решения обзора (W5 — приняты)
Внешний обзор W5 (адверсариальный проход по дизайну + утверждение владельцем) завершён. Каждое решение ниже принято и отражено в тексте §16 выше; обязательные ограничения запуска повторно изложены в конце. Реализация может продолжаться в их рамках.
- Честность листа пути по умолчанию (
kind=0x02) — РЕШЕНО. Поставить разделение на два вида листа как специфицировано:kind=0x02не фиксирует ниbody_hash, ниdrand_round. Никакого поля*_body_hashна байт-слепом пути (оно было бы самым читаемым ложным сигналом «верифицировано» для интеграторов и удобством, которое §11 уже предоставляет из бандла). Не требовать серверного запечатывания для qub, засвидетельствованных журналом (это протолкнуло бы открытый текст через Worker и разрушило бы ров криптошреддинга). Любое самоописательное замыкание принадлежит.qub-бандлу / конверту доказательства как поле, пересчитываемое верификатором, никогда не полю листа. Потолок утверждения, подтверждённый владельцем: §16.11. - Подотчётность раздвоения / пропуска — РЕШЕНО. Ключ квитанции запечатывания закреплён в
LogProfile+ кросс-подписанanchor_owner(закрывая прежнее противоречие «нет ключа подписи»; §16.6). Модель свидетеля запуска: закреплённая квитанция + методология мониторинга + обход цепочки prev + двойные самопубликуемые вершины (принадлежащий qub публичный репозиторий GitHub, соцсети best-effort), маркетируемая как обнаружимая + квитированная, никогда как независимо засвидетельствованная. Настоящий сторонний свидетель отложен на повышение управления §15. - Закреплённый корень доверия anchor-owner + ротация — РЕШЕНО. Принять закрепление
LogProfile(§16.6); верификатор проверяетanchor_tx.owner == anchor_ownerи верифицирует привязку данных → tx_id локально. Управление ротацией — это расширение §15 для построения (триггер §15.3 добавлен), а не повторное использование; плановые ротации кросс-подписывают, ротации, вызванные компрометацией, откатываются к повышению §15 с проверкой форка, ограничивающей ущерб. - Ослепление листа приватного qub — РЕШЕНО. Сохранить ослепление для приватных qub (
ref = SHA3-256(qub_id ‖ log_blind_secret)), сыройqub_idдля публичных qub (уже §16.2.1),chashкак самостоятельную привязку.log_blind_secret— это секрет уровня корреляции/Sybil, ротация только вперёд (§16.2.1). received_at— РЕШЕНО. Сохранить его в листе, зафиксированным, но явно не-доказательным; никогда не подавать как доказательство или подтверждение спора ни на какой поверхности. Любая проверка вменяемости мониторинга сравнивает с временем блока ArweaveT, а не с контролируемым операторомanchored_at(§16.6).- Доказуемый тайминг бесплатного уровня — РЕШЕНО (утверждение владельцем). Долговечность не регрессирует; грубеет только доказуемое верхне-граничное время обязательства до времени блока привязки. Копирайт бесплатного уровня не использует числовой SLA («…добавляется при следующей привязке журнала, обычно ежедневно»); доказательство с точностью до часа — это платное свойство T3, раскрываемое на поверхности сравнения уровней + в условиях (§16.1).
- Кумулятивное дерево на Workers — РЕШЕНО. Единое кумулятивное дерево RFC 9162 + единственный писатель LogDO с кэшированным фронтиром (комфортный запас против потолка ~1k записей/сек DO; отложить шардирование Merkle-of-shard-roots до приближения к нему). Блокирующая предпосылка: хранилище узлов R2 с ключом по координате
(level, index)+ тестовый вектор холодного листа со стёртым DO (§16.9);< 300 ms— это измеряемый гейт запуска над двумя сериализованными DO (§16.10). - Схема подписи ANS-104 + deep-hash — РЕШЕНО. RSA-PSS (тип подписи 1, переиспользуя JWK выделенного кошелька привязки); Ed25519 отложен на путь PQ §15. Рукописный deep hash SHA-384 контролируется кросс-имплементационной фикстурой в обоих направлениях, interop-проверкой только-со-статикой через эталонный бандлер, round-trip с общим
crypto.subtleи пост-бандл монитором приёмки Arweave (§16.8).
Обязательные ограничения запуска (перенести в реализацию + продуктовый/юридический обзор):
- Потолок утверждения (Q1/Q6). Никакая поверхность не может говорить, что журнал доказывает содержимое или раунд разблокировки qub по умолчанию; разрешённое утверждение — упорядочено, защищённо от подделки, с бездоверительным верхне-граничным временем обязательства. Копирайт метки времени бесплатного уровня не несёт числовой задержки; доказательство с точностью до часа — только платный T3.
- Честность свидетеля (Q2). Маркетировать раздвоение как обнаружимое + квитированное, никогда как независимо засвидетельствованное.
- Ключи квитанции + привязки (Q2/Q8). Ключ квитанции закреплён + кросс-подписан; кошелёк привязки — собственный JWK, отдельный от кошелька загрузки.
- Гейт deep-hash (Q8). Никакая транзакция привязки или T3 не поставляется, пока не пройдут фикстура в обоих направлениях + interop-проверка; монитор приёмки пейджит при сбое.
- Предпосылка хранения (Q7). Хранилище узлов с ключом по координате + вектор холодного листа со стёртым DO являются предпосылками для гарантии «рекламация никогда не аннулирует доказательство».