Спецификация протокола qub

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

Три примитива заставляют это работать. дранд является децентрализованным маяком случайности — дата раскрытия обеспечивается криптографически, а не по усмотрению qub. Прочное хранилище сохраняет признанные запечатанные байты, в то время как текущие пути публикации планируют отдельные транзакции постоянного хранения; успешные добавления в журнал общего загрузки могут дополнительно присоединяться к пакетным, закреплённым обязательствам. ML-DSA-65 является постквантовой цифровой подписью — когда включена авторизация, qub привязан к паре ключей, секрет которой никогда не покидает устройство автора.

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

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


Спецификация протокола qub

Поле Значение
Выпуск документа 1.0.0 (protocol-v1.0.0)
Проводной протокол 0x01
Внешняя оболочка 0x01
Дата вступления в силу 23.09.2026
Статус Текущий
Просмотрено 23.09.2026

Этот документ — нормативная спецификация протокола системы 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,              // 0x00 = private; 0x01 = public
    content_type:   u8,              // 0x01 text; 0x03 pact; 0x04 verdict
    plaintext:      Vec<u8>,         // Raw body bytes (UTF-8 for text)
    sender_label:   Option<String>,  // Display name; V2-signed when authorship is enabled
    title:          Option<String>,  // Plaintext countdown title; bound via title_hash
    reply_to:       Option<[u8; 32]>,// Parent qub_id; V2-signed when authorship is enabled
    outcome_at:     Option<i64>,     // Optional future judgment time; bound to qub_id
    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>,     // When reality renders judgment; bound to qub_id
    sender_label:        Option<String>,  // Not in qub_id; V2-signed when authorship is enabled
    reply_to:            Option<[u8; 32]>,// Parent qub_id; not in qub_id; V2-signed when present
    body:                Vec<u8>,         // UTF-8 text or canonical CBOR pact/verdict body
    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; поля для подписи и созаёмщика отсутствуют. Могут присутствовать другие необязательные поля метаданных.

Другие конфигурации 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). Это внутренний артефакт канала: публичная доставка хранит эти байты напрямую, тогда как приватная доставка оборачивает их в OuterWrapper перед хранением (§13).

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x00 = private/wrapped; 0x01 = public/bare
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // 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
    drand_chain_version: Option<u8>,    // W3 — chain-migration version. Absent / 0 = quicknet
                                        //   (the only chain today). Lets a future chain swap
                                        //   be expressed on the wire without a breaking format
                                        //   change. NOT part of the §4.1 qub_id preimage, so its
                                        //   addition never alters an existing qub's identity.
    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 hostile/control code points.
}

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>,       // Carried from both wire layers; drives the verdict-watch block
    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 (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 (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)
"drand_chain_version" (20 encoded bytes) ← only if present (W3; absent = quicknet)

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 кодировка: Ревизия предварительной реализации увеличила предварительное изображение с 92 до 100 байт, чтобы объединить опциональное outcome_at поле в привязку. Отсутствует outcome_at кодируется как 8 нулевых байтов; протоколные валидаторы отклоняют outcome_at <= 0 повсюду, поэтому этот страж не может сталкиваться с допустимым значением. См. §3.2 (формат передачи данных) и встроенное дерево tasks/verdict-uplift-plan.md для механизма вердикта, который мотивирует это поле.

drand_round кодировка: Поздняя предварительная версия реализации увеличила размер исходного образа с 100 до 108 байт для свертки drand_round (целевой круг drand, §4.3) в привязку и увеличил разделитель области до QUB_ID_V2. Это связывает раунд таймлока с идентичностью qub: шлюз не может перепривязать шифротекст к другому (например, уже прошедшему) раунду, чем показанный unlock_at подразумевает. Процедура разблокировки (§8) дополнительно проверяет, что раунд, закодированный в зашифрованном разделе tlock, совпадает unlock_round(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 = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
Параметр Источник Пример
unlock_at Выбранные пользователем секунды Unix по UTC 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time информация о цепочке drand (genesis_time) 1595431050
chain_period_seconds информация о цепочке drand (period) 30

Это справочная карта блокировки (drand's CurrentRound). drand публикует раунд N в chain_genesis_time + (N - 1) * chain_period_seconds, поэтому формула выбирает ток по кругу на unlock_at — раунд, подпись которого является первой для зрителя, который только что прибыл unlock_at можно использовать.

Свойство выравнивания (случай, который имеет значение на практике): когда (unlock_at - chain_genesis_time) делится нацело на chain_period_seconds, подпись выбранного раунда опубликована ровно в unlock_at, никогда раньше этого. Это всегда верно для эталонного развертывания: время создания quicknet (1692803367) делится на его 3-секундный период, и эталонные приложения фиксируют время разблокировки пин-кода целыми минутами. Для невыравненного unlock_at, подпись выбранного раунда публикуется строго менее чем за один период до unlock_at — временная точность обязательства составляет один период маяка.

Предварительное отображение наследия и допуск с разблокировкой: оригинальная карта была ceil((unlock_at - chain_genesis_time) / chain_period_seconds), который—для приведённого выше случая с выравниванием по периоду—выбрал полностью опубликованный раунд одного периода перед unlock_at, делая шифротекст расшифровываемым раньше ровно на один период. Два отображения отличаются ровно +1 когда дельта делит период и согласны иначе. Потому что drand_round свернуто в неизменное qub_id предобраз (§4.1), артефакты, запечатанные по устаревшему отображению, не могут быть восстановлены; следовательно, проверяющие, выполняющие шаг 6a раунда §8, ОБЯЗАНЫ принять сохранённый drand_round равный либо полученный раунд или полученный круг минус один (и ОБЯЗАТЕЛЬНО требует, чтобы раунд блока tlock точно совпадал с сохранённым раундом). Допуск расширяет самую раннюю сигнатуру управления максимум на один период. Служба подготовки пакта применяет тот же допуск при повторном вычислении подготовленного пакта qub_id (на этапе и при со-подписи): если текущий раунд сопоставления не воспроизводит зафиксированное qub_id и дельта делит период, затем он повторяет попытку с раундом минус один, и он закрепляет финализированный пакт за каким бы раундом ни был qub_id фактически связывается — никогда слепо с пересчитанным раундом, что сделало бы артефакт навсегда недоступным для вывода.

Проверка: 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 bytes), value: String (≤ 2,000 bytes) } // NFC
PartyIdentifier{ label: String (≤ 100 bytes), contact: Option<String (≤ 320 bytes)> }

Канонические порядки ключей 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) ДОЛЖНЫ обеспечить:

  1. Только HTTPS. Строка ДОЛЖНА начинаться с байтовой последовательности https://. Любая другая схема — http, ftp, javascript, data, file и т. д. — отклоняется.
  2. Ограничение длины. ≤ 2 048 байт (практический предел URL в браузерах).
  3. NFC + проверка на враждебные кодпойнты. То же правило, что и для title и reflection — кодпойнты bidi-override / zero-width / tag-block / BOM / C0 / C1 отклоняются. Определение соответствует Rust crate::handle::contains_hostile_text_codepoint и TS workers/api/src/utils/unicode.ts::isHostileCodepoint (держите их синхронизированными).
  4. Без пробельных символов, без управляющих ASCII. Пробельные символы / DEL / байты ниже 0x20 где-либо в URL отклоняются — закрывает вектор инъекции \n/\t, который не покрывает правило bidi.
  5. Непустой сегмент хоста. Всё между 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.
    f. visibility is 0x00 (private) or 0x01 (public).
 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 = floor((unlock_at - chain_genesis_time) / chain_period_seconds) + 1
    (§4.3). (Computed here, before qub_id, because drand_round is bound into the
    qub_id preimage—§4.1.)
 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,
    visibility, unlock_at, drand_chain_id, and drand_round.
11. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12. Select the delivery shape from visibility:
    a. Private (0x00): generate K = 32 random bytes and N = 12 random bytes
       using a CSPRNG. Compute W = wrap_sealed_qub(SealedQubCbor,
       qub_id=qub_id, key=K, nonce=N) per §13. Upload payload = W.
    b. Public (0x01): upload payload = bare SealedQubCbor; do not generate K.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit the selected upload payload to the qub upload service. For a private
    browser seal, the service is byte-blind to the inner SealedQubCbor and never
    receives K. The Builder `/api/v1/seal` route is an explicit exception: it
    receives plaintext and caller-supplied K in memory, then persists neither.
16. Receive arweave_tx_id from the service. For private delivery, construct
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or the equivalent short-code
    path). For public delivery, omit the fragment. Browsers do not transmit URL
    fragments to servers, so K from the browser-seal path is not observed by
    qub.social or any storage gateway.

Слой метки хранилища (out-of-band). Служба загрузки qub прикрепляет намеренно небольшой набор тегов транзакций хранения вместе с выбранной нагрузкой для загрузки. Content-Type=application/octet-stream является нормативно требуемым. Справочная служба дополнительно добавляет три необязательных тега, когда создатель решает их отобразить: Intent (разрешённый в белом списке составной запрос—announcement, thesis, prediction, letter, secret, commitment, proof, либо сгенерированный системой verdict), Author (отпечаток публичного ключа создателя §9.3 в виде 64-символьного шестнадцатеричного значения в нижнем регистре), и Parent-Tx-Id (идентификатор транзакции хранения родительского qub для цепочек ответов, 43-символьный base64url).

Этот Author тег есть подписка на каждый qub: приложение для создания ссылок прикрепляет его только тогда, когда пользователь явно включает публичное указание авторства во время печати. Когда переключатель выключен — по умолчанию — ничего не Author тег написан, а qub не имеет атрибуции в цепочке: ничего в постоянном хранилище не связывает загрузку с именем создателя, электронной почтой или другими qub. Когда переключатель включен, Author отпечаток пальца соответствует выбранному создателем @handle через цепочку аттестации §9.5. Отношения в цепочке ответов и Intent не идентифицируются. Для частной доставки наружная обертка (§13) шифрует распознаваемую внутреннюю SealedQub артефакт, поэтому сбор сохранённых обёрток и получение публичных подписей drand по-прежнему недостаточны для восстановления тела без K; теги хранения остаются намеренно публичными метаданными.

Эталонный сервис намеренно НЕ прикрепляет тэги App-Name, App-Version или Type: любой такой фильтр с единственным значением возвращал бы весь корпус qub по GraphQL-запросу, что несовместимо с областью конфиденциальности обёртки, ограниченной только телом.

Соответствующий верификатор НЕ ДОЛЖЕН зависеть от какого-либо тэга хранилища для верификации сторонними лицами по §11; body hash / qub_id / подпись обязуются только на внутреннем CBOR, никогда — на наборе тэгов.


8. Протокол разблокировки

Полная последовательность разблокировки. Каждый шаг нормативен.

 1. Viewer opens delivery URL. Extract arweave_tx_id from the path and retain
    the optional URL fragment. Do not assume a missing fragment is an error:
    public/bare delivery intentionally has no K.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch the stored bytes (with multi-gateway fallback).
 3a. Resolve the delivery shape structurally:
    a. If the bytes parse as OuterWrapper, require a well-formed 32-byte K in
       the URL fragment, require wrapper version 0x01, and unwrap per §13.
       Any missing/malformed K or AEAD failure is a terminal error.
    b. Otherwise require the bytes to parse as bare SealedQubCbor; no K is
       required. If neither shape parses, report an integrity error.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01), visibility is known, and the
    delivery shape matches it (wrapped = 0x00 private; bare = 0x01 public).
    Reject any mismatch or unknown value.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check. Recompute expected_round from
    SealedQub.unlock_at per §4.3. Reject unless SealedQub.drand_round ==
    expected_round OR SealedQub.drand_round == expected_round - 1 (the
    legacy pre-release mapping—see §4.3), AND the round baked into the tlock
    ciphertext stanza (read via the age/tlock header, no signature required)
    == SealedQub.drand_round exactly. 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.
12a. Verify: QubEnvelope.outcome_at == SealedQub.outcome_at (both absent, or
    both present and equal). Fail → integrity error.
12b. Content re-derivation. Recompute qub_id per §4.1 from the decrypted
    fields — (QubEnvelope.version, content_type, created_at, unlock_at,
    outcome_at, SealedQub.drand_round, QubEnvelope.body_hash,
    title_hash(SealedQub.title)) — and verify it equals SealedQub.qub_id.
    Fail → integrity error. The pairwise checks in steps 10-12a only prove
    the two layers agree with EACH OTHER; a forger who rewrites a bound
    field consistently on both surfaces (a pre-reveal title swap, or a
    post-round body swap with a recomputed body_hash re-encrypted to the
    same round under the same qub_id) passes them all. Only re-deriving
    the identity from content closes this.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact), 0x04 (verdict).
    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 the appropriate renderer (see §10 for text and §6 for pact/verdict).
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 байт Активный
0x02 Ed25519 32 байта 64 байта Зарезервированная константа; не поддерживается в протоколе v1

Просмотрщики Protocol-v1 ДОЛЖНЫ отклонять любое значение вне {0x00, 0x01}, включая сдержанный 0x02 значение. Резервирование предотвращает случайное повторное использование; это не активация. Для её активации требуется управляемое изменение, описанное в §15.

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 предобраз
body ✓ Транзитивно, через body_hash = SHA3-256(body)
author_pubkey — (неявный) Ключ, который подтвердил подпись, по определению является автором
cosigner_pubkey / cosigner_signature — Независимо подписано тем же sig_input (см. §9.7)
drand_chain_id, tlock_ciphertext, visibility — Внешний SealedQub поля, а не внутри конверта — покрытые собственными структурными инвариантами (круговая / цепная согласованность), но не подписью автора. (drand_round теперь связан транзитивно через qub_id preimage — см. выше.)

Почему V2 — единственный принимаемый прообраз.

Реализации, отображающие 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:

Оба поля ДОЛЖНЫ присутствовать вместе или оба отсутствовать. Если присутствует ровно одно, просмотрщики ДОЛЖНЫ сообщить об ошибке целостности.

Процедура верификации:

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."

Свойства:

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 Разрешённые элементы

10.2 Запрещённые элементы

Элемент Обработка
Сырой HTML (<div>, <script> и т. д.) Полностью вырезается. Никакой HTML не пропускается.
Изображения (![alt](url)) Вырезаются. Синтаксис изображения удаляется из вывода.
Ссылки ([text](url)) URL рендерится как видимый простой текст. Не делается автоссылкой. Не кликабелен без явного действия пользователя.
Опасные схемы URL javascript:, data:, vbscript:, file: — вырезаются.
Iframes, embeds, objects Вырезаются.
HTML-сущности Декодируются в отображаемые символы только если это безопасно.

10.3 Реализация

Реализации ДОЛЖНЫ использовать строгий парсер на основе allowlist, а не blocklist. Рекомендуемый подход:

  1. Парсить Markdown с помощью pulldown-cmark (или эквивалента).
  2. Обойти AST и отбросить любой узел, не входящий в allowlist (§10.1).
  3. Для узлов ссылок: выдавать URL как видимый текст, а не как кликабельный элемент <a>.
  4. Преобразовать отфильтрованный AST в типизированное промежуточное представление (например, enum MarkdownNode только с безопасными вариантами). Сырой HTML структурно непредставим в этом IR.
  5. Рендерить из типизированного IR в целевой слой представления (например, реактивные view-компоненты, узлы DOM). Никакой конкатенации HTML-строк или innerHTML ни на каком этапе.

Подходы на основе blocklist хрупки, так как новые расширения Markdown или особенности парсера могут вводить нефильтруемые элементы. Подход с типизированным AST делает XSS структурно невозможным — нет варианта, который мог бы нести произвольный HTML.

10.4 Ограничения размера и структуры


11. Проверка третьей стороной

Любая третья сторона, держащая сохранённые байты (и K для приватного/обернутого qub), может проверить криптографический артефакт без сотрудничества с qub. Независимо с указанием времени существование требование также требует либо проверенного постоянное хранение per-qub или проверенное доказательство прозрачного журнала §16.

1. Obtain the stored bytes. For a private delivery, also obtain K from the
   delivery-link fragment.
2. Resolve the delivery shape (§8 step 3a): unwrap OuterWrapper with K, or
   accept bare SealedQubCbor. Require shape/visibility agreement.
3. Parse SealedQubCbor → SealedQub; validate protocol version, visibility,
   content type, chain identity, and structural bounds.
4. Recompute expected_round from unlock_at (§4.3); require the stored round
   (allowing the documented legacy minus-one case) and ciphertext-stanza round
   to agree exactly as §8 step 6a specifies.
5. Obtain the drand signature for SealedQub.drand_round and verify its BLS
   signature against the pinned chain public key.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify envelope/sealed equality for qub_id, unlock_at, and outcome_at.
10. Recompute qub_id from the decrypted fields, sealed drand_round, and title;
    require it to equal the carried qub_id (§4.1).
11. If sig_alg != 0x00, verify the V2 author signature (§9.4). If cosigner
    fields are present, verify their pairing, key separation, and signature
    (§9.7).
12. For an existence-time claim, independently verify either:
    a. the permanent-storage transaction's data-to-id binding, owner, block
       inclusion, and block timestamp; or
    b. a §16 inclusion proof through its pinned anchor and anchor block time.
13. Report each verified claim separately; do not collapse an absent storage
    or anchor proof into a successful timing verdict.

Что подтверждает проверка:

Доказательство ввода Что это устанавливает
Действительный пакет / запечатанный артефакт + подпись drand Обнаруженное тело совпадает body_hash; метаданные, встроенные в qub_id цел; шифротекст привязан к объявленному раунду drand; и этот раунд истек. Это делает не установить, когда был создан шифротекст.
Действительная подпись автора/созаемщика V2 Держатель(и) соответствующего(их) секретного(ых) ключа(ей) аутентифицировал(и) подписанную поверхность в §9.3.
Самостоятельно проверенная транзакция хранения за qub Точный сохранённый зашифрованный текст существовал не позднее его временной отметки блока.
Действительное доказательство прозрачности с привязкой к журналу Утверждение, специфичное для типа листа, в §16.11, включая время обязательства с верхней границей от опорного блока.

Что проверка НЕ доказывает:

Неподтвержденный Почему
Авторство Этот sender_label является декоративным. Без sig_alg ≥ 0x01, любой мог бы запечатать этот контент.
Намерение Артефакт доказывает байты и криптографические связи, а не то, что создатель субъективно имел в виду.
Предшествующее обязательство от .qub один Создатель может собрать действительный пакет после того, как истек привязанный раунд. Встроенная подпись drand доказывает, что раунд истек, а не то, что шифротекст существовал до него.
Точное время кнопки-печати Метка времени блока хранения или анкера является независимо проверяемой верхней границей и может отставать от локального действия пользователя. sealed_at / received_at утверждения не имеют доказательной основы.

Реализованный журнал прозрачности (§16) расширяет проверку на кюбах с с индикатором вскрытия заказ и недоверчивый время верхней границы обязательства (the время якорного блока), ограниченное видом листа (§16.11). Оно не добавляет авторство или намерение; для пути загрузки по умолчанию, не различающего байты, оно само по себе не доказывает body_hash или drand_round, которые продолжают поступать из проверок артефактов.


12. Управление версиями и выпуском

Выпуски документов, внутренний протокол проводки и внешний обёртка являются отдельными пространства версий. Следовательно, уточнение только документа не происходит тихо изменить байты, и будущая миграция по проводам не может маскироваться под редакционную пересмотр.

12.1 Версия выпуска документа

Эта спецификация использует семантические релизы документов (MAJOR.MINOR.PATCH) и неизменяемый тег Git с названием protocol-v<release>.

Статус выпуска является одним из Черновик (пока не нормативный), Текущий (единственный рекомендуемая целевая платформа реализации), или Заменённый (сохранено для исторических проверка). Неверсионированный /protocol маршрут отображает текущий выпуск; тег выпуска сохраняет свой точный источник и все локали, опубликованные с ним. Изменение статуса или номера выпуска требует обновления этой таблицы и выпуска история в том же рассмотренном изменении.

Выпуск документа Дата вступления в силу Статус Протокол передачи данных Обертка Источник
1.0.0 23.09.2026 Текущий 0x01 0x01 protocol-v1.0.0

12.2 Версия протокола

Этот version поле (u8) в обоих SealedQub и QubEnvelope определяет основную версию протокола.

12.3 История версий протокола

Версия Значение Описание
v1 0x01 Приватная/упакованная и публичная/открытая доставка; текст (0x01), пакт (0x03), и вердикт (0x04) тела; автор/соавтор ML-DSA-65 V2 подпись; drand quicknet tlock; SHA3-256.

12.4 Прямая совместимость с будущими версиями

Просмотрщик 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 не содержит других допустимых алгоритмов — строгий отказ и мягкий сбой наблюдательно идентичны до тех пор, пока не будет зарегистрирован третий алгоритм. Поведение мягкого сбоя выше становится значимым, как только §9.2 допускает новую запись, и эталонный просмотрщик будет обновлён до мягкого сбоя в этот момент.

12.5 Версия внешней оболочки

Внешняя оболочка, описанная в §13, имеет собственную version байт, независимый из SealedQub.version и QubEnvelope.versionДва пространства версий развиваются отдельно: будущая пост-квантовая безопасная симметричная замена увеличивает байт обертки, не затрагивая версию внутреннего протокола, а будущее добавление на уровне протокола (например, новое поле конверта) увеличивает внутреннюю версию, не затрагивая байт обертки.

OUTER_WRAPPER_VERSION_* Значение Алгоритм Статус
OUTER_WRAPPER_VERSION_1 0x01 AES-256-GCM с 12-байтовым nonce, 16-байтовым тегом аутентификации, AAD привязан к qub_id Активно для частной доставки
— 0x02–0xFF Зарезервировано Будущее

Зрители ДОЛЖНЫ отклонять неизвестные версии обёртки с явной ошибкой. Протокол намеренно сохраняет пространство версий обёртки узким до появления конкретного драйвера миграции (например, рекомендации NIST, отдающие предпочтение другому AEAD); a 0x02 Слот будет выделен в той же редакции, которая вводит алгоритм.


13. Внешняя обёртка шифрования

13.1 Обоснование

Слои протокола (QubEnvelope → tlock → SealedQub) делают запечатанный qub запечатанным по времени: тело нечитаемо до unlock_at и до публикации подписи раунда drand. Однако после разблокировки подпись раунда становится публичной, а каноническая CBOR-форма SealedQub распознаваема, поэтому сборщик, индексировавший транзакции постоянного хранилища, мог бы массово расшифровать весь корпус qub.

Для частной доставки внешний слой шифрования закрывает этот канал, вставляя дополнительный симметричный слой AEAD между каноническими SealedQubCbor и сохранённые байты. В пути browser-seal 256-битный ключ K жизни только в фрагменте URL адреса доставки и на устройствах пользователей; браузеры не передают фрагменты URL на серверы, поэтому qub.social, каждый шлюз хранения и каждый CDN перед любым из них наблюдательно слепы к K. Хранимая репрезентация приватного qub поэтому является непрозрачным шифротекстом, чей открытый текст невозможно восстановить без URL, который создатель решил предоставить. Публичная доставка намеренно опускает этот слой (§13.8).

Чистый эффект:

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
  ├─ public (visibility=0x01) ───────────────▶ stored directly (§13.8)
  └─ private (visibility=0x00)
       ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12, this section)
     OuterWrapper CBOR bytes         ← stored private payload

Запечатывание и разблокировка на уровне протокола (§7, §8) остаются неизменными ниже границы обёртки; обёртка прикрепляется в точке вызова seal() и снимается в точке вызова unlock().

13.3 Структура данных OuterWrapper

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.5
    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
}

Инварианты полей.

Кодирование 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
    I := canonical_cbor_decode(S) as SealedQub
    require I.qub_id == Q               // reject mismatched caller AAD
    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.5
    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
    S := canonical_cbor_decode(P) as SealedQub
    require S.qub_id == O.qub_id       // explicit inner/outer cross-check
    return P                            // P is the validated SealedQubCbor

Схлопывание режимов сбоя. Неверный K, неверный nonce, несоответствие AAD и подделанный шифротекст — все производят одну и ту же ошибку DECRYPT_FAILED. Это намеренное свойство AEAD: различение режима сбоя создало бы боковой канал, который удалённый атакующий мог бы прозондировать, отправляя сформированные неправильно обёртки и измеряя время ответа. Эталонные реализации ДОЛЖНЫ схлопывать все сбои AEAD в единую форму ошибки.

13.6 Ключевой материал и распространение

Ключ обёртки K — это 256-битное равномерное случайное значение, генерируемое для каждого qub с помощью CSPRNG. Эталонные реализации получают его из:

Распространение: K ДОЛЖЕН быть закодирован как URL-безопасный base64 (RFC 4648 §5, без паддинга) и добавлен к ссылке доставки как компонент фрагмента:

delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>

Фрагмент никогда не передаётся ни на какой сервер соответствующим браузером. Каналы восстановления (серверный индекс истории, опт-ин email auto-send), которые персистируют полную ссылку доставки — включая фрагмент — за пределы устройства пользователя, являются явным компромиссом против стандартной криптошреддинговой позы и ДОЛЖНЫ быть привязаны к явному согласию пользователя.

Потеря фрагмента. Если пользователь теряет фрагмент URL и не имеет канала восстановления, qub нечитаем. Это несущий компромисс дизайна и ДОЛЖЕН раскрываться пользователю во время запечатывания. MVP усиливает раскрытие во время запечатывания явным текстом «сохраните этот URL» и каналом восстановления через верифицированную почту для пользователей, которые соглашаются.

13.7 Вне области применения этого раздела

13.8 Публичные qub (опускание обёртки)

Внешняя оболочка необязательно на уровне доставки. Создатель может запечатать qub, как публичный, в этом случае канонический SealedQubCbor входит в конвейер хранения непосредственно, без OuterWrapper слой и нет ключа K:

SealedQubCbor bytes  ──(public)──▶  stored as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ stored

Публичный паб — заблокировано по времени, но не защищено ссылкой: оно остается нечитаемым, пока не будет опубликован его рандомный раунд (слой блокировки не меняется), но после разблокировки любой, у кого есть идентификатор транзакции хранения, может его расшифровать — фрагмент URL не требуется, потому что его нет K. Это преднамеренная торговля за поверхности, которые сервер должен обслуживать: электронные письма с уведомлениями о раскрытии, ссылки oEmbed/авто-встраивания без фрагментов и более богатый SEO после раскрытия — все это требует ссылку, которая работает без секрета, который сервер никогда не хранит (§13.6). Приватный qub все еще может использовать явный <qub-embed src="full_delivery_url"> форма, когда издатель предоставляет свою полную способность с фрагментами.

Последствия, которые производитель ДОЛЖЕН учитывать:

Приватный (обёрнутый) остаётся по умолчанию; публичный — это явный выбор создателя для каждого 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  = 4695446  (= floor((1736294400 - 1595431050) / 30) + 1, §4.3 mapping, 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—current protocol v1):
  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)
  0x000000000047A596  ||  // drand_round as u64 big-endian (4695446)
  body_hash           ||  // 32 bytes
  title_hash              // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 4a84e3dfaec32954949c30073f8e6506
           fd3204c1bb97f9162b81c7587afe412e

Реализации ДОЛЖНЫ давать идентичные результаты body_hash и qub_id значения для этого ввода. Этот тестовый вектор ДОЛЖЕН быть первым написанным модульным тестом. Канонические значения выше были вычислены эталонной реализацией и ДОЛЖНЫ совпадать бит в бит. Исторические предварительные макеты до запуска (никакие живые qub не зависели от первых двух) использовали 92 байта до outcome_at (3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0) и 100 байт после добавления outcome_at_or_zero (b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed). Текущая 108-байтная компоновка затем добавлена drand_round и QUB_ID_V2 разделитель домена. Ранний 108-байтовый вектор использовал устаревший ceil круглая карта (drand_round = 4695445) и произведенный 3a9fcb31b750d985c262fada6d4f777fd6a28be831d941d85c131f5a4bbaf8a4—по-прежнему действителен qub_id для этого входа раунда, в то время как приведённый выше пример следует текущему отображению раунда §4.3.

14.2 Разблокировка-сопоставление раундов

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  floor(4675285.0) + 1 = 4675286

drand_round = 4675286

Раунд 4675286 публикуется в 1595431050 + (4675286 - 1) * 30 = 1735689600—ровно в unlock_at, никогда раньше. (Предварительный выпуск наследия ceil сопоставление дало 4675285, опубликовано в 1735689570—На 30 секунд раньше; проверяющие принимают этот устаревший раунд в соответствии с §4.3.)

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-файл:

На данный момент приспособление закрепляет три низкоуровневых случая оберток. Они тестируют детерминированное поведение OuterWrapper кодирование и совместимость AEAD независимо от инварианта формы доставки §13.8; в частности, историческое название basic-text-public и его внутренний visibility = 0x01 делать не сделать полученные обернутые байты соответствующей публичной доставкой. Производитель ДОЛЖЕН по-прежнему хранить публичные внутренние байты в открытом виде и оборачивать только приватные (0x00) внутренние байты.

Дело Покрытие
basic-text-public Историческое название элемента низкого уровня. Наименьшее реалистичное SealedQub форма, без дополнительных полей; проверяет только оберточные байты и не является соответствующей доставкой §13.8.
with-recipient-pubkey SealedQub с recipient_pubkey установить (зарезервированный будущий путь). Использует другой внутренний набор ключей CBOR; его отдельное содержимое фикстуры независимо приводит к другому qub_id (recipient_pubkey само по себе не находится в образе предварительного отображения §4.1).
longer-body ~4 КБ тела — упражняет много байтовые префиксы длины CBOR как внутри внутреннего конверта, так и во внешнем шифротексте.

Реализации ДОЛЖНЫ выдавать побайтово идентичный expected_wrapper_hex для записанных входов. Регенерация фикстуры требует QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors и зарезервирована для намеренных изменений формата.


15. Управление криптографическим профилем (будущее)

Этот раздел является информативным для v1 и становится нормативным в тот момент, когда второй алгоритм впервые войдёт в любую из криптографических примитивов qub.

15.1 Текущая позиция

Протокол v1 привязывает ровно один алгоритм на примитив:

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

15.2 Предполагаемая форма

Когда второй алгоритм войдёт в протокол, верификатор будет настроен на именованный CryptoProfile (например, ExqubV1), перечисляющий точный набор разрешённых значений на примитив — sig_algs, chains drand, версии обёртки, типы контента. Профиль фиксируется во время верификации, никогда не согласуется по основному каналу. Любое значение вне активного профиля отклоняется.

Это гарантирует, что добавление ML-DSA-87 или активация Ed25519 не может задним числом ослабить существующие конфигурации верификаторов: верификатор v1 остаётся верификатором v1 даже после публикации профиля v2.

15.3 Условия активации

Повышайте §15 до нормативного статуса, когда предлагается любое из следующего:

До тех пор §15 является заглушкой, которая фиксирует форму миграции, чтобы будущие PR приземлялись против известной цели, а не пере-обсуждали поверхность согласования с нуля.


16. Журнал прозрачности и уровни долговечности (дизайн — обзор завершён)

Статус. Этот раздел является реализованный (W5/UP-B1, Этапы 1–8), с указанным здесь производителем и областью доверенного корня. Форматы проводов, хеширование и пути проверяющего активны: основные типы Merkle + canonical-CBOR (qub-core), зеркало TypeScript + сборщик ANS-104 (workers/api/src/crypto/), один автор LogDO + координатно-ключевое хранилище узлов R2, это /upload попытка добавления в журнал, ежедневные задачи anchor + bundler-drain, GET /api/v1/qub/:tx_id/proof (включение) и GET /api/v1/log/consistency (RFC 9162) конечные точки доказательства, типизированное доказательство включения, содержащееся в .qub пакет (§17.5), встроенный проверяющий анкеров ANS-104 (tools/qub-verify), и двойной крючок для самостоятельной публикации (§16.6). Успешный /upload всегда R2-прочный, но покрыт корой только когда LOG_DO настроен и встроенное добавление выполняется успешно; только тогда его ответ имеет значение log_seq, receipt, и anchor_status. Если RECEIPT_SK отсутствует или недействителен, этот квитанция sig_b64url пуст и не обеспечивает непризнание факта отправки. Текущий /seal и договор о публикации путей планирует отдельные транзакции Arweave, но не добавляет лист журнала. В настоящее время никакой код этого не выполняет /upload Предложенное комментарием позднее примирение после сбоя добавления. Внешний обзор W5 завершен: §16.15 фиксирует проектные решения и ограничения запуска, но эти ограничения не расширяют только что указанное покрытие производителя. Остаются три элемента доверия/развертывания, доступ к которым ограничен: (a) выделенный кошелек якоря (ANCHOR_JWK; LogProfile.anchor_owner по-прежнему является [0xAB; 32] заполнитель); (b) ключ подписи квитанции и соответствующий PIN-код открытого ключа (RECEIPT_SK является необязательным и LogProfile.receipt_pubkey в настоящее время пусто); и (c) репозиторий GitHub self-published-heads + токен (§16.6). До тех пор, пока анкеры/профильные пины не будут подготовлены, автономный проверяющий сообщает о статусе доказательства честно, а не утверждает о полностью закрепленной, зафиксированной проверке. Дизайн строго аддитивный и существует нет изменений в SealedQub / QubEnvelope формат передачи по проводу.

16.1 Обоснование и уровни долговечности

Текущие пути публикации разъединяют подтверждение от Arweave: они выводят и подписывают отдельную транзакцию, сохраняют артефакт и точное состояние отправки в R2, затем публикуют асинхронно. Журнал прозрачности добавляет независимо закреплённый слой упорядочивания для подмножества общего. /upload запросы, чьи LogDO добавление успешно:

уровень Имя Гарантия Когда
T1 R2-первый синхронный подтверждение Уровень надежности — зафиксированные байты и точное состояние публикации записываются в постоянное хранилище перед возвратом успешного результата. Реализовано на текущих путях публикации.
T2 Пакетное включение журнала прозрачности Только для добавления, защищенное от подделки обязательство + полная упорядоченность после включения и закрепления. Текущий производитель: успешный LogDO добавляет из /upload; ответ содержит кортеж квитанции. Не универсально.
T3 Постоянство Per-qub Arweave Одна транзакция Arweave для qub. В настоящее время подготовлено для каждой принятой публикации и размещается асинхронно; точная подписанная транзакция остаётся в выводимом исходящем ящике до момента доставки.

Уровни описывают различные свойства достоверности и долговечности, а не текущий коммерческий план. Текущий код по-прежнему планирует отдельную транзакцию Arweave для каждой принятой публикации; он не делает T3 доступным только в виде платного улучшения. Ограничения по квотам API-ключа/учетной записи остаются отдельными средствами контроля приложения.

Прочность честность. Запись T1 является синхронной, поэтому успешный ответ обеспечивает долговечность на уровне приложения без ожидания шлюза Arweave. Сама по себе она не устанавливает независимую временную метку. Подтвержденная отдельная транзакция предоставляет верхнюю границу времени блока. Для ответа, содержащего полный кортеж T2, следующий подтвержденный якорь может предоставить доказательство журнала, описанное ниже. Если кортеж отсутствует, ничто не может подразумевать, что этот qub уже находится в журнале прозрачности. Задержка якоря и публикации не имеет числовых 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 байт, потому что общий путь загрузки не зависит от байтов: POST /api/v1/upload намеренно рассматривает обе принятые формы полезной нагрузки как непрозрачные и получает qub_id и unlock_at только как неподтверждённые утверждения клиента. На стандартном приватном пути, body_hash, drand_round, created_at, и drand_chain_version дополнительно скрыты внутри внешней оболочки §13, ключ которой Рабочий никогда не держит. Система типов также определяет подтверждённую форму для производителя, который производит body_hash / drand_round само. Текущий /seal маршрут имеет эти значения, но не вызывает LogDO, так что производство в настоящее время выделяет только утвержденное (0x02) оставляет от успешных общих загрузок добавления. Разделение сохраняет честность каждого зафиксированного значения, не притворяясь, что подтвержденный производитель подключен:

Ключ Длина окна Тип Присутствие Значение
seq четыре u64 требуется Глобальный индекс листа с нуля; позиция, на которую ссылается доказательство включения.
kind пять u8 требуется 0x01 заведомо способный (определено, в настоящее время не испускается) или 0x02 заявлено (клиентская печать / загрузка с побайтной слепотой).
ref четыре bstr[32] требуется Идентификатор ссылки на лист. Подтверждено → необработано qub_id. Утверждено → the ослепленный идентификатор SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash шесть bstr[32] требуется Адрес контента SHA3-256(stored_bytes) — единую связующую величину содержания, которую Работник всегда может честно вычислить на обоих путях.
unlock_at десять i64 требуется Скопировано (засвидетельствовано) или утверждено (утверждено); проверено > 0 прежде чем оно попадет в лист.
received_at двенадцать i64 требуется Стенной рабочий часы на R2-ack. Не доказательный (заявлено оператором; §16.6). Используется для самодескрипции, никогда не является доказательством. Подтверждено > 0.
body_hash десять bstr[32] kind=0x01 только Пропущено в 0x02 — У Работника этого нет в соответствии с §13.
drand_round двенадцать 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.

Этот LogDO Прочный объект является один писатель (blockConcurrencyWhile, зеркалирование QuotaDO / EntitlementDO) — добавление в общий журнал является операцией чтение-изменение-запись на общем состоянии и поэтому ДОЛЖНО выполняться через DO, никогда через KV. Оно кэширует правую границу дерева (O(log n) хеши), так что закрытие пакета является O(batch). A партия это набор закрепленных вместе листьев; его реализованные триггеры являются tree_size продвижение как минимум LOG_BATCH_MAX_LEAVES (по умолчанию 4096), возраст достигает якорной каденции или явное административное/cron-принудительное закрытие. root_i это кумулятивный хеш дерева Меркле по листьям 0 .. tree_size_i.

16.6 Подписанная вершина дерева через привязку к Arweave

Транзакция привязки Arweave является подписанной вершиной дерева (Signed Tree Head) и заменяет подпись оператора для самой вершины дерева: суточная привязка не нуждается в qub-ключе, потому что owner транзакции Arweave и есть подпись. Тезис рва сохраняется — несущим для привязанного корня является неизменный субстрат, а не секрет, хранимый qub.

Дизайн журнала предусматривает один успешный тёплый append ключ квитанции (§16.10), прикреплено в LogProfile и перекрестно подписан anchor_owner. Текущая реализация не завершила предоставление этого корневого доверия: RECEIPT_SK необязательно, отсутствие/недействительный ключ дает sig_b64url: "", и скомпилированный LogProfile.receipt_pubkey пусто. Такая квитанция может описывать приложенный лист, но является не неотказуемая подписанная квитанция. Более сильное утверждение о конструкции применяется только после того, как проверяющий выпуск закрепит совпадающий открытый ключ и владелец якоря сделает его повторную подпись. Ответ на публикацию без полного кортежа квитанции не делает утверждения о принятии в журнал; ответ с пустой подписью делает утверждение о позиции добавления, но не делает утверждения о проверке подписи.

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 → anchor confirmation (каденс + окончательность Arweave, без гарантии задержки протокола). До предоставления корня доверия текущая реализация обеспечивает эксплуатационную целостность qub плюс любые присутствующие неподписанные метаданные добавления; она не обеспечивает планируемую гарантию неотказуемости. Три артефакта подотчетности определяют завершённый дизайн (модель свидетеля — это разрешение §16.15 Q2):

  1. Печать квитанции (зависит от снабжения) — аналог SCT возвращается, когда добавление в журнал загрузки успешно (§16.10). Он становится неоспоримым только когда sig_b64url не пустой и Соответствующая пара открытого ключа/владельца якоря зафиксирована у проверяющего. В настоящее время пустой пин производственного профиля не может поддерживать этот вердикт. Этот контроль не применяется к опущенной кортежу квитанции или неподписанной квитанции.
  2. Опубликованная методология мониторинга + предыдущая цепочка прохода — якорь prev цепь проходит голову→генезис; вилка (два якоря в одном) size с разным root, или сломанный prev) является публикуемым доказательством неправильного поведения. Обнаружение двусмысленности является заявленным операционным обязательством, а не скрытым предположением.
  3. Двойные самостоятельно изданные головы — каждая новая глава {sth_hash, tree_size} размещается на специально принадлежащем QUB публичный репозиторий GitHub только для добавления (несущий твираж, защищённый от подделки элемент самопубликации), с социальной публикацией только как подтверждением наилучших усилий. Неудачная публикация ДОЛЖНА вызвать страницу (не молчать об ошибке). Реализовано (Этап 8) как publishHead зацепиться за якорный крон (workers/api/src/utils/heads-publish.ts): а PUT к API содержимого без sha только для добавления (a 422 означает, что голова уже опубликована, никогда не перезаписывается); участие по выбору / с развертыванием с ограничением PUBLISH_HEAD_GITHUB_{TOKEN,OWNER,REPO} и инертно до тех пор, пока хранилище не будет подготовлено. Жесткая ошибка GitHub страницы через health_alert канал и вызывает срабатывание метрики прочного отказа (m:tlog_publish_head_fail); сам якорь Arweave никогда не откатывается при сбое публикации. «Не молчать о сбое» гарантируется этой долговечной метрикой — на которую операции ОБЯЗАНЫ настроить дашборд-оповещение — даже если страница с уведомлением по электронной почте не может быть доставлена. Две честные ограничения следуют из «якорь-при-движении вперёд» (cron публикует только тогда, когда размер увеличивается): временный сбой GitHub оставляет разрыв в последовательности опубликованных заголовков для этого размера — ограниченной, а не молчаливой (она осуществляет постраничный вывод), и поскольку каждый заголовок фиксирует дерево-супермножество, доказательство согласованности по §16.9 преодолевает разрыв; что важно, это доказательство согласованности вычисляется из авторитетное дерево с привязкой к Arweave, а не с поверхности GitHub, так что разрыв на GitHub никогда не ослабляет проверяемость. Донаполнение, которое заполняет разрывы опубликованных версий, является отложенным улучшением.

Честность как обязательство (обязующее ограничение). Потому что 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 — единственная авторитетная инстанция.

Каденция: ежедневно по умолчанию, пересмотрено с учётом объёма (триггер размера автоматически сокращает эффективный темп под нагрузкой). Текущий производитель не реализует механизм принудительной привязки оплачиваемой печати. кошелек Anchor посвящённый и низкоскоростной, отдельно от кошелька для загрузки — он ДОЛЖЕН быть своим собственный JWK (отдельный ключ, а не логическая роль на кошельке для загрузки), чтобы компрометация кошелька для загрузки не могла подделывать якоря — с жёстким дневным лимитом транзакций для якорей. Позиция по хранению изложена прямо: a горячая клавиша узкого диапазона с жестким автоматическим выключателем и низким балансом, а не «холодный» — кошелек, который автоматически подписывает транзакции ежедневно, не может быть холодным, и спецификация не утверждает обратное.

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 повсюду).

Путь кода ANS-104 обслуживает отложенный механизм резервного варианта/слива и записывает AnchorBundle Элементы данных. Обычный путь публикации сначала создаёт точную подписанную транзакцию Arweave и сохраняет её JSON в надёжном исходящем хранилище; прямое размещение является оптимизацией задержки, а путь отвода повторно пытается выполнить ту же транзакцию перед применением резервного варианта бандлера. Схема подписи (решено — §16.15 Q8): v1 подписывает с помощью RSA-PSS (тип подписи 1) повторное использование существующего механизма JWK кошелька Arweave (ноль новых долгоживущих ключей для хранения, поддержка тезиса «на один секрет меньше»); Ed25519 отложен до пути PQ-миграции §15.

Рукописный deep hash — это код наивысшего риска и наименьшего естественного покрытия в W5, поэтому его контроль не подлежит обсуждению (§16.15 Q8):

  1. Кросс-языковая фикстура tlog_v1.json (Rust + TS, паттерн wrapper_v1.json §14.5) покрывает deep-hash, байты DataItem + id, хеши листьев, корень из 5 листьев + аудиторский путь, хеш STH, доказательство включения и доказательство согласованности — в обоих направлениях, подписи и верификации (направление верификации важно, потому что локальная проверка tx → tx_id §16.6 втягивает deep hash в каждый самостоятельный верификатор, а не только в писателя).
  2. Однократный interop round-trip через эталонный бандлер ANS-104, потребляемый как только статические тестовые данные — никогда npm-зависимость времени выполнения (поза только-Web-Crypto / без install-скриптов сохраняется).
  3. Путь deep-hash + RSA-PSS должен round-trip через те же примитивы crypto.subtle, что использует продакшен, чтобы собственный кодировщик был байт-совместим.
  4. Постоянный пост-бандл монитор приёмки подтверждает, что каждый 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 последовательность такая:

  1. Ворота передней части (аутентификация, проверка, ключ шардирования идемпотентности) — без изменений.
  2. Создайте, отметьте и подпишите точную индивидуальную транзакцию Arweave. Это вытекает tx_id локально, хотя создание транзакции может получать метаданные вознаграждения/якоря с шлюза. Сбой подготовки все равно приводит к неудаче запроса до подтверждения.
  3. Синхронно записать выбранный артефакт в qub-cache/<tx_id> и сохранять устойчивые записи создания-операции/почтового ящика. Это гарантии надежности и минимальные попытки повторов; сбои до завершения возвращают 503.
  4. Когда LOG_DO настроено, синхронно пытаться LogDO.append(leaf). Один писатель назначает seq, расширяет цепочку записей и обновляет фронтир. RPC добавления делает только это; пакетное закрытие выполняется вне пути при срабатывании сигнала. В настоящее время сбой транспорта/приложения при добавлении непрерывно работающий при сбое: ответ всё ещё может быть успешным без log_seq, receipt, или anchor_status. Несмотря на комментарий об реализации, сегодня никакая автоматическая последующая сверка журналов не настроена.
  5. Верните подтверждение. Включите { log_seq, anchor_status: "pending", receipt } только когда append вернул полный успешный кортеж. receipt.sig_b64url пусто, когда подписант квитанции недоступен; клиенты НЕ ДОЛЖНЫ считать это значение подписанным или неоспоримым. Отсутствие кортежа означает только долговечную публикацию, а не прием через журнал прозрачности.
  6. Используйте одну отложенную задачу для отправки точно подписанной транзакции. Успех удаляет исходящие сообщения; неудача оставляет её для ограниченного cron-дренажа и не должна изменять уже подтверждённое tx_id. Промежуточные метаданные и другие вспомогательные элементы, выполненные по принципу «лучших усилий», также откладываются.

Пограничная задержка. Путь запроса включает работу с авторитетом/квотой передней части, подготовку/подписание транзакции, долговременные записи R2 и (при настройке) LogDO попытка. < 300 ms появляется на обзорe проекта как операционная цель, а не как гарантия протокола; текущий этап подготовки транзакции может выполнять запрос метаданных шлюза. Сигналы задержки и пусковые ворота являются операционными средствами контроля, а не доказательствами, доступными проверяющему.

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) лист, вышеуказанное ограниченное утверждение является потолок на чем может утверждать любой продукт, маркетинг, условия или поверхность для рендеринга доказательств. Ни одна поверхность не может заявлять или подразумевать, что журнал доказывает содержание или раунд разблокировки байт-слепой загрузки — журнал доказывает порядок + доверенную верхнюю границу времени обязательства непрозрачного шифротекста. Содержание и доказательство раунда приходят исключительно из существующего §11 .qub-проверка пакета, которая не зависит от журнала. Публикация, к которой не было успешного добавления/получения, вообще не имеет требований к журналу.

16.12 Версионирование и координация с W3

Есть нет SealedQub выбоина провода и поэтому нет обновления версии протокола (§12.2): журнал является сайдкаром, который фиксирует существующие поля и байты, поэтому он не входит в историю версий протокола §12.3. Опционально W3 drand_chain_version остается нетронутым и остается единственным необязательным SealedQub область. Вместо этого журнал вводит свои собственные независимые пространства версий — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — отражая независимость версии оболочки §12.5 (оболочка содержит байт версии, независимый от версии протокола, и версии журналов следуют той же разграниченности).

Доставка доказательства по умолчанию запрашивается, с необязательным попутчиком. Доказательство не может существовать во время запечатывания (привязка ещё не записана), поэтому .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 выше; привязка ограничений запуска повторяются в конце. Реализация может происходить в соответствии с ними.

  1. Путь по умолчанию (kind=0x02) честность листа — РЕШЕНО. Отправьте разделенный на два листа вид, как указано: kind=0x02 не совершает ни того ни другого body_hash ни drand_round. Нет *_body_hash поле на байтослепом пути (это был бы наиболее читаемый ложный «проверенный» сигнал для интеграторов и является удобством, которое §11 уже предоставляет из пакета). Делать не требовать серверное заключение для журналируемых qubs (это заставит открытый текст проходить через Worker и уничтожит крипто-измельчающий ров). Любой самоописывающийся короткий замыкатель должен находиться в .qub пакет / конверт с доказательством как поле, пересчитанное проверяющим, никогда не как конечное поле. Подтвержденный владельцем предел претензии: §16.11.
  2. Ответственность за уклонение / сокрытие — ДИЗАЙН УСТАНОВЛЕН, ОБЕСПЕЧЕНИЕ НЕ ЗАВЕРШЕНО. Конструкция требует, чтобы ключ для пломбирования был закреплён LogProfile и перекрестно подписан anchor_owner, плюс методология мониторинга, обход предыдущей цепочки и два самостоятельно опубликованных узла. Скомпилированный профиль и хуки развертывания все еще являются заполнителями/опциональными, как подробно описано в §16.6, поэтому более сильное отслеживаемое + подтвержденное утверждение не актуально, пока эти этапы не завершены. Оно никогда не должно рекламироваться как независимо засвидетельствованное. Настоящий независимый свидетель будет отложен до повышения управления по §15.
  3. Закреплённый корневой сертификат владельца якоря + ротация — РЕШЕНО. Примите LogProfile шпилька (§16.6); проверяющий проверяет anchor_tx.owner == anchor_owner и проверяет привязку данных транзакции → tx_id локально. Управление ротацией является §15 расширение для сборки (§15.3 триггер добавлен), не повторное использование; запланированные ротации перекрестной подписи, ротации, основанные на компромиссе, возвращаются к §15 с проверкой форка, ограничивающей ущерб.
  4. Приватное ослепление листьями — РЕШЕНО. Продолжайте ослеплять для частных qub (ref = SHA3-256(qub_id ‖ log_blind_secret)), сырой qub_id для публичных qub (уже §16.2.1), chash как самостоятельный галстук. log_blind_secret является корреляционной/секретом уровня Sybil, только вращение вперёд (§16.2.1).
  5. received_at — РЕШЕНО. Держите это в листе, приверженно, но явно не как доказательство; никогда не использовать в качестве подтверждения или спорного доказательства на какой-либо поверхности. Любая проверка мониторинга сравнивается с временем блока Arweave. T, а не управляемый оператором anchored_at (§16.6).
  6. Многоуровневое доказуемое время — РАЗРЕШЕНИЕ ПРОЕКТИРОВАНИЯ, А НЕ ТЕКУЩАЯ МАРШРУТИЗАЦИЯ. Рассмотренный дизайн назначает время якорного блока пакетному уровню и точный час доказательства для платного T3, при этом для первого числовой SLA нет. Текущие маршруты не реализуют это коммерческое различие: они планируют отдельную транзакцию для каждой принятой публикации, а логирование покрытия остается условным, как указано в §16.1/§16.10. Копия продукта должна описывать реализацию, а не этот будущий раздел уровней.
  7. Суммарное дерево по Работникам — РЕШЕНО. Единое кумулятивное дерево RFC 9162 + кэширующий на фронтире однописательный LogDO (комфортный запас по сравнению с потолком DO примерно в ~1k записей/с; откладывать шардинг Merkle-of-shard-roots до приближения к нему). Координатно-ключевое (level, index) Хранение узла R2 + тестовый вектор wiped-DO cold-leaf реализованы (§16.9). < 300 ms остается целью проектирования/эксплуатации, а не обещанием протокола (§16.10).
  8. Схема подписи ANS-104 + глубокое хеширование — РЕШЕНО. RSA-PSS (тип подписи 1, повторное использование выделенного JWK якорного кошелька); Ed25519 отложен до пути PQ §15. Ручная глубокая хеш-функция SHA-384 привязана к тестовой системе кросс-реализационной проверки в обоих направлениях, статической проверке совместимости через сборщик ссылок, общей-crypto.subtle круговой рейс и монитор принятия Arweave после пакета (§16.8).

Привязка ограничений запуска (воплотить в жизнь + проверка продукта/юридическая проверка):


17. Портативный пакет проверки (.qub)

Статус. Этот раздел является реализовано (W7 / UP-C2): qub_core::export создает и разбирает пакет, и tools/qub-verify является общедоступным, автономным CLI, который проверяет один оффлайн. §11 и §16.9 уже ссылаются на «the» .qub «пакет» как единица, которую использует самостоятельный проверяющий; этот раздел указывает его байты и пошаговую проверку. Это строго добавочно — пакет объединяет существующие входные данные §11 и не изменяет формат передачи данных в цепочке.

17.1 Цель

§11 устанавливает, что любая третья сторона может проверить криптографический артефакт qub без сотрудничества qub. The .qub пакет выполняет эту проверку портативный и офлайн: он упаковывает запечатанный CBOR и подпись раунда drand, которая его разблокирует, в единый автономный объект, чтобы получатель мог проверить целостность содержимого, привязку к раунду и любые подписи авторства с совсем нет сетевого вызова (никакого извлечения из хранилища, никаких запросов к live drand, никакого API qub). Один только пакет не доказывает, когда был создан его шифротекст; независимо проверенная транзакция хранения или закреплённое доказательство журнала предоставляет это отдельное утверждение о времени существования (§11, §17.5).

17.2 Формат пакета

А QubBundle является ручным каноническим CBOR по профилю §3.1 (определённой длины, без тегов, без чисел с плавающей запятой, целые числа в короткой форме, текст в NFC, необязательные поля опускаются при отсутствии, ключи упорядочены по возрастанию длины закодированных байт, затем посимвольно). Три ключа длиной 15 символов упорядочены d < i < s. Сырой .qub файл состоит ровно из этих байтов; для передачи через URL или через копирование и вставку те же байты кодируются в base64url (без заполнителя).

Ключ Длина окна Тип Присутствие Значение
version восемь u8 требуется Версия формата пакета (0x01).
sealed_at десять i64 необязательно Время запроса печати создателем (секунды Unix); самодокументируемое, не имеющее доказательной силы.
drand_round двенадцать u64 требуется Круг, к которому прикреплён qub. Выступ встроенного запечатанного qub.
arweave_tx_id четырнадцать tstr требуется Идентификатор транзакции, под которым были сохранены запечатанные байты (указатель происхождения).
drand_chain_id пятнадцать tstr требуется Цепочка drand (шестнадцатеричная). Проекция встроенного запечатанного qub.
drand_signature шестнадцать bstr требуется Сигнатура маяка drand для drand_round — значение, которое расшифровывает зашифрованный текст.
inclusion_proof шестнадцать bstr необязательно Доказательство включения Merkle журнала прозрачности §16, после того как доступно закреплённое доказательство (§17.5).
sealed_qub_cbor шестнадцать bstr требуется Внутренний SealedQubCbor байты (после §13-распаковки), т.е. вход для проверки по §11.

drand_round и drand_chain_id являются удобными проекциями sealed_qub_cbor, сделано так, чтобы инструменты могли их читать без разбора внутреннего CBOR. Они создаются при конструировании и повторно проверено на декодирование против разобранного запечатанногоqub; пакет, верхнее поле которого не соответствует его полезной нагрузке, отклоняется. Дисциплина кодировщика соответствует остальному формату передачи: отклонять пустой drand_signature или arweave_tx_id, и связал каждое поле переменной длины.

17.3 Что доказывает встроенная подпись drand

Пачка содержит подпись drand, вместо того чтобы требовать от проверяющего её получения. Расшифровка с тайм-локом (tlock над цепочкой drand, §8) может быть успешной только при подлинный подпись маяка для заблокированного раунда — значение, которое цепочка публикует только один раз после завершения раунда и которое является действительной подписью BLS под публичным ключом цепочки. Поддельная или неправильная подпись не проходит проверку BLS или расшифровку IBE/AEAD. Пакет, который можно расшифровать, таким образом доказывает: шифротекст привязан к раунду R, и раунд R истек. Проверяющий закрепляет цепь (DrandTimelockProvider::quicknet()) и применяет проверку связывания с раундом по §11, поэтому пакет не может претендовать на раунд, к которому его шифротекст не привязан.

Это доказательство условия выпуска, а не отметка времени создания. После раунда R прошло, любой может создать новый шифртекст для R и упаковать его, который уже общедоступен подпись. Поэтому сам пакет НЕ ДОЛЖЕН описываться как доказательство того, что шифртекст или содержимое существовало до R, до unlock_at, или перед любым событием.

17.4 Пошаговая проверка в автономном режиме

qub-verify <file.qub> запускает стандартную процедуру §11 полностью из пакета, управляя qub_core::unlock::unlock с прикреплённым DrandTimelockProvider:

1. Parse the .qub bytes → QubBundle (canonical-CBOR guard; bound every field;
   re-check drand_round / drand_chain_id against the embedded sealed qub).
2. BLS-verify bundle.drand_signature for the pinned chain and round, then
   tlock_decrypt(sealed.tlock_ciphertext, bundle.drand_signature) → QubEnvelope.
3. Verify SHA3-256(body) == body_hash               (§11 step 8).
4. Verify QubEnvelope.qub_id   == SealedQub.qub_id   (§11 step 9).
5. Verify QubEnvelope.unlock_at == SealedQub.unlock_at (§11 step 10).
6. Verify ciphertext round == unlock_round(unlock_at) and the chain binding.
7. If sig_alg != 0x00: verify author_signature (and any cosigner; §9.4).
8. Report integrity, round-elapsed/round-binding, authorship, and cosigner
   verdicts separately, plus the recovered body. Do not report a commitment
   timestamp unless step 9 succeeds.
9. Optional existence-time leg: verify an included §16 proof through its pinned
   anchor, or independently verify the referenced storage transaction. Report
   its block time as an upper bound on ciphertext existence.

CLI завершает работу 0 (проверено), 1 (проверка не удалась — по-прежнему заблокировано, несоответствие хеша тела, нарушена связь раунда/цепочки или подпись не прошла проверку), или 2 (использование / поврежденный пакет). A --json отчет выносит те же заключения по поводу автоматизации. Поскольку пакет является автономным, корзина проверяющего (qub-core) и CLI (qub-verify) это единственное программное обеспечение, которое требуется третьей стороне; оба являются общедоступными и используют существующий путь проверки протокола — никакой индивидуальной криптографии.

17.5 Отношение к журналу прозрачности

inclusion_proof является необязательным слотом для доказательства включения Merkle §16. Проверка только пакета (§17.4) завершена для целостность, круговая привязка / круговое прошедшее время и необязательное авторство, но намеренно не имеет отдельного заявления о существовании с временной отметкой. Заполненный, полностью проверенный с привязкой к якорям inclusion_proof добавляет обязательство, специфичное для типа листа, и верхнюю границу времени из §16.11 без изменения версии формата пакета. Отсутствие доказательства означает только «доказательство не включено» — а не «недействительно» и не обязательно «не закреплено».

В справочной реализации слот теперь напечатанный: qub_core::export::QubBundle::inclusion_proof_typed() возвращает Option<InclusionProof> несущий полную структуру §16.9 (лист, путь аудита, закрепленный корень, и AnchorRef) через то же непрозрачное поле CBOR — без повышения версии формата пакета. Самостоятельный qub-verify CLI потребляет это через свой --anchor нога, и — до тех пор, пока кошелёк с якорем не будет подготовлен (§16, Статус) — сообщает о заполненном, но временном доказательстве владельца как только включение, а не полностью проверенное через якорь.