qub 프로토콜 명세

qub은 암호학적 시간 약속을 위한 프로토콜입니다. 미래의 특정 날짜에 단어들을 봉인하고, 그 날짜가 도래했을 때 무엇이 정확히 언제 말해졌는지를 증명하는 시스템입니다.

세 가지 기본 요소가 이를 가능하게 합니다. drand는 탈중앙화된 무작위성 비콘입니다 — 공개 일자는 어떠한 당사자의 선의가 아니라 물리 법칙에 의해 강제됩니다. 영구 공개 저장소는 변조 방지 공개 저장소입니다 — 어떠한 당사자도 한 번 봉인된 qub을 편집하거나 삭제할 수 없습니다. ML-DSA-65는 포스트 양자 디지털 서명입니다 — 각 qub은 비밀 키가 작성자의 기기를 절대 떠나지 않는 키 쌍에 결속됩니다.

이 기본 요소들이 함께 만들어내는 진술은 시간 잠금되어 있고, 변조가 드러나며, 귀속 가능한 — 세상이 과거를 조작하는 능력이 향상될수록 그 가치가 커지는 영수증입니다.

이 문서의 나머지 부분은 상호 운용 가능한 구현을 위해 요구되는 규범적 사양입니다.


qub 프로토콜 사양

항목
버전 1.0 (프로토콜 버전 0x01, 외부 래퍼 버전 0x01)
날짜 2026-05-01
상태 초안
검토 완료 시점 2026-05-01

이 문서는 qub 시간 약속 시스템의 규범적 프로토콜 사양입니다. 상호 운용 가능한 구현에 요구되는 데이터 구조, 직렬화 규칙, 도출 공식, 검증 절차를 정의합니다.

범위: 프로토콜 계층은 의도적으로 언어 중립적입니다 — qub 본문은 불투명한 평문 / 마크다운 / 약정 바이트이며, 로케일 인식 렌더링은 열람자의 책임입니다 (qub.social 웹 앱, <qub-embed> iframe, MCP 클라이언트 등).


1. 표기법 및 규약

표기 의미
u8, u64, i64 지정된 비트 폭의 부호 없는/부호 있는 정수
[u8; N] N 바이트의 고정 길이 바이트 배열
Vec<u8> 가변 길이 바이트 배열
Option<T> T 타입의 값, 또는 부재
String UTF-8 텍스트 문자열, NFC 정규화됨
`
SHA3-256(x) 바이트 문자열 x의 NIST SHA3-256 해시 (FIPS 202)
ceil(x) 천장 함수: x 이상의 가장 작은 정수
CBOR Concise Binary Object Representation (RFC 8949)
big-endian 최상위 바이트 먼저

원본 이미지 구성에 사용되는 모든 정수는 별도로 명시되지 않는 한 big-endian 고정 폭 바이트 배열로 인코딩됩니다 (i64 → 8 바이트, u8 → 1 바이트).

모든 타임스탬프는 UTC Unix 초입니다.


2. 데이터 구조

2.1 ComposeQub (작성자 인메모리 상태)

CBOR로 직렬화되지 않습니다. 영구 저장소에 기록되지 않습니다. 작성자 앱에 로컬로 존재합니다.

ComposeQub {
    draft_id:       [u8; 16],        // Random, generated locally
    created_at:     i64,             // Unix seconds UTC
    unlock_at:      Option<i64>,     // Unix seconds UTC; None while composing
    visibility:     u8,              // 0x01 = public (only value in MVP)
    content_type:   u8,              // 0x01 = text (only value in MVP)
    plaintext:      Vec<u8>,         // UTF-8 qub body
    sender_label:   Option<String>,  // Decorative display name; not authenticated
    status:         DraftStatus,     // Composing | Sealed | Uploaded | Failed
}

2.2 QubEnvelope (복호화된 페이로드)

정규 CBOR (§3)을 사용하여 직렬화됩니다. SealedQub 내부에 암호화됩니다. 복호화 후 콘텐츠 무결성을 증명하는 구조입니다.

QubEnvelope {
    version:             u8,              // Protocol major version (0x01 for v1)
    qub_id:              [u8; 32],        // Derived (see §4.1)
    content_type:        u8,              // Content type registry (see §6)
    created_at:          i64,             // Unix seconds UTC
    unlock_at:           i64,             // Unix seconds UTC
    outcome_at:          Option<i64>,     // V1.1 — when reality renders judgment (verdict-uplift-plan §3.1)
    sender_label:        Option<String>,  // Decorative; not authenticated in MVP
    reply_to:            Option<[u8; 32]>,// Parent qub_id for reply chains; not in qub_id preimage; not signed (see §9.3)
    body:                Vec<u8>,         // Content payload (UTF-8 for text, CBOR for pact)
    body_hash:           [u8; 32],        // SHA3-256(body) (see §4.2)
    sig_alg:             u8,              // Signature algorithm (see §9.2)
    author_signature:    Option<Vec<u8>>, // Set when sig_alg != 0x00
    author_pubkey:       Option<Vec<u8>>, // Set when sig_alg != 0x00
    cosigner_pubkey:     Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
    cosigner_signature:  Option<Vec<u8>>, // Set for cosigned pact bilateral agreements
}

기준선 (서명되지 않은 텍스트 qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, 모든 Option 필드는 부재.

기타 v1 구성: content_type = 0x03 (약정 본문, §6.1 참조); sig_alg = 0x01 (ML-DSA-65)에 author_signatureauthor_pubkey 존재 (§9.3 참조); 공동서명된 약정에 대해 cosigner_pubkeycosigner_signature 함께 존재 (§9.7 참조); 회신 체인 qub에 대해 부모 qub의 qub_id로 설정된 reply_to (서명 범위에 관한 함의는 §9.3 참조).

2.3 SealedQub (정규 와이어 형식)

정규 CBOR (§3)을 사용하여 직렬화됩니다. 영구 저장소에 기록됩니다. 이것이 온체인 산출물입니다.

SealedQub {
    version:           u8,              // Protocol major version (0x01 for v1)
    qub_id:            [u8; 32],        // Same as QubEnvelope.qub_id
    visibility:        u8,              // 0x01 = public; v1 viewers reject other values
    unlock_at:         i64,             // Unix seconds UTC
    outcome_at:        Option<i64>,     // V1.1 — surfaced on the verdict-watch CTA
                                        //   before reveal; mirrors QubEnvelope.outcome_at;
                                        //   bound to qub_id via the §4.1 preimage.
    drand_chain_id:    String,          // drand chain hash (hex string)
    drand_round:       u64,             // Target drand round number
    tlock_ciphertext:  Vec<u8>,         // tlock-encrypted QubEnvelope CBOR bytes
    recipient_pubkey:  Option<[u8; 32]>,// Reserved field; accepted by canonical CBOR
                                        //   but not interpreted by the v1 reference viewer
    title:             Option<String>,  // Plaintext title surfaced on the viewer
                                        //   countdown before reveal. Bound to qub_id
                                        //   via title_hash (§4.1). 1..=100 NFC code
                                        //   points, no control characters.
}

2.4 RevealedQub (열람자 애플리케이션 상태)

CBOR로 직렬화되지 않습니다. 열람자 앱에 로컬로 존재합니다. 복호화 및 검증 성공 후에 구성됩니다.

RevealedQub {
    qub_id:              [u8; 32],
    arweave_tx_id:       String,
    visibility:          u8,
    content_type:        u8,
    created_at:          i64,
    unlock_at:           i64,
    outcome_at:          Option<i64>,       // V1.1 — QubEnvelope.outcome_at / SealedQub.outcome_at에서 이어받음; 공개 페이지의 판정 대기 블록을 구동합니다 (verdict-uplift-plan §5.1)
    drand_chain_id:      String,
    drand_round:         u64,
    sender_label:        Option<String>,
    title:               Option<String>,    // Carried forward from SealedQub.title
    reply_to:            Option<[u8; 32]>,
    body:                Vec<u8>,
    body_hash:           [u8; 32],
    body_hash_verified:  bool,
    author_signature:    Option<Vec<u8>>,
    author_pubkey:       Option<Vec<u8>>,
    signature_verified:  Option<bool>,
    cosigner_pubkey:     Option<Vec<u8>>,
    cosigner_signature:  Option<Vec<u8>>,
    cosigner_verified:   Option<bool>,
}

3. 정규 CBOR 프로파일

모든 SealedQubQubEnvelope 직렬화는 이 프로파일을 준수해야 합니다. 동일한 논리적 구조가 주어진 두 구현은 동일한 바이트를 생성해야 합니다.

3.1 인코딩 규칙

규칙 사양
표준 RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements)
맵 키 정렬 인코딩된 바이트 길이 우선 정렬 (짧은 것이 긴 것보다 먼저), 그 다음 사전식 (동일 길이 인코딩에 대해 바이트별)
정수 인코딩 최단 형식: 0–23은 초기 바이트에; 24–255는 2 바이트에; 256–65535는 3 바이트에; 이하 동일
길이 인코딩 확정 길이만 허용. 불확정 길이 배열, 맵, 바이트 문자열, 텍스트 문자열 금지 (추가 정보 = 31 금지).
태그 CBOR 태그 없음 (주 타입 6 금지).
부동 소수점 부동 소수점 없음 (주 타입 7의 값 0xF9–0xFB 금지).
텍스트 문자열 UTF-8 인코딩, NFC 정규화됨 (유니코드 정규화 형식 C).
바이트 문자열 원시 바이트. CBOR 계층에서 base64 인코딩 없음.
중복 키 오류와 함께 거부. 파서는 중복 맵 키를 조용히 받아들여서는 안 됩니다.
알려지지 않은 키 오류와 함께 거부. 파서는 타입의 정규 키 집합 밖에 있는 맵 키를 허용해서는 안 됩니다 — 서로 다른 두 정규 바이트 문자열이 결코 같은 값으로 디코딩되어서는 안 되며 (encode(decode(x)) == x), 서명된 페이로드의 경우 추가 키는 두 서명이 모두 약속하는 숨겨진 콘텐츠가 됩니다. 스키마 진화는 추가 키가 아니라 version을 통해 이루어집니다.
단순 값 true (0xF5), false (0xF4), null (0xF6)만 허용됩니다.
선택적 필드 부재한 선택적 필드는 CBOR 맵에서 완전히 생략됩니다 (null로 인코딩되지 않음). 존재하는 선택적 필드는 정렬된 키 순서로 포함됩니다.

3.2 검증된 정규 키 순서

이 키 순서는 규범적입니다. 구현은 정확히 이 순서로 키를 방출해야 합니다. 디버그 어설션은 비-릴리즈 빌드에서 순서를 검증해야 합니다.

QubEnvelope (버전 0x01, 서명되지 않음, 모든 선택적 필드 부재):

"body"                (5 encoded bytes)
"qub_id"              (7 encoded bytes)
"sig_alg"             (8 encoded bytes)
"version"             (8 encoded bytes)
"reply_to"            (9 encoded bytes)   ← only if present (reply chains)
"body_hash"           (10 encoded bytes)
"unlock_at"           (10 encoded bytes)
"created_at"          (11 encoded bytes)
"outcome_at"          (11 encoded bytes)  ← only if present (V1.1 verdict mechanic)
"content_type"        (13 encoded bytes)
"sender_label"        (13 encoded bytes)  ← only if present
"author_pubkey"       (14 encoded bytes)  ← only if present
"cosigner_pubkey"     (16 encoded bytes)  ← only if present (pact cosign)
"author_signature"    (17 encoded bytes)  ← only if present
"cosigner_signature"  (19 encoded bytes)  ← only if present (pact cosign)

QubEnvelope 키 순서 도출: 각 키는 CBOR 텍스트 문자열입니다. 인코딩된 길이 = 1 바이트 헤더 + 문자열 길이 (24 바이트 미만의 문자열에 대해). 총 인코딩 길이 우선 정렬, 동일 길이 키에 대해서는 사전식 정렬.

SealedQub (버전 0x01, 공개, 수신자 없음):

"title"             (6 encoded bytes)   ← only if present
"qub_id"            (7 encoded bytes)
"version"           (8 encoded bytes)
"unlock_at"         (10 encoded bytes)
"outcome_at"        (11 encoded bytes)  ← only if present (V1.1 verdict mechanic)
"visibility"        (11 encoded bytes)
"drand_round"       (12 encoded bytes)
"drand_chain_id"    (15 encoded bytes)
"recipient_pubkey"  (17 encoded bytes)  ← only if present
"tlock_ciphertext"  (17 encoded bytes)

PactTerms (약정 본문, content_type 0x03):

"notes"         (6 encoded bytes)  ← only if present
"terms"         (6 encoded bytes)
"title"         (6 encoded bytes)
"party_a"       (8 encoded bytes)
"party_b"       (8 encoded bytes)
"pact_version"  (13 encoded bytes)

PactTerm (terms 배열의 행):

"key"    (4 encoded bytes)
"value"  (6 encoded bytes)

PartyIdentifier (party_a / party_b 맵):

"label"    (6 encoded bytes)
"contact"  (8 encoded bytes)  ← only if present

3.3 바이트 인코딩 참조

타입 CBOR 인코딩 예시
SHA3-256 해시 (32 바이트) 0x58 0x20 + 32 바이트 body_hash, qub_id
타임스탬프 (i64) 주 타입 0 (양수) 또는 1 (음수), 최단 인코딩 Unix 초
버전 (u8, 값 1) 0x01 (단일 바이트)
콘텐츠 타입 (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에 결속합니다. 봉투 콘텐츠로부터 결정론적으로 도출됩니다.

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

도메인 분리자 인코딩: 문자열 "QUB_ID_V2"는 9개의 ASCII 바이트입니다. 정렬을 위해 10 바이트에 도달하도록 단일 0x00 패딩 바이트가 추가됩니다. 구현은 정확히 다음 10 바이트를 사용해야 합니다: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].

outcome_at 인코딩: V1.1은 선택적 outcome_at 필드를 결속에 접어 넣기 위해 원본 이미지를 92 바이트에서 100 바이트로 확장했습니다. 부재한 outcome_at은 8개의 0 바이트로 인코딩됩니다; 프로토콜 검증기는 모든 곳에서 outcome_at <= 0을 거부하므로 이 센티넬이 정당한 값과 충돌할 수 없습니다. §3.2 (와이어 형식) 및 이 필드를 동기 부여하는 verdict 메커니즘에 대해서는 인트리 tasks/verdict-uplift-plan.md를 참조하십시오.

drand_round 인코딩: V1.2는 drand_round (대상 drand 라운드, §4.3)를 결속에 접어 넣기 위해 원본 이미지를 100 바이트에서 108 바이트로 확장했으며, 도메인 분리자를 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 정규화는 해시 시점에 실행되어 시각적으로 동등한 코드 포인트 시퀀스에 대해 다이제스트가 안정적입니다. 전체 0 센티넬은 부재 케이스를 위해 예약됩니다. 빈 문자열은 "부재"의 비정규 인코딩으로서 정규 CBOR 경계에서 거부됩니다 (정규 인코딩은 필드 전체를 생략합니다).

4.3 공개 라운드 매핑

drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
매개변수 출처 예시
unlock_at 사용자가 선택한 UTC Unix 초 1735689600 (2025-01-01 00:00:00 UTC)
chain_genesis_time drand 체인 정보 (genesis_time) 1595431050
chain_period_seconds drand 체인 정보 (period) 30

ceil() 연산은 공개 시간이 ≥ unlock_at인 첫 번째 drand 라운드를 선택합니다. 이는 qub이 선택된 공개 시간 이전에 복호화 가능해지지 않음을 보장합니다.

경계 케이스: (unlock_at - chain_genesis_time)chain_period_seconds로 정확히 나누어떨어지면, 결과는 정확히 그 라운드입니다 — qub은 그 라운드의 공개 시간에 정확히 잠금 해제됩니다.

검증: unlock_at은 봉인 시점에 미래여야 합니다. unlock_atcreated_at으로부터 10년 이상 떨어져서는 안 됩니다 (장기 drand 의존성 리스크를 제한하기 위함; UI는 2년 이상의 공개 일자에 대해 경고해야 합니다).


5. 와이어 형식 뉴타입

와이어 형식 뉴타입은 CBOR 바이트를 JSON, 원시 평문, 또는 다른 바이트 인코딩과 혼동하는 것에 대한 컴파일 타임 안전성을 제공합니다.

타입 포함 생성자 소비자
SealedQubCbor SealedQub의 정규 CBOR serialize_sealed_qub() 영구 저장소 업로드, 열람자 페치
QubEnvelopeCbor QubEnvelope의 정규 CBOR 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, 제한된 마크다운) 유료 50 KB / 무료 10 KB 렌더링 규칙은 §10 참조. 무료 / 유료 분할은 업로드 서비스에 의해 강제됩니다. 프로토콜 계층의 절대 상한은 50 KB입니다.
0x02 예약 (향후) 향후 콘텐츠 타입을 위해 할당됨; v1에서는 유효하지 않습니다. 열람자는 아래 규칙에 따라 거부해야 합니다.
0x03 약정 (양자 합의, CBOR 본문) 100 KB 본문은 정규 CBOR PactTerms입니다 (§6.1). 공동서명자 서명은 §9.7에 따릅니다.
0x04 판정 (작성자 자기 채점, CBOR 본문) 8 KB 본문은 정규 CBOR VerdictBody입니다 (§6.2). 시스템 측 verdict 의도에 의해서만 방출됩니다. 상위 관계는 본문이 아니라 Parent-Tx-Id Arweave 태그에 있습니다. verdict-uplift-plan §3.4 참조.

열람자는 알려지지 않은 콘텐츠 타입을 명확한 사용자 가시 오류와 함께 거부해야 합니다. 열람자는 알려지지 않은 타입을 텍스트로 렌더링하려 시도해서는 안 됩니다.

6.1 약정 본문 (content_type = 0x03)

약정 본문은 PactTerms 값의 정규 CBOR 인코딩입니다:

PactTerms {
    pact_version:  u8,                    // 0x01 for structured/v1
    title:         String,                // ≤ 200 bytes, NFC
    terms:         Vec<PactTerm>,         // ≤ 20 rows
    party_a:       PartyIdentifier,       // initiator
    party_b:       PartyIdentifier,       // counter-signer
    notes:         Option<String>,        // ≤ 5,000 bytes, NFC; absent key if none
}

PactTerm       { key: String (≤ 100), value: String (≤ 2,000) }   // NFC on both sides
PartyIdentifier{ label: String (≤ 100), contact: Option<String (≤ 320)> }

세 맵 모두에 대한 정규 CBOR 키 순서는 §3.2에 주어져 있습니다. 직렬화된 약정 CBOR 총 크기는 100 KB를 초과해서는 안 됩니다 (§6과 일치).

스키마 판별자. structured/v1 약정의 terms의 첫 번째 행은 { key: "pact_schema", value: "structured/v1" }이어야 합니다. 이 마커가 없는 행은 "custom" 약정이며 구조화된 검증이나 스키마 인식 렌더링을 받지 않습니다.

고정된 확인 슬롯. structured/v1 약정은 다음 키 하에 정확히 4개의 확인 행을 가집니다:

"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"

각각의 value(role, kind) 쌍에 의해 선택된 8개의 고정된 영어 문자열 중 하나이며, 여기서 role ∈ { seller, buyer, provider, client }, kind ∈ { standard, capacity }입니다. 문자열 자체는 규범적 프로토콜 데이터입니다 — 두 당사자의 ML-DSA-65 서명이 body_hash를 통해 정확한 바이트에 결속됩니다. 이들은 현지화되지 않습니다; 서명된 본문은 언어 중립적입니다. 어떠한 문구 변경도 새로운 스키마 버전 (structured/v2)을 요구합니다.

8개의 문자열, 그 조회 (acknowledgement_for(role, kind)), 그리고 각각의 근거는 참조 구현에 의해 고정됩니다. 준수 구현은 바이트 동일한 확인 값을 방출해야 합니다; 네 가지 역할 조합 모두를 다루는 골든 픽스처 SHA3-256 body-hash 테스트가 어떤 드리프트라도 포착합니다.

열람자 표시 순서. 확인 문자열에는 "described above"와 같은 어구가 포함되어 있으며, 이는 설명 / 범위 행이 확인보다 먼저 렌더링됨을 전제로 합니다. 열람자는 terms 배열을 CBOR 순서로 렌더링해야 합니다; 재정렬은 산문의 의미를 깨뜨립니다.

상대방 연락처. 당사자 B의 contact가 유효한 이메일 주소일 때, qub 업로드 서비스는 스테이지 시점에 검토 / 공동서명 초대 이메일을 자동 발송하고, 이후의 공동서명을 동일한 주소의 검증과 결속합니다 (§9.7). 당사자 B 연락처가 부재한 약정은 여전히 공동서명될 수 있지만, 오직 대역 외 채널을 통해서만 가능합니다 — 서비스는 일치하는 15분 이메일 검증 마커를 생성할 수 없는 공동서명 요청을 거부합니다.

6.2 판정 본문 (content_type = 0x04)

판정 본문은 VerdictBody 값의 정규 CBOR 인코딩입니다:

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 KB를 초과해서는 안 됩니다 (위 레지스트리 행과 일치).

Outcome 열거형. 와이어 바이트는 의도 중립적입니다; 네 가지 구분 Right / Partial / Wrong / Unfalsifiable은 판정을 동반하는 모든 의도의 결과 공간을 포괄합니다. 의도별 라벨(Right에 대해 "적중", "지킴", "출시", "확인" 등)은 상위 qub의 의도에 따라 해소되는 열람자 측 렌더링 사항입니다 — 와이어는 언어 및 의도 중립으로 유지됩니다. 1..=4 범위를 벗어난 값은 디코드 시점에 거부해야 합니다.

상위 연결. 판정 qub은 본문에 상위 참조를 담지 않습니다. 상위 qub의 Arweave 트랜잭션 id는 업로드 시점에 Parent-Tx-Id 저장소 태그로 방출됩니다(§7 저장소 태그 계층). 이렇게 하면 본문은 자기 평가에 대한 자체 완결적 서명 진술로 유지되며, 감사 체인("무엇에 대해 맞았는가?")은 Arweave 태그 조회를 통해 성립됩니다.

증거 URL 안전성 (규범적). evidence_url이 존재할 때, 검증자(작성 측, 와이어 측, Worker 엣지)는 다음을 강제해야 합니다:

  1. HTTPS만. 문자열은 바이트 시퀀스 https://로 시작해야 합니다. 그 외의 스킴 — http, ftp, javascript, data, file 등 — 은 거부합니다.
  2. 길이 상한. ≤ 2,048 바이트 (브라우저 URL 실용 한계).
  3. NFC + 적대적 코드포인트 확인. titlereflection과 동일한 규칙 — bidi-override / 영폭 / tag-block / BOM / C0 / C1 코드포인트는 거부합니다. 정의는 Rust의 crate::handle::contains_hostile_text_codepoint 및 TS의 workers/api/src/utils/unicode.ts::isHostileCodepoint와 일치해야 합니다 (일치 유지).
  4. 공백 없음, ASCII 제어 문자 없음. URL 어디에서도 공백 / DEL / 0x20 미만 바이트는 거부합니다 — bidi 규칙이 막지 못하는 \n/\t 주입 벡터를 차단합니다.
  5. 비어 있지 않은 호스트 세그먼트. https://와 첫 /, ?, 또는 # 사이의 모든 것은 비어 있지 않아야 합니다.

서버 측 페치 없음. Worker는 URL을 프록시하거나, 페치하거나, 미리보기 해서는 안 됩니다. 프로토콜은 문자열을 저장하며, 렌더링은 rel="nofollow noopener noreferrer" target="_blank"와 함께 링크 텍스트 옆에 표시되는 호스트와 함께 열람자 측에서 일어납니다.

회고. 선택적인 작성자 작성 회고 텍스트("무엇이 바뀌었나요, 무엇을 배우셨나요"). title과 동일한 NFC + 적대적 코드포인트 검증. 비어 있거나 공백만 있는 입력은 구성 시점에 부재로 접힙니다.

스키마 버전. v1은 verdict_version = 0x01만 지원합니다. 향후의 스키마 개정은 이 바이트를 올리고 §12에 따라 새로운 프로토콜 버전과 함께 배포합니다.


7. 봉인 프로토콜

완전한 봉인 시퀀스. 각 단계는 규범적입니다.

 1. User composes plaintext and metadata in ComposeQub.
 2. Validate:
    a. body is non-empty.
    b. body size ≤ max for content_type and user tier (see §6).
    c. unlock_at is in the future.
    d. unlock_at ≤ created_at + 10 years.
    e. content_type is a known, supported value.
 3. Compute body_hash = SHA3-256(body).
 4. Set created_at = current Unix seconds UTC.
 5. Select drand chain. Load chain_genesis_time and chain_period_seconds, and
    compute drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds).
    (Computed here, before qub_id, because drand_round is bound into the qub_id
    preimage — §4.1, V1.2.)
 6. Compute qub_id (see §4.1), folding in drand_round from step 5.
 7. Construct QubEnvelope with all fields.
 8. Serialise QubEnvelope using canonical CBOR → bytes B.
    Assert: serialised output matches canonical profile (§3).
 9. Compute C = tlock_encrypt(B, drand_round, drand_chain_public_key).
10. Construct SealedQub with tlock_ciphertext = C, and matching qub_id, version,
    unlock_at, drand_chain_id, drand_round.
12. Serialise SealedQub using canonical CBOR → SealedQubCbor.
12a. Generate K = 32 random bytes (CSPRNG) and N = 12 random bytes (CSPRNG).
     Compute W = wrap_sealed_qub(SealedQubCbor, qub_id=qub_id, key=K, nonce=N)
     per §13. The bytes uploaded to permanent storage are the OuterWrapper CBOR W,
     never the bare SealedQubCbor. K leaves the device only as the URL
     fragment in step 16.
13. Display seal-time disclosure. User confirms.
14. Validate upload eligibility via the qub upload service (bot-detection, entitlement, rate limits).
15. Submit W (the OuterWrapper bytes) to the qub upload service; the service
    signs and uploads to permanent storage. The service is byte-blind to the inner
    SealedQubCbor and never receives K.
16. Receive arweave_tx_id from the service. Construct delivery URL as
    `<origin>/c/<arweave_tx_id>#<base64url(K)>` (or `<origin>/s/<short_code>#<base64url(K)>`
    when a short code is allocated). Browsers do not transmit URL fragments
    to servers, so K is never observed by qub.social or any storage gateway.

저장소 태그 계층 (대역 외). qub 업로드 서비스는 래핑된 페이로드와 함께 의도적으로 작은 저장소 트랜잭션 태그 세트를 첨부합니다. Content-Type=application/octet-stream은 규범적으로 요구됩니다. 참조 서비스는 작성자가 노출하기로 선택한 경우 세 개의 선택적 태그를 추가로 첨부합니다: Intent (허용 목록 검증된 작성 의도 — 예: quote, reply, commitment), Author (64자 소문자 16진수로 표현된 작성자의 §9.3 공개 키 지문), Parent-Tx-Id (회신 체인에 대한 부모 qub의 저장소 트랜잭션 ID, 43자 base64url).

Author 태그는 qub별로 옵트인입니다: 참조 작성자 앱은 사용자가 봉인 시점에 공개 귀속을 명시적으로 활성화한 경우에만 이를 첨부합니다. 토글이 꺼져 있을 때 — 기본값 — Author 태그는 작성되지 않고 qub은 체인상에서 귀속 없이 존재합니다: 영구 저장소의 어떤 것도 업로드를 작성자의 핸들, 이메일, 또는 다른 qub과 연결하지 않습니다. 토글이 켜져 있을 때, Author 지문은 §9.5 증명 체인을 통해 작성자가 선택한 @handle로 해석됩니다. 회신 체인 관계 및 Intent는 비식별적입니다. 외부 래퍼 (§13)는 내부 본문을 암호문 상관 관계로부터 보호합니다 — 수집자가 qub 형태의 업로드를 인식하고 drand 라운드가 공개된 후 대량 복호화하는 것을 막습니다.

참조 서비스는 의도적으로 App-Name, App-Version, 또는 Type 태그를 첨부하지 않습니다: 그러한 단일 값 필터는 GraphQL 쿼리에 qub 전체 말뭉치를 반환할 것이며, 이는 래퍼의 본문 전용 기밀성 범위와 일치하지 않습니다.

준수 검증자는 §11 제3자 검증을 위해 어떠한 저장소 태그에도 의존해서는 안 됩니다; 본문 해시 / qub_id / 서명은 오직 내부 CBOR에만 결속되며, 태그 세트에는 절대 결속되지 않습니다.


8. 잠금 해제 프로토콜

완전한 잠금 해제 시퀀스. 각 단계는 규범적입니다.

 1. Viewer opens delivery URL. Extract arweave_tx_id from path AND
    K = base64url_decode(fragment) from the URL fragment. If the fragment
    is absent or malformed → display "this URL is missing its decryption
    key" and stop; the viewer MUST NOT contact the storage gateway
    without K, since fetching wrapped bytes the viewer cannot decrypt
    serves no purpose and only leaks the access attempt.
 2. Check denylist. If tx_id is denylisted → display block message. Stop.
 3. Fetch OuterWrapper bytes from permanent storage (with multi-gateway fallback).
 3a. Unwrap: parse the bytes as OuterWrapper (§13), verify the wrapper
    `version` byte is `0x01`, and compute SealedQubCbor =
    unwrap_sealed_qub(OuterWrapper, key=K). Any AEAD authentication
    failure (wrong K, tampered ciphertext, swapped qub_id-as-AAD,
    swapped nonce) → display "this URL's decryption key does not match
    the stored qub" and stop. Authentication failures are
    indistinguishable to the viewer per §13.5.
 4. Parse SealedQubCbor → SealedQub.
 5. Validate: SealedQub.version is known (0x01). Reject unknown versions.
 6. If current time < SealedQub.unlock_at → display countdown. Poll or wait.
 6a. Round-binding check (V1.2). Recompute expected_round =
    ceil((SealedQub.unlock_at - chain_genesis_time) / chain_period_seconds).
    Reject unless SealedQub.drand_round == expected_round AND the round baked
    into the tlock ciphertext stanza (read via the age/tlock header, no signature
    required) == expected_round. The stanza round is the one that actually gates
    decryption; without this check a malicious creator could bind the ciphertext
    to an already-past round while displaying a future countdown, so anyone
    reading the stored bytes could decrypt before unlock_at. Implementations with
    no chain identity (test mocks) skip this check.
 7. Once current time ≥ SealedQub.unlock_at:
    a. Fetch drand round signature for SealedQub.drand_round from drand network.
    b. Compute B = tlock_decrypt(SealedQub.tlock_ciphertext, round_signature).
 8. Parse B → QubEnvelope.
 9. Validate QubEnvelope.version is known.
10. Verify: SHA3-256(QubEnvelope.body) == QubEnvelope.body_hash.
    Fail → integrity error.
11. Verify: QubEnvelope.qub_id == SealedQub.qub_id.
    Fail → integrity error.
12. Verify: QubEnvelope.unlock_at == SealedQub.unlock_at.
    Fail → integrity error.
13. Verify: QubEnvelope.content_type is known and renderable.
    Known values: 0x01 (text), 0x03 (pact). Unknown → display error.
14. If QubEnvelope.sig_alg != 0x00 → verify author signature (see §9.4).
15. If cosigner_pubkey or cosigner_signature present → verify cosigner (see §9.7).
16. Render content using appropriate renderer (see §10 for text, §6 for pact).
17. Construct RevealedQub for display.

9. 작성자 서명

9.1 근거

qub은 영구 저장소에 저장됩니다. 작성자 서명은 무기한 위조 불가능해야 하며, 이것이 v1.0이 qub의 영구 수명 내에 보안성이 저하될 수 있는 고전적 방식이 아닌 포스트 양자 ML-DSA-65 방식 (FIPS 204)을 사용하는 이유입니다.

9.2 알고리즘 레지스트리

sig_alg 방식 키 크기 서명 크기
0x00 서명 없음 (서명되지 않음)
0x01 ML-DSA-65 (FIPS 204) 1,952 바이트 3,309 바이트

열람자는 알려지지 않은 sig_alg 값을 거부해야 합니다.

9.3 서명 원본 이미지 구성

두 가지 원본 이미지 버전이 존재해 왔습니다. 모든 서명은 V2를 사용해야 하며, 검증자는 V2만 수용해야 합니다. 레거시 V1 원본 이미지 (역사적 참조를 위해 아래에 문서화됨)는 V2 마이그레이션 동안 검증 전용 폴백으로 수용되었습니다; 그 폴백은 폐기되었으며 V1 전용 서명은 이제 거부됩니다.

V2 (현행 — 모든 신규 작성자 서명, 그리고 약정 스테이징 / 공동서명 흐름의 두 서명 모두에 의해 생성됨):

sig_input = SHA3-256(
    "QUB_AUTHOR_SIG_V2"  ||    // domain separator (17 bytes)
    version              ||    // u8 (1 byte)
    qub_id               ||    // [u8; 32] (32 bytes)
    body_hash            ||    // [u8; 32] (32 bytes)
    unlock_at            ||    // i64 big-endian (8 bytes)
    0x00                 ||    // u8 (1 byte): MUST be 0x00 in v1.x
    sender_label_hash    ||    // [u8; 32]: SHA3-256(NFC(sender_label)),
                               //   or 32 zero bytes when absent
    reply_to_or_zero           // [u8; 32]: parent qub_id, or 32 zero
                               //   bytes when absent
)

// Total preimage: 155 bytes → 32-byte hash

signature = Sign(author_secret_key, sig_input)

sender_label_hashtitle_hash (§4.2.1)와 동일한 부재 센티넬 관례를 따릅니다: 32개의 0 바이트는 유효한 SHA3-256 출력이 아니므로, "부재"는 존재하는 라벨과 절대 충돌할 수 없습니다. 모든 필드는 고정 너비이므로, 원본 이미지는 길이 접두사 없이도 모호하지 않습니다.

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_labelreply_to를 생략했습니다. 이는 V2로의 마이그레이션 동안 검증 전용 폴백으로 수용되었습니다; 그 폴백은 이후 폐기되었으며 — 검증자는 V2 원본 이미지만 수용해야 합니다. 이 정의는 역사적 참조와 아래의 도메인 분리자 설명을 위해 여기에 보존됩니다. V1에 대해서만 검증되는 서명은 검증 실패로 취급되어야 합니다.

도메인 분리자: "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이어야 합니다. 참조 구현은 이를 crates/qub-core/src/signing.rs에서 상수 ORG_ID_PRESENT_INDIVIDUAL = 0x00으로 노출합니다; 검증을 위해 sig_input을 재구성하는 열람자는 동일한 바이트를 방출해야 합니다.

서명 범위 — 무엇이 다루어지고 무엇이 다루어지지 않는가. V2 sig_inputversion, qub_id, body_hash, unlock_at, sender_label, reply_to에 직접 결속됩니다 (고정된 도메인 분리자 및 org_id_present 바이트에 더해). qub_id 자체는 §4.1 원본 이미지를 통해 version, content_type, created_at, unlock_at, outcome_at, drand_round, body_hash로부터 도출되므로, 그 필드들의 어떤 변경도 다른 qub_id를 생성하고 서명을 전이적으로 무효화합니다. 따라서 인증되는 표면은:

필드 서명에 의해 인증됨 방법
version sig_input에 대한 직접 입력
qub_id 직접 입력
body_hash 직접 입력
unlock_at 직접 입력
sender_label sender_label_hash를 통한 직접 입력 (V2 원본 이미지 — 유일하게 수용되는 형식)
reply_to reply_to_or_zero를 통한 직접 입력 (V2 원본 이미지 — 유일하게 수용되는 형식)
content_type 전이적으로, qub_id 원본 이미지를 통해
created_at 전이적으로, qub_id 원본 이미지를 통해
outcome_at 전이적으로, qub_id 원본 이미지를 통해
drand_round 전이적으로, qub_id 원본 이미지를 통해 (V1.2)
body 전이적으로, body_hash = SHA3-256(body)를 통해
author_pubkey — (암묵적) 서명을 검증한 키가 정의상 작성자임
cosigner_pubkey / cosigner_signature 동일한 sig_input에 대해 독립적으로 서명됨 (§9.7 참조)
drand_chain_id, tlock_ciphertext, visibility 외부 SealedQub 필드, 봉투 내부에 없음 — 자체 구조적 불변량 (라운드 / 체인 일관성)에 의해 다루어지지만 작성자 서명에 의해서는 다루어지지 않음. (drand_round은 이제 qub_id 원본 이미지를 통해 전이적으로 결속됩니다 — 위 참조.)

왜 V2가 유일하게 수용되는 원본 이미지인가.

sender_label 또는 reply_to를 최종 사용자에게 표시하는 구현은 라벨이 아니라 인증된 정체성 (공개 키 지문, 증명)을 주요 정체성 신호로 표면화해야 합니다.

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). 더 저렴한 모든 검사 (해시, qub_id, unlock_at)가 통과된 후에 수행되어야 합니다.

9.5 정체성 증명

정체성 증명 — author_pubkey를 qub 핸들, 이메일 주소, 소셜 핸들, 패스키 자격 증명과 같은 사람이 인식 가능한 정체성 청구로 매핑하는 것 — 은 열람자측 점진적 향상이며 서명 검증에 필요하지 않습니다. 증명을 표시 정체성으로 해석하는 열람자는 다음 우선순위를 적용해야 합니다:

handle > email > social > fingerprint

지문 폴백은 SHA3-256(author_pubkey)의 소문자 16진수입니다; 이는 서명된 어떤 qub에 대해서도 항상 사용 가능합니다. 열람자는 표시를 위해 이를 축약할 수 있습니다 — 참조 열람자는 qub: 뒤에 처음과 마지막 4 바이트를 렌더링합니다 (qub:<8 hex>…<8 hex>).

준수 검증자는 qub API에 연락하지 않고, 영구 저장소 및 drand 이외의 어떠한 네트워크 없이도, 어떠한 서버측 조회 없이도 §9.4의 모든 검사를 완료할 수 있습니다. 증명 해석은 서명 검증이 성공한 후에만 수행되는 별도의 최선 노력 단계입니다.

9.6 크기 영향

Ed25519 ML-DSA-65
서명 64 바이트 3,309 바이트
공개 키 32 바이트 1,952 바이트
qub당 총합 96 바이트 5,261 바이트
저장 비용 차이 (~$5/MB 기준) ~$0.0005 ~$0.026

500–2,000 바이트의 텍스트 qub에 대해, ML-DSA-65는 저장 크기를 대략 세 배로 만듭니다. 절대 비용은 무시할 만합니다.

9.7 공동서명자 검증 (약정 양자 합의)

양자 합의 (content_type = 0x03)에 대해, 두 번째 서명 계층은 두 당사자 모두가 동일한 조건에 동의했음을 증명합니다.

봉투 필드:

두 필드 모두 함께 존재하거나 모두 부재해야 합니다. 정확히 하나만 존재하면, 열람자는 무결성 오류를 보고해야 합니다.

검증 절차:

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

속성:

이메일 결속 게이트 (운영적). 스테이지된 약정이 당사자 B 이메일 연락처 (§6.1)를 포함할 때, qub 업로드 서비스는 스테이징 ID 및 그 연락처의 정규화 이메일 해시 모두와 일치하는 단명 이메일 검증 마커가 존재하지 않으면 공동서명 요청을 거부해야 합니다. 매직 링크 토큰이 staging_id를 포함하고 검증된 주소가 SHA-256(normalise_email(party_b.contact))과 일치할 때 /api/v1/auth/verify에 의해 마커가 작성됩니다 — 여기서 normalise_email(addr)은 로컬 부분의 대소문자를 보존하고 도메인 부분만 소문자화하며 (RFC 5321 §2.3.11에 따라), 여기서 SHA-256은 NIST FIPS 180-4 해시입니다 (§4 도출에서 사용되는 SHA3-256과는 구별됨) — 발행 후 900초 (15분) 후에 만료됩니다. 이는 운영적 사칭 방지 게이트이며, 온체인 qub 증명의 일부가 아닙니다 — §11을 재현하는 제3자 검증자는 어떠한 서버측 조회 없이 영구 저장소 및 drand만을 필요로 합니다. 마커는 오직 서버측에만 존재하며 서명된 본문의 일부가 절대 아닙니다.

크기 영향 (ML-DSA-65 작성자 + 공동서명자):

구성 요소 크기
작성자 서명 3,309 바이트
작성자 공개 키 1,952 바이트
공동서명자 서명 3,309 바이트
공동서명자 공개 키 1,952 바이트
총 암호 오버헤드 10,522 바이트
저장 비용 차이 ~$0.05

10. 마크다운 렌더링 및 살균

이 절은 보안에 결정적입니다. 열람자는 제한된 마크다운 부분집합을 사용하여 텍스트 qub (content_type = 0x01)을 렌더링합니다.

10.1 허용되는 요소

10.2 금지되는 요소

요소 처리
원시 HTML (<div>, <script> 등) 완전히 제거됨. 어떠한 HTML도 통과하지 않음.
이미지 (![alt](url)) 제거됨. 이미지 구문은 출력에서 제거됨.
링크 ([text](url)) URL이 보이는 평문으로 렌더링됨. 자동 링크 없음. 명시적 사용자 행동 없이는 클릭 불가능.
위험한 URL 스킴 javascript:, data:, vbscript:, file: — 제거됨.
iframe, 임베드, 객체 제거됨.
HTML 엔티티 안전한 경우에만 표시 문자로 디코딩됨.

10.3 구현

구현은 차단 목록이 아닌 엄격한 허용 목록 파서를 사용해야 합니다. 권장 접근 방식:

  1. pulldown-cmark (또는 동등물)을 사용하여 마크다운을 파싱합니다.
  2. AST를 순회하며 허용 목록 (§10.1)에 없는 노드를 모두 제거합니다.
  3. 링크 노드의 경우: URL을 클릭 가능한 <a> 요소가 아닌 보이는 텍스트로 방출합니다.
  4. 필터링된 AST를 타입화된 중간 표현으로 변환합니다 (예: 안전한 변형만 가지는 MarkdownNode 열거형). 원시 HTML은 이 IR에서 구조적으로 표현 불가능합니다.
  5. 타입화된 IR로부터 대상 뷰 계층으로 렌더링합니다 (예: 반응형 뷰 컴포넌트, DOM 노드). 어느 지점에서도 HTML 문자열 연결 또는 innerHTML 사용 없음.

차단 목록 접근 방식은 새로운 마크다운 확장이나 파서의 특이사항이 필터링되지 않은 요소를 도입할 수 있어 취약합니다. 타입화된 AST 접근 방식은 XSS를 구조적으로 불가능하게 만듭니다 — 임의의 HTML을 담을 수 있는 변형이 존재하지 않습니다.

10.4 크기 및 구조 제한


11. 제3자 검증

어떠한 제3자도 qub의 협력 없이 공개 qub을 검증할 수 있습니다. 검증 절차:

1. Obtain arweave_tx_id (from delivery URL or direct knowledge).
2. Fetch SealedQubCbor from any storage gateway.
3. Confirm storage block inclusion (block height, block timestamp).
4. Parse SealedQubCbor → SealedQub.
5. Fetch drand round signature for SealedQub.drand_round.
6. tlock_decrypt(tlock_ciphertext, round_signature) → QubEnvelope CBOR bytes.
7. Parse → QubEnvelope.
8. Verify SHA3-256(body) == body_hash.
9. Verify QubEnvelope.qub_id == SealedQub.qub_id.
10. Verify QubEnvelope.unlock_at == SealedQub.unlock_at.
11. If sig_alg != 0x00: verify author_signature (see §9.4).
12. All checks pass → qub is verified.

검증이 증명하는 것:

증명 무엇을 확립하는가
약속 암호문이 저장소 블록 타임스탬프까지 존재했음.
무결성 평문 본문이 약속된 해시와 일치하며 변경되지 않았음.
타이밍 콘텐츠는 선택된 공개 시간에 해당하는 drand 라운드까지 읽을 수 없었음 (tlock 및 drand 보안 가정에 따름).

검증이 증명하지 않는 것:

비증명 이유
작성권 sender_label은 장식적입니다. sig_alg0x01 없이는 누구든지 이 콘텐츠를 봉인했을 수 있습니다.
의도 qub은 콘텐츠와 타이밍을 증명하며, 작성자가 주관적으로 의미한 바를 증명하지 않습니다.
사전 이벤트 타이밍 저장소 블록 포함은 실제 업로드보다 몇 분 지연될 수 있습니다. 약속 타임스탬프는 블록 시간이며, 사용자가 "봉인하다"를 누른 순간이 아닙니다.

12. 버전 관리

12.1 프로토콜 버전

SealedQubQubEnvelope 모두의 version 필드 (u8)는 주 프로토콜 버전을 식별합니다.

12.2 버전 히스토리

버전 설명
v1 0x01 공개 텍스트 qub (content_type 0x01), 약정 양자 합의 (0x03, structured/v1 스키마, ML-DSA-65 작성자 + 공동서명자), tlock, SHA3-256

12.3 정방향 호환성

알려지지 않은 CBOR 맵 키 (§3.2 정규 순서에 없는 키)를 가진 QubEnvelope을 만나는 v1 열람자는 디코드 오류와 함께 그것을 거부해야 합니다 (§3.1). 정방향 호환성은 키 허용이 아니라 version 필드에 실립니다: 향후의 추가 (사소한 메타데이터라도) 는 새로운 version 값 아래 배포되며, v1 열람자는 서명이 약속하는 콘텐츠를 조용히 버리는 대신 명확한 "더 새로운 프로토콜" 오류와 함께 그것을 거부합니다.

sig_alg = 0x01 (ML-DSA-65)을 만나지만 ML-DSA-65 검증 지원이 부족한 v1 열람자는 qub을 완전히 거부하기보다 "서명은 존재하나 검증할 수 없음" 알림과 함께 qub 콘텐츠를 표시해야 합니다. 참조 구현은 오늘 v1 레지스트리에 다른 유효한 알고리즘이 없기 때문에 0x000x01을 제외한 모든 sig_alg 값을 거부합니다 — 엄격한 거부와 소프트 페일은 세 번째 알고리즘이 등록될 때까지 관찰적으로 동일합니다. §9.2가 새 항목을 인정하는 순간 위의 소프트 페일 동작은 부담을 지게 되며, 그 시점에서 참조 열람자는 소프트 페일하도록 업데이트될 것입니다.

12.4 외부 래퍼 버전

§13에 기술된 OuterWrapper는 SealedQub.versionQubEnvelope.version독립적인 자체 version 바이트를 가집니다. 두 버전 공간은 별도로 발전합니다: 향후 포스트 양자 안전 대칭 교체는 내부 프로토콜 버전을 건드리지 않고 래퍼 바이트를 올리고, 향후 프로토콜 계층 추가 (예: 새 봉투 필드)는 래퍼 바이트를 건드리지 않고 내부 버전을 올립니다.

OUTER_WRAPPER_VERSION_* 알고리즘 상태
OUTER_WRAPPER_VERSION_1 0x01 12 바이트 논스, 16 바이트 인증 태그를 가진 AES-256-GCM, qub_id에 결속된 AAD v1 기본값
0x020xFF 예약됨 향후

열람자는 알려지지 않은 래퍼 버전을 명확한 오류와 함께 거부해야 합니다. 프로토콜은 구체적인 마이그레이션 동인이 나타날 때까지 (예: 다른 AEAD를 선호하는 NIST 지침) 의도적으로 래퍼 버전 공간을 좁게 유지합니다; 0x02 슬롯은 알고리즘을 도입하는 동일한 개정판에서 할당될 것입니다.


13. 외부 암호화 래퍼

13.1 근거

프로토콜 계층 (QubEnvelope → tlock → SealedQub)은 봉인된 qub을 시간 잠금시킵니다: 본문은 unlock_at까지 그리고 drand 라운드 서명이 공개될 때까지 읽을 수 없습니다. 그러나 잠금 해제 후에는 라운드 서명이 공개되고 SealedQub의 정규 CBOR 형태가 인식 가능하므로, 영구 저장소 트랜잭션을 색인한 수집자는 qub 전체 말뭉치를 대량으로 복호화할 수 있습니다.

외부 암호화 래퍼는 정규 SealedQubCbor와 영구 저장소에 기록되는 바이트 사이에 추가적인 대칭 AEAD 계층을 끼워 넣음으로써 그 채널을 닫습니다. 256비트 키 K오직 전달 링크의 URL 프래그먼트와 사용자 기기에만 존재합니다; 브라우저는 URL 프래그먼트를 서버로 전송하지 않으므로, qub.social, 모든 저장소 게이트웨이, 그리고 그 앞에 있는 모든 CDN은 관찰적으로 K에 대해 맹목적입니다. 따라서 영구 저장소의 모든 qub은 작성자가 공유하기로 선택한 URL 없이는 평문을 복구할 수 없는 불투명한 암호문입니다.

순효과:

13.2 계층화

plaintext body                       ← QubEnvelope.body (§2.2)
  ↓ canonical CBOR (§3)
envelope CBOR
  ↓ tlock encrypt to drand round (§7 step 10)
tlock_ciphertext (inside SealedQub) (§2.3)
  ↓ canonical CBOR (§3)
SealedQubCbor bytes                  ← inner wire artifact
  ↓ AES-256-GCM(K, nonce, AAD=qub_id) (§7 step 12a, this section)
OuterWrapper CBOR bytes              ← uploaded to permanent storage (§7 step 15)

프로토콜 계층의 봉인 및 잠금 해제 (§7, §8)는 래퍼 경계 아래에서 변경되지 않습니다; 래퍼는 seal()의 호출 사이트에서 부착되고 unlock()의 호출 사이트에서 분리됩니다.

13.3 OuterWrapper 데이터 구조

struct OuterWrapper {
    version:    u8,           // 0x01, see §12.4
    qub_id:     [u8; 32],     // copied from inner SealedQub; AEAD AAD
    nonce:      [u8; 12],     // 96-bit AEAD nonce
    ciphertext: Vec<u8>,      // AES-256-GCM(K, nonce, SealedQubCbor, AAD=qub_id) || 16-byte tag
}

필드 불변량.

CBOR 인코딩. 동일한 키 정렬 규칙 (인코딩된 바이트 길이 오름차순, 그 다음 사전식 정렬)을 가진 §3에 따른 정규 CBOR. 네 개의 키는:

인코딩된 바이트 순서
nonce 6 1
qub_id 7 2
version 8 3
ciphertext 11 4

따라서 OuterWrapper CBOR의 첫 바이트는 4-항목 맵에 대한 확정 길이 맵 헤더 (0xA4)입니다.

13.4 qub_id에 대한 AAD 결속

래퍼는 qub_id를 AEAD 추가 인증 데이터로 결속합니다. 이는 세 종류의 공격에 대한 부담을 지는 구조적 방어입니다:

공격 방어
래퍼의 다른 qub_id 필드 아래로 암호문 이동 AAD 불일치 → AEAD 인증 실패
qub A의 URL 프래그먼트를 qub B의 영구 저장소 바이트와 혼합 AAD 불일치 → AEAD 인증 실패
업로드 후 래퍼의 qub_id 필드 변조 AAD 불일치 → AEAD 인증 실패

래퍼 평문에 qub_id를 포함시키는 것은 열거 면역성을 의미 있게 약화시키지 않습니다 — qub_id 자체가 다이제스트로부터 복구 가능한 원본 이미지가 없는 §4.1 원본 이미지의 SHA3-256 해시이며, 래퍼 바이트를 이미 수집한 열거자는 업로드의 존재 자체로부터 추론할 수 없는 어떠한 것도 가시적인 qub_id로부터 알 수 없습니다.

13.5 래핑 및 언래핑 알고리즘

wrap_sealed_qub(SealedQubCbor S, qub_id Q, key K, nonce N):
    require K.len() == 32 and N.len() == 12 and Q.len() == 32
    C := AES_256_GCM_encrypt(key=K, nonce=N, msg=S, aad=Q)
    // C includes the 16-byte authentication tag at the end
    return canonical_cbor_encode(OuterWrapper{
        version:    0x01,
        qub_id:     Q,
        nonce:      N,
        ciphertext: C,
    })

unwrap_sealed_qub(OuterWrapper bytes W, key K):
    require K.len() == 32
    O := canonical_cbor_decode(W) as OuterWrapper
    require O.version == 0x01           // §12.4
    P := AES_256_GCM_decrypt(
            key=K, nonce=O.nonce, ciphertext=O.ciphertext, aad=O.qub_id
         )
    // any AEAD failure → DECRYPT_FAILED, indistinguishable to caller
    return P                            // P is the inner SealedQubCbor

실패 모드 붕괴. 잘못된 K, 잘못된 논스, AAD 불일치, 변조된 암호문은 모두 동일한 DECRYPT_FAILED 오류를 생성합니다. 이는 의도적인 AEAD 속성입니다: 실패 모드를 구별한다면 원격 공격자가 잘못된 형식의 래퍼를 보내고 응답 시간을 측정하여 탐색할 수 있는 사이드 채널을 만들어낼 것입니다. 참조 구현은 모든 AEAD 실패를 단일 오류 형태로 붕괴시켜야 합니다.

13.6 키 자료 및 배포

래핑 키 K는 CSPRNG에 의해 qub당 생성되는 256비트 균일 무작위 값입니다. 참조 구현은 이를 다음에서 소싱합니다:

배포: K는 URL 안전 base64 (RFC 4648 §5, 패딩 없음)로 인코딩되어 전달 링크의 프래그먼트 구성 요소로 추가되어야 합니다:

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

프래그먼트는 준수하는 브라우저에 의해 어떠한 서버로도 전송되지 않습니다. 사용자 기기를 넘어 프래그먼트를 포함한 전체 전달 링크를 유지하는 복구 채널 (서버측 기록 색인, 옵트인 이메일 자동 전송)은 기본 암호 분쇄 자세에 대한 명시적인 교환이며, 명시적인 사용자 동의에 게이트되어야 합니다.

프래그먼트 손실. 사용자가 URL 프래그먼트를 잃고 복구 채널이 없으면, qub은 읽을 수 없습니다. 이는 설계의 부담을 지는 교환이며 봉인 시점에 사용자에게 공개되어야 합니다. MVP는 명시적인 "이 URL을 저장하라" 문구 및 옵트인하는 사용자에 대한 검증된 이메일 복구 채널로 봉인 시점 공개를 강화합니다.

13.7 이 절의 범위 외

13.8 공개 qub (래퍼 생략)

외부 래퍼는 전달 계층에서 선택적입니다. 작성자는 qub을 공개로 봉인할 수 있으며, 이 경우 정규 SealedQubCborOuterWrapper 계층 없이 그리고 키 K 없이 영구 저장소에 직접 기록됩니다:

SealedQubCbor bytes  ──(public)──▶  uploaded to permanent storage as-is
SealedQubCbor bytes  ──(private)─▶  AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded

공개 qub은 시간 잠금되지만 링크 게이트되지 않습니다: drand 라운드가 공개될 때까지는 읽을 수 없는 상태로 유지되지만 (tlock 계층은 변경되지 않음), 잠금 해제 후에는 arweave_tx_id를 가진 누구나 이를 복호화할 수 있습니다 — K가 없으므로 URL 프래그먼트가 필요하지 않습니다. 이는 서버가 구동해야 하는 표면들을 위한 의도적인 교환입니다: 공개 알림 이메일, 제3자 임베드, 그리고 더 풍부한 공개 후 SEO는 모두 서버가 결코 보유하지 않는 비밀 없이도 작동하는 링크를 필요로 합니다 (§13.6).

생산자가 반드시 고려해야 하는 결과:

비공개 (래핑됨)가 기본값으로 유지됩니다; 공개는 명시적인 qub별 작성자 선택입니다.


14. 테스트 벡터

14.1 qub_id 도출

Input:
  version      = 0x01
  content_type = 0x01
  created_at   = 1735689600 (2025-01-01 00:00:00 UTC)
  unlock_at    = 1736294400 (2025-01-08 00:00:00 UTC)
  outcome_at   = absent
  drand_round  = 4695445  (= (1736294400 - 1595431050) / 30, drand mainnet params §14.2)
  body         = "Hello, future."  (UTF-8, 14 bytes)
  title        = absent

Intermediate:
  body_hash  = SHA3-256("Hello, future.")
             = 76ab8b3f843c6ed4f2d0fd75b9f457b4
               ad49dd4450f9c22723ae430e3af3211d
  title_hash = [0u8; 32]   (title absent — §4.2.1 sentinel)

Domain separator (10 bytes):
  [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00]

Preimage (108 bytes — V1.2):
  domain_separator   ||  // 10 bytes
  0x01               ||  // version
  0x01               ||  // content_type
  0x0000000067748580 ||  // created_at as i64 big-endian (1735689600)
  0x00000000677DC000 ||  // unlock_at as i64 big-endian (1736294400)
  0x0000000000000000 ||  // outcome_at_or_zero (outcome_at absent)
  0x000000000047A595 ||  // drand_round as u64 big-endian (4695445)
  body_hash          ||  // 32 bytes
  title_hash             // 32 bytes (all-zeros sentinel; title absent)

Expected output:
  qub_id = SHA3-256(preimage)
         = 3a9fcb31b750d985c262fada6d4f777f
           d6a28be831d941d85c131f5a4bbaf8a4

구현은 이 입력에 대해 동일한 body_hashqub_id 값을 산출해야 합니다. 이 테스트 벡터는 가장 먼저 작성되는 단위 테스트여야 합니다. 위의 정규 값들은 참조 구현에 의해 계산되었으며 비트 단위로 일치해야 합니다. 과거의 원본 이미지 레이아웃 (사전 출시 — 어떠한 라이브 qub도 이 값들에 의존하지 않았음): 92 바이트 V1.0 qub_id는 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0였고; 100 바이트 V1.1 qub_id (outcome_at_or_zero를 접어 넣은 후)는 b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed였습니다. V1.2는 drand_round을 접어 넣고 도메인 분리자를 QUB_ID_V2로 올립니다.

14.2 잠금 해제 라운드 매핑

Input:
  unlock_at           = 1735689600
  chain_genesis_time  = 1595431050
  chain_period_seconds = 30

Calculation:
  (1735689600 - 1595431050) / 30 = 4675285.0
  ceil(4675285.0) = 4675285

drand_round = 4675285

14.3 정규 CBOR 라운드 트립

구현은 모든 유효한 입력에 대해 serialize(parse(serialize(qub))) == serialize(qub)을 검증해야 합니다. 이는 단일 벡터가 아닌 속성 테스트입니다.

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 바이트 및 SHA3-256 body_hash는 참조 구현에 의해 계산됩니다. 구현은 이 입력에 대해 바이트 동일한 CBOR을 생성해야 합니다.

구현은 또한 모든 유효한 PactTerms 입력에 대해 serialize(parse(serialize(pact))) == serialize(pact)을 검증해야 합니다 (속성 테스트).

14.5 외부 래퍼 교차 언어 벡터

외부 래퍼 (§13)는 crates/qub-core/tests/vectors/wrapper_v1.json에 별도의 정규 픽스처를 가집니다. 각 케이스는 (key, nonce, qub_id, sealed_cbor) 튜플을 불투명한 16진수 입력으로 고정하고 특정 expected_wrapper_hex 출력을 단언합니다. 두 참조 구현 모두 동일한 JSON 파일을 소비합니다:

픽스처는 현재 세 케이스를 고정합니다:

케이스 범위
basic-text-public 가장 작은 현실적인 SealedQub 형태; 선택적 필드 없음. v1.0-전형적 qub에 대한 정규 래퍼 형태를 확립합니다.
with-recipient-pubkey recipient_pubkey가 설정된 SealedQub (Phase 2 경로). 다른 내부 CBOR 키 세트, 다른 qub_id.
longer-body ~4 KiB 본문 — 내부 봉투와 외부 암호문 모두 안에서 멀티바이트 CBOR 길이 접두사를 실행합니다.

구현은 기록된 입력에 대해 바이트 동일한 expected_wrapper_hex를 생성해야 합니다. 픽스처 재생성은 QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors를 요구하며 의도적인 형식 변경을 위해 예약됩니다.


15. 암호 프로파일 거버넌스 (향후)

이 절은 v1에 대해 정보적이며 qub의 암호 기본 요소 중 어디라도 두 번째 알고리즘이 진입하는 첫 시점에 규범적이 됩니다.

15.1 현재 자세

프로토콜 v1은 기본 요소당 정확히 하나의 알고리즘에 결속됩니다:

검증자는 현재 기본 요소별로 키와 서명 길이를 하드코딩합니다. 와이어 형식에 의해 노출되는 민첩성 표면은 없습니다.

15.2 의도된 형태

두 번째 알고리즘이 프로토콜에 진입할 때, 검증자는 기본 요소별로 허용되는 값의 정확한 집합 — sig_alg, drand 체인, 래퍼 버전, 콘텐츠 타입 — 을 나열하는 이름 붙은 CryptoProfile (예: ExqubV1)에 대해 구성될 것입니다. 프로파일은 검증 시점에 고정되며, 대역 내에서 협상되지 않습니다. 활성 프로파일 밖의 어떠한 값도 거부됩니다.

이는 ML-DSA-87을 추가하거나 Ed25519를 활성화하는 것이 기존 검증자 구성을 소급적으로 약화시킬 수 없음을 보장합니다: v1 검증자는 v2 프로파일이 게시된 후에도 v1 검증자로 남습니다.

15.3 트리거 조건

다음 중 어떠한 것이라도 제안되면 §15를 규범적 상태로 승격합니다:

그때까지 §15는 향후 PR이 협상 표면을 처음부터 재논의하기보다 알려진 대상에 대해 착지하도록 마이그레이션 형태를 고정하는 자리 표시자입니다.


16. 투명성 로그 및 내구성 계층 (설계 — 검토 완료)

상태. 이 절은 설계 명세입니다. 아래의 와이어 형식, 해싱, 신뢰 모델은 구현에 대해 규범적이지만, 아직 투명성 로그 코드는 출시되지 않았습니다. W5 외부 검토는 완료되었습니다: §16.15는 해결된 결정과 그로부터 도출된 결속적 출시 제약을 기록합니다. 구현은 그 제약 하에서 진행될 수 있습니다. §16은 §15와 동일한 의미에서 미래 지향적으로 남습니다 — 구현이 코드 리뷰에서 신뢰 모델을 다시 도출하기보다 확정된 설계에 착지하도록 목표를 고정합니다. 이는 엄격하게 추가적입니다 — 기존의 모든 qub은 개별 Arweave 트랜잭션을 유지하며 SealedQub / QubEnvelope 와이어 형식에는 변경이 없습니다.

16.1 근거 및 내구성 계층

오늘날 qub의 내구성과 그 시간적 약속은 모두 qub당 단일 Arweave 트랜잭션 (§11)에 기댑니다. 이는 봉인 지연 시간을 Arweave 최종성에 결합시키고, qub당 업로드를 제품 비용 상한 (ARWEAVE_DAILY_CEILING)으로 만들며, qub 간에 변조 탐지가 가능한 순서를 제공하지 못합니다. 투명성 로그는 그 단일 계층 아래와 주변에 두 계층을 추가합니다:

계층 이름 보장 시점
T1 R2 우선 동기 ACK 내구성 하한 — 봉인 바이트가 봉인 반환 전에 영구 저장소에 기록됩니다 (< 300 ms p95). 모든 qub, 동기적 (§16.10).
T2 일괄 처리된 투명성 로그 포함 보편적 추가 전용, 변조 탐지가 가능한 약속 + 전체 순서, Arweave에 앵커링됨. 모든 qub, 지연 + 일괄 처리 (§16.5–16.7).
T3 qub당 Arweave 영속성 qub에 대한 개별 Arweave 트랜잭션. 유료 업셀, 그리고 Arweave 가용 불가 시 폴백 (§16.8).

T2는 qub당 Arweave를 유일한 내구성 경로가 아닌 선택 (T3)으로 만듭니다. ARWEAVE_DAILY_CEILING은 제품 상한으로서 폐기되고 전용 앵커 지갑에 대한 회로 차단기로만 강등됩니다 (§16.7); 사용자 봉인은 그것을 초과한다는 이유로 결코 거부되지 않습니다.

내구성 정직성 (해결됨 — §16.15 Q6). 내구성은 퇴행하지 않습니다: T1 R2 기록은 동기적이고 일회 기록이므로, T3을 구매하지 않은 무료 계층 qub은 봉인이 반환되는 즉시 완전히 내구적입니다. 거칠어지는 것은 증명 가능한 상한 약속 시각입니다: 무료 qub의 경우 이는 qub당 트랜잭션 블록 시간이 아닌 앵커 블록 시간이 됩니다. 낮은 볼륨에서 — 현실적인 초기 출시 및 비성수기 상태 — 전체 일일 주기는 드문 엣지 케이스가 아니라 전형적인 하한입니다. 따라서 제품 프레이밍은 약정된 수치 지연 시간이 없는 상한 — "지금 봉인되고 내구적이며; 독립적인 공개 타임스탬프는 다음 로그 앵커에서 추가됨 (일반적으로 매일)" — 이고 정확한 시각 약속 증명은 유료 T3 속성으로, 계층 비교 표면 및 약관에서 공개됩니다 (§16.11, §16.15 Q6). 어떠한 시간 한도도 내부 SLO일 뿐이며, 결코 마케팅된 SLA가 아닙니다.

16.2 LogLeaf 구조 (두 개의 약속된 형태)

로그 항목은 LogLeaf이며, §3.1 프로파일 하에서 직접 작성한 정규 CBOR로 인코딩됩니다 (확정 길이, 태그 없음, 부동소수점 없음, 최단 형식 정수, NFC 텍스트, 부재 시 생략되는 선택적 필드, 인코딩된 바이트 길이 오름차순 그 다음 사전식으로 정렬된 키). §3.1의 파싱 → 재인코딩 → 비교 정규 가드는 (디코딩 시에만이 아니라) 해싱 전 인코딩 경로에서 적용되므로, 두 구현이 정수 폭이나 키 순서 차이를 통해 리프 바이트에 대해 불일치할 수 없습니다. 모든 정수는 u8 / u64 / i64입니다; 모든 다이제스트는 32 바이트 바이트 문자열 (bstr[32])입니다. 저장된 Arweave 트랜잭션 id는 base64url 텍스트 문자열이 아니라 bstr[32]로 전달되는 원시 32 바이트 SHA-256 다이제스트입니다 (§3.3과 일치).

리프는 kind 바이트로 선택되는 두 가지 형태를 가집니다. 왜냐하면 기본 업로드 경로에서 Worker는 바이트 맹목적이기 때문입니다: POST /api/v1/uploadqub_idunlock_at만을 신뢰할 수 없는 클라이언트 주장으로 수신합니다 — body_hash, drand_round, created_at, drand_chain_version은 모두 §13 외부 래퍼 안에 봉인되어 있으며, Worker는 그 키를 결코 보유하지 않습니다. 서버 봉인 경로 (POST /api/v1/seal)만이 평문으로부터 body_hash / drand_round를 도출합니다. 따라서 body_hash + drand_round를 전달하는 단일 리프 형태는 실제 qub의 대다수에 대해 운영자가 결코 검증하지 않은 값을 약속하게 됩니다. 이 분할은 약속된 모든 값을 정직하게 유지합니다:

인코딩 길이 타입 존재 여부 의미
seq 4 u64 필수 전역 0 기반 리프 인덱스; 포함 증명이 약속하는 위치.
kind 5 u8 필수 0x01 입증됨 (서버 봉인) 또는 0x02 주장됨 (클라이언트 봉인 / 바이트 맹목 업로드).
ref 4 bstr[32] 필수 리프 참조 id. 입증됨 → 원시 qub_id. 주장됨 → 블라인드된 id SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1).
chash 6 bstr[32] 필수 콘텐츠 주소 SHA3-256(stored_bytes) — Worker가 두 경로 모두에서 항상 정직하게 계산할 수 있는 유일한 콘텐츠 결속.
unlock_at 10 i64 필수 복사됨 (입증됨) 또는 주장됨 (주장됨); 리프에 진입하기 전에 > 0으로 검증됨.
received_at 12 i64 필수 R2-ACK 시점의 Worker 벽시계. 비증거적 (운영자 주장; §16.6). 자기 기술을 위해 존재하며, 결코 증명이 아닙니다. > 0으로 검증됨.
body_hash 10 bstr[32] kind=0x01 0x02에서 생략됨 — Worker는 §13 하에서 이를 갖지 못합니다.
drand_round 12 u64 kind=0x01 0x02에서 생략됨.

kind=0x02 리프는 의도적으로 body_hashdrand_round도 약속하지 않습니다: 이는 콘텐츠 주소 chash에 있는 불투명한 암호문의 약속과 순서를 입증하며, qub_idunlock_at을 주장합니다 — 그 평문이나 라운드가 아닙니다. 주장된 qub에 대한 평문/라운드 레그는 로그가 아닌 기존 §11 .qub 번들 검증에서 옵니다 (§16.11). drand_chain_version은 리프에 없습니다 (기본 경로에서는 래퍼 안에 있습니다); 체인 단위는 앵커에 있습니다 (§16.7). 인코더 규율: 모두 0인 ref 또는 chash를 거부하고, 음수가 아닌 unlock_at / received_at을 거부하며, cbor.rsoutcome_at > 0 센티넬 가드를 반영합니다.

16.2.1 비공개 qub 블라인딩

로그는 §13 외부 래퍼가 방지하기 위해 존재하는 열거 오라클이 되어서는 안 됩니다 (§13.1). 비공개 (래핑된) qub의 경우 asserted 리프는 블라인드된 식별자 SHA3-256(qub_id ‖ log_blind_secret)을 약속하며, 여기서 log_blind_secret은 서버가 보유한 비밀이고, body_hash를 생략합니다. 제3자는 그러한 리프를 특정 qub_id에 결속할 수 없습니다; 전달 URL을 가지고 있어 따라서 qub_id를 가진 qub의 보유자는 블라인드를 재계산하여 자신의 포함을 확인할 수 있습니다. 공개 qub (이미 열거 가능하며, §13.8에 따라 이미 Visibility: public Arweave 태그를 담고 있음)은 원시 qub_id를 약속합니다. 이는 독립 검증 가능성이 의도적으로 부담을 지는 프라이버시 불변량에 양보하는 유일한 지점입니다; 비공개 qub에 대한 독립 결속은 chash입니다 (§16.9).

log_blind_secret 보관 (해결됨 — §16.15 Q4). 블라인드는 평문 기밀성이 아니라 리프 비연결성을 보호합니다 (§13 래퍼가 그것을 독립적으로 보유함). log_blind_secret이 손상되면, 적대자가 이미 보유하거나 재구성할 수 있는 어떠한 qub_id (번들/URL을 가진 모든 qub과, 모든 저엔트로피 또는 공개 qub_id)에 대해서도 적대자는 한 번의 해시로 리프 ref를 재계산하여 연결합니다 — 이는 알려지지 않은 공간에 대한 무차별 대입이 아니라 알려진 모집단의 직접 연결입니다. log_blind_secret을 다른 서버 비밀과 동일한 보관 계층의 상관/시빌 등급 비밀로 분류하고, 정방향으로만 회전하십시오 (회전은 미래 리프를 다시 블라인드하며; 이미 앵커링된 리프를 소급적으로 비연결할 수는 없습니다).

16.3 리프 및 노드 해싱

SHA-256을 SHA3-256으로 대체한 RFC 6962 §2.1 도메인 분리 해싱:

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 좌측-완전 불균형 트리이며 (각 내부 분할은 서브트리 리프 개수보다 엄격하게 작은 2의 최대 거듭제곱에서 일어남), 이는 포함 증명과 일관성 증명이 하나의 감사 경로 알고리즘을 공유하도록 합니다. 참조 명세는 명시적인 좌/우 도출 의사 코드를 담고 있으며, 2의 거듭제곱이 아닌 (5-리프) 테스트 벡터를 고정하여 4-리프 벡터가 숨기는 우측 가장자리 승격 사례가 실행되도록 합니다.

16.4 해시 체이닝 (내부)

LogDO는 크래시 일관성만을 위한 내부 항목 체인을 유지합니다. 이는 결코 게시되지 않으며 검증자에게 노출되지 않습니다:

entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1]  = SHA3-256("QUB_TLOG_GENESIS_V1")

게시된 추가 전용 권위는 운영자가 우연히 리프를 제공하는 원시 순서가 아니라 누적 Merkle 루트 + 그 앵커 (§16.5–16.6)입니다: 체인은 제공된 어떠한 순서에 대해서도 재계산되므로, 오직 앵커링된 루트만이 정규 위치를 고정합니다.

16.5 누적 Merkle 트리 및 일괄 처리

모든 리프를 seq 순서로 담는 끊임없이 성장하는 하나의 RFC 6962 트리가 있습니다 — 격리된 배치별 트리가 아닙니다. (캐리-리프-체인 배치별 구성은 거부되었습니다: 이는 진정한 접두 관계가 아니므로 그 "일관성 증명"은 건전하지 않습니다.) 누적 트리는 진정한 RFC 9162 일관성 증명을 제공하며, 단일 최근 앵커가 더 오래된 어떠한 qub에 대해서도 포함을 증명하도록 합니다.

LogDO Durable Object는 단일 작성자 (blockConcurrencyWhile, QuotaDO / EntitlementDO를 반영)입니다 — 공유 로그에 추가하는 것은 공유 상태에 대한 읽기-수정-쓰기이므로 KV가 아닌 DO를 반드시 거쳐야 합니다. 이는 트리의 우측 가장자리 프런티어 (O(log n) 해시)를 캐시하므로 배치 마감은 O(batch)입니다. 배치는 함께 앵커링되는 리프 집합입니다; 그 트리거는 구성 가능하며 프로토콜에 고정되지 않습니다: 최소 LOG_BATCH_MAX_LEAVES (기본 4096)의 tree_size 전진, 또는 앵커 주기에 도달하는 경과 시간, 또는 유료 T3 봉인이 착지할 때의 강제 플러시. root_i는 리프 0 .. tree_size_i에 대한 누적 Merkle Tree Hash입니다.

16.6 Arweave 앵커를 통한 서명된 트리 헤드

Arweave 앵커 트랜잭션은 서명된 트리 헤드 그 자체이며 트리 헤드 자체에 대한 운영자 서명을 대체합니다: 일일 앵커는 Arweave tx owner가 곧 서명이므로 qub 키가 필요 없습니다. 모트 명제가 성립합니다 — qub이 보유한 비밀이 아니라 불변의 기질이 앵커링된 루트에 대해 부담을 집니다.

설계에는 정확히 하나의 웜 qub 서명 키가 있으며, 이는 고정됩니다: 봉인당 영수증 키 (§16.10). 그 공개 키는 LogProfile (검증자와 함께 배포됨)에 약속되고 또한 anchor_owner에 의해 교차 서명되므로, 검증자는 앵커와 동일한 고정된 루트에 대해 영수증을 검증합니다. 이는 §16.15 Q2의 해결입니다 — 고정되지 않은, 운영자가 회전 가능한 영수증 키는 부인 가능할 것이며 (운영자가 그 키가 자신의 것임을 부정할 수 있음), 이는 영수증이 억제하기 위해 존재하는 운영자 수준 적대자에 대한 영수증의 책임 가치를 무효화할 것입니다. 따라서: qub은 고정되지 않은 로그 서명 키를 보유하지 않습니다; 영수증 키는 고정되고 anchor_owner에 의해 교차 서명됩니다.

SignedTreeHead는 정규 CBOR입니다 (인코딩 길이순 키): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (이전 sth_hash; 제네시스 = 32개의 0 바이트), 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 (그리고 영수증 키 공개 키)는 DrandTimelockProvider::quicknet()에 이미 있는 quicknet 상수와 함께 LogProfile로서 qub_core에 베이크되어 검증자 바이너리와 함께 배포됩니다. 검증자는 또한 게이트웨이 /raw/ 응답을 신뢰하기보다 Arweave tx 데이터 → tx_id 결속을 로컬로 반드시 검증해야 합니다. 이는 불량 지갑 이중성 구멍을 닫습니다: "Arweave에 앵커링됨"은 검증자가 어떤 지갑인지 고정하기 전까지는 무의미합니다.

*회전은 §15 거버넌스 확장이며, 재사용이 아닙니다 (해결됨 — §16.15 Q3).* §15.2의 프로파일 표면은 현재 sig_algs / drand 체인 / 래퍼 버전 / 콘텐츠 타입만을 열거하며, §15.3의 트리거는 이들 중 어느 것도 나열하지 않습니다 — LogProfile / anchor_owner는 아직 §15의 표면에 없습니다. 따라서 회전 거버넌스는 반드시 구축되어야 합니다: §15.3이 확장되어 (아래) LogProfile 트리거를 추가하며, 회전은 검증자 업데이트로 출시되는 서명된 LogProfile 범프입니다. 계획된 회전은 발신 → 수신 교차 서명을 담습니다; 손상 기반 회전은 그럴 수 없으며 (바로 그 시점에 발신 키는 신뢰 불가/가용 불가임) §15가 거버넌스하는 범프로 폴백하고, 이전-앵커 포크 검사 (아래)가 그 사이의 피해를 한정합니다.

이중성 윈도우 (1급 신뢰 매개변수). 리프는 그것을 덮는 앵커가 Arweave에서 확인된 후에야 이중성 저항적입니다. 윈도우는 received_at → 앵커 확인 (≤ 주기 + Arweave 최종성)입니다. 그 안에서 유일한 보장은 고정된 봉인 영수증 (§16.10)과 qub의 운영 무결성입니다. 세 가지 책임 아티팩트가 이를 손짓이 아닌 정직한 것으로 만듭니다 (증인 모델은 §16.15 Q2의 해결입니다):

  1. 고정된 서명 봉인 영수증 — 업로드 응답에서 반환되는 SCT 유사물 (§16.10)로, 고정된 anchor_owner 교차 서명 영수증 키로 서명됨. 앵커 전에 폐기된 리프는 피해자에게 게시할 수 있는 부인 불가능한 영수증을 남기며, 침묵하는 누락 구멍을 닫습니다.
  2. 게시된 모니터 방법론 + 이전-체인 워크 — 앵커 prev 체인은 헤드→제네시스로 워크됩니다; 포크 (하나의 size에서 서로 다른 root를 가진 두 앵커, 또는 깨진 prev)는 부정행위의 게시 가능한 증거입니다. 이중성 탐지는 침묵하는 가정이 아니라 명시된 운영 약속입니다.
  3. 이중 자가 게시 헤드 — 각 새 헤드 {sth_hash, tree_size}는 전용 qub 소유 공개 추가 전용 GitHub 저장소 (부담을 지는 변조 탐지 자가 게시 레그)에 게시되며, 소셜 게시는 최선 노력 보강일 뿐입니다. 게시 실패는 (침묵 실패가 아니라) 반드시 호출(page)해야 합니다.

정직성 한도 (결속적 제약). qub이 두 게시 표면을 모두 통제하므로, 이는 독립적으로 증언된 것이 아니라 자가 게시된 것입니다. 어떠한 제품, 마케팅, 법적 표면도 로그가 "독립적으로 증언됨"이라고 주장할 수 없습니다; 허용되는 주장은 이중성이 탐지 가능하며 부인 불가능한 영수증을 남긴다는 것입니다. 진정한 독립 제3자 증인은 향후 §15 거버넌스 범프로 연기됩니다.

received_at은 운영자 주장이며 어떠한 주장도 그것에 기댈 수 없습니다 — 이는 어떠한 제품 / 법적 / API / 증명 렌더링 표면에서도 결코 증명이나 분쟁 보강으로 표면화되지 않습니다. Arweave 앵커 블록 시간 T가 유일한 무신뢰 타임스탬프입니다 ("로깅됨" 시점에 대한 상한). received_at에 대한 어떠한 모니터 정상성 검사도 운영자가 통제하는 anchored_at STH 필드가 아니라 T에 대해 반드시 비교해야 합니다; 그러한 검사는 정직한 운영자의 시계 버그에 대한 가드일 뿐이며, 악의적 운영자에 대한 책임 통제가 아닙니다 (§16.15 Q5).

16.7 앵커 트랜잭션 형식 및 주기

AnchorBundle은 §16.8 번들러를 통해 기록되는 정규 CBOR Arweave 트랜잭션 본문입니다: ver:u8, sth:bstr (정규 SignedTreeHead 바이트), prev_anchor:bstr (이전 앵커 tx id 원시 바이트; 제네시스에서는 생략), chain_hash:tstr (효력 중인 drand 체인 — quicknet), 그리고 앵커가 자기 완결적이도록 배치의 리프-CBOR 스트림을 seq 순서로: 모니터는 qub 의존성 없이 본문으로부터 root를 재도출합니다. (리프 스트림이 고볼륨에서 커지면, 향후 개정판은 참조로 리프 범위만 약속할 수 있습니다; 기록되었으나 v1에서는 채택되지 않음.)

Arweave 태그는 의도적으로 열거 가능합니다 — 로그는 비공개 qub과 달리 발견되도록 의도되었습니다: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. 태그는 신뢰할 수 없는 힌트입니다; CBOR 본문이 유일한 권위입니다.

주기: 기본적으로 매일이며, 볼륨에 따라 재검토됩니다 (크기 트리거는 부하 하에서 유효 주기를 자동으로 단축합니다); 유료 T3 봉인은 앵커를 강제하므로 유료 고객은 결코 하루를 기다리지 않습니다. 앵커 지갑은 전용이며 저속이고, 업로드 지갑과 분리됩니다 — 이는 업로드 지갑 손상이 앵커를 위조할 수 없도록 반드시 자체 JWK (업로드 지갑상의 논리적 역할이 아니라 별개의 키)여야 합니다 — 하루당 앵커 트랜잭션의 엄격한 예산 (강등된 ARWEAVE_DAILY_CEILING)을 가집니다. 보관 자세는 명백하게 명시됩니다: "콜드"가 아니라 엄격한 회로 차단기와 낮은 잔액을 가진 좁은 범위의 핫 키 — 매일 자동 서명하는 지갑은 콜드일 수 없으며, 명세는 그런 척하지 않습니다.

16.8 ANS-104 번들러

자체 제작 ANS-104 DataItem 인코더 및 deep-hash 서명자, 대략 300줄, Web Crypto만, npm 의존성 제로 (두 Turbo SDK 모두 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입니다 — ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data]에 대한 재귀적 SHA-384 다이제스트 (Arweave의 와이어 요구사항, crypto.subtle.digest("SHA-384")) — 그 다음 crypto.subtle를 통해 지갑 JWK로 deep hash에 대한 RSA-PSS; id = base64url(SHA-256(signature)). 여기서의 SHA-384는 Arweave-와이어-전용 기본 요소로 격리되며, 결코 qub 신뢰 기본 요소가 아닙니다 (§15가 그 펜스를 기록함; qub 신뢰 해싱은 전반에 걸쳐 SHA3-256임).

하나의 코드 경로가 세 소비자를 섬깁니다: 유료 T3 qub당 영속성, Arweave 가용 불가 폴백 (DataItem을 큐에 넣고, 어쨌든 R2 우선 ACK를 반환 — 이는 현재의 ARWEAVE_UNAVAILABLE 503 막다른 길을 닫음), 그리고 AnchorBundle 기록. 서명 방식 (해결됨 — §16.15 Q8): v1은 RSA-PSS (서명 타입 1)로 서명하며 기존 Arweave 지갑 JWK 메커니즘을 재사용합니다 (새로운 장기 키 보관 제로, "비밀 하나 줄이기" 명제에 기여); Ed25519는 §15 PQ 마이그레이션 경로로 연기됩니다.

손으로 만든 deep hash는 W5에서 가장 위험이 높고 자연적 커버리지가 가장 낮은 코드이므로, 그 게이팅은 타협 불가입니다 (§16.15 Q8):

  1. 교차 언어 픽스처 tlog_v1.json (Rust + TS, §14.5 wrapper_v1.json 패턴)은 deep-hash, DataItem 바이트 + id, 리프 해시, 5-리프 루트 + 감사 경로, STH 해시, 포함 증명, 일관성 증명을 — 서명 방향과 검증 방향 모두에서 다룹니다 (검증 방향이 중요한 이유는 §16.6의 로컬 tx → tx_id 검사가 deep hash를 작성자뿐 아니라 모든 독립 검증자로 끌어들이기 때문입니다).
  2. 참조 ANS-104 번들러를 통한 일회성 상호 운용 라운드 트립, 정적 테스트 데이터로만 소비 — 결코 npm 런타임 의존성이 아님 (Web-Crypto-전용 / 설치 스크립트 없음 자세가 유지됨).
  3. deep-hash + RSA-PSS 경로는 프로덕션이 사용하는 동일한 crypto.subtle 기본 요소를 통해 라운드 트립해야 하므로, 자체 제작 인코더는 바이트 호환됩니다.
  4. 지속적인 번들 후 수용 모니터가 각 앵커 / 폴백 DataItem이 실제로 Arweave 수용을 달성하는지를 알람 + 회로 차단기와 함께 확인합니다 — 왜냐하면 deep hash는 또한 Arweave 가용 불가 폴백 큐를 섬기므로, 침묵하는 퇴행은 바로 그것이 커버하기 위해 존재하는 장애 동안 그 큐를 네트워크-거부된 항목으로 채울 것이기 때문입니다.

16.9 포함 및 일관성 증명

둘 다 RFC 9162, SHA3-256이며, 정규 CBOR로 제공됩니다.

InclusionProofGET /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 }.

ConsistencyProofGET /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)로 키가 매겨진 영속적 Merkle 노드 저장소인 경우에 정확성-중립적입니다. 좌표 키 저장소에서는, 어떠한 (leaf i, size N) 감사 경로도 배치 경계를 넘는 재계산 없이 O(log N)개의 직접 R2 GET의 집합입니다; 배치 키 저장소에서는 그렇지 않으며, 이것이 이 해결이 닫는 저장소 레이아웃 격차입니다. 리프 본문도 마찬가지로 seq에 의해 콘텐츠 주소 지정 가능합니다. W5 테스트 벡터는 LogDO 저장소가 지워진 상태에서 R2 + Arweave만 사용하여 훨씬 나중의 루트에 대해 제네시스 시대의 콜드 리프를 반드시 증명해야 하며, 그래서 §16.13의 회수 안전성 주장이 단언에 그치지 않고 근거로 뒷받침됩니다. O(log N)개의 순차적 R2 GET은 비동기 증명 엔드포인트에 속하며 — 결코 봉인 핫 경로 (§16.10)나 틱별 크론에 속하지 않습니다.

16.10 R2 우선 ACK 순서

POST /api/v1/upload 시퀀스는 다음과 같이 됩니다:

  1. 전반부 게이트 (인증, 검증, 멱등성 샤드 키) — 변경 없음.
  2. 동기적으로 await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes) — 내구성 하한; W1의 프리캐시 경쟁 (이전에는 Arweave 제출 후의 ctx.waitUntil)도 닫음.
  3. 동기적으로 await LogDO.append(leaf) — 하나의 인-콜로 DO RPC; 단일 작성자가 seq를 할당하고, 항목 체인을 확장하며, 프런티어를 갱신함. (공유 상태에 대한 RMW → DO, KV 아님.) append RPC는 오직 그것만 수행함 — O(batch) Merkle 배치 마감 작업은 LogDO 알람에서 이 RPC 밖에서 실행되며, 그렇지 않으면 append p95가 LOG_BATCH_MAX_LEAVES번째 봉인마다 치솟음.
  4. 지금 ACK 반환 — 봉인 영수증 (고정된 영수증 키로 서명됨, §16.6)과 { tx_id, log_seq, anchor_status: "pending" }을 포함. 다중 초의 Arweave 팬아웃은 임계 경로에서 제거됨.
  5. 하나의 ctx.waitUntil이 지연 작업을 큐에 넣음: qub당 Arweave 제출 (이제 최선 노력 / 유료; 실패 시 사용자에게 503을 반환하기보다 번들러 폴백 큐로 라우팅) 그리고 기존 잠정 메타 기록. 배치 마감과 앵커링은 LogDO 알람과 일일 앵커 크론에서 독립적으로 실행됨. 루프 안의 ctx.waitUntil 없음; 기존 멱등성 샤드 키는 보존됨.

지연 시간 예산 (해결됨 — §16.15 Q7). < 300 ms p95 목표는 가정이 아니라 측정된 출시 게이트입니다. 정직한 임계 경로는 전반부 KV 읽기 + 하나의 R2 PUT + 두 개의 직렬화된 Durable Object — 기존 QuotaDO 봉인 할당량 차감 그리고 새로운 LogDO append — 이므로, 예산은 하나가 아니라 두 개의 인-콜로 DO 라운드 트립을 반드시 고려해야 합니다. QuotaDO의 것을 반영하는 LogDO 지연 시간 알람을 출시하고, p95 퇴행을 릴리스 차단기로 다루십시오.

16.11 신뢰 모델 — 리프 종류로 범위가 정해진 정확한 주장

kind=0x01 (입증됨)의 경우: "이 콘텐츠 — body_hash와 일치하는 본문, qub_id로 식별됨 — 는 위치 seq에서 qub의 추가 전용 로그에 약속되었으며 Arweave 블록 시간 T보다 늦지 않게 존재했습니다; 이는 drand 라운드 R = unlock_round(unlock_at)까지 암호학적으로 읽을 수 없었습니다." 이것은 완전한 {tlock 라운드 결속 + Merkle 포함 + 앵커링된 루트} 삼중입니다.

kind=0x02 (주장됨, 기본): "콘텐츠 주소 chash를 가지며 qub_idunlock_at을 주장하는 불투명한 암호문이 위치 seq에서 추가 전용 로그에 약속되었으며 Arweave 블록 시간 T보다 늦지 않게 존재했습니다." 라운드와 본문 레그는 로그가 아니라 기존 §11 .qub 번들 검증 (qub_core::unlock)이 제공합니다; 로그가 단순한 qub당 트랜잭션 위에 추가하는 것은 변조 탐지가 가능한 순서, 무신뢰 상한 약속 시각, 그리고 이중성 저항입니다.

두 주장 모두 §11에 따라 다음을 제외합니다: sig_alg ≥ 0x01 없는 작성권, 의도, 그리고 앵커 단위 미만의 타이밍. 어느 쪽도 어떠한 주장이 received_at에 기대도록 허용하지 않습니다.

주장 천장 (결속적 출시 제약 — 해결됨 §16.15 Q1). 무료 / 기본 (kind=0x02) qub의 경우, 위의 범위가 정해진 kind=0x02 주장이 어떠한 제품, 마케팅, 약관, 또는 증명 렌더링 표면이 주장할 수 있는 것의 천장입니다. 어떠한 표면도 로그가 기본 qub의 콘텐츠나 잠금 해제 라운드를 증명한다고 진술하거나 암시할 수 없습니다 — 로그는 불투명한 암호문의 순서 + 무신뢰 상한 약속 시각을 증명합니다. 콘텐츠와 라운드 증명은 로그와 독립적인 기존 §11 .qub 번들 검증에서만 옵니다. 이는 스타일적 선호가 아니라 카피에 대한 엄격한 출시 차단기입니다; 이것이 바이트 맹목 기본 경로를 정직하게 유지하는 해결입니다.

16.12 버전 관리 및 W3 조율

SealedQub 와이어 범프가 없으며 따라서 프로토콜 버전 범프가 없습니다 (§12.1): 로그는 기존 필드와 바이트에 약속하는 사이드카이므로 §12.2 프로토콜 버전 히스토리에 진입하지 않습니다. W3의 선택적 drand_chain_version은 손대지 않으며 유일한 선택적 SealedQub 필드로 남습니다. 로그는 대신 자체적인 독립 버전 공간을 도입합니다 — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — §12.4 래퍼 버전 독립성을 반영합니다 (래퍼는 프로토콜 버전과 독립적인 버전 바이트를 담으며, 로그 버전은 동일한 분리를 따릅니다).

증명 전달은 기본적으로 가져오며, 선택적 동승이 가능합니다. 증명은 봉인 시점에 존재할 수 없으므로 (앵커가 아직 기록되지 않음), 봉인 시점 .qub 번들은 증명이 없는 상태로 유지됩니다. W7의 검증자는 GET …/proof를 한 번 가져오거나, 완전 오프라인 모드에서는 Log-Id에 대한 Arweave 쿼리를 통해 공개 AnchorBundle로부터 증명을 재구성합니다. .qub 번들 (W7)은 선택적 inclusion_proof 멤버를 예약합니다 — 봉인 시점에는 부재하고, 콜드 아카이빙을 위한 앵커 후 재내보내기로 채워짐 — W3의 drand_chain_version과 동일한 "선택적, 기본적으로 생략, 추가적" 패턴을 따릅니다.

16.13 보존

LogDO 오픈 테일, R2 증명 제공 기질, 앵커 회로 차단기 카운터, 번들러 폴백 큐에 대한 보존 윈도우는 docs/DATA-RETENTION.md에 명시되어 있습니다. 원칙: 로그의 핫 항목별 저장소 (LogDO)는 앵커 후 회수 가능합니다; 그 감사 자료 — 좌표 키 기반 (level, index) Merkle 노드 저장소 + seq 주소 리프 본문 (§16.9) + Arweave 앵커 — 는 영구적입니다. DO에서 콜드 리프를 회수하는 것은 발행된 증명을 결코 무효화하지 않습니다. 왜냐하면 증명은 DO가 아니라 그 영구적 R2 노드 저장소와 Arweave 앵커에 대해 해결되기 때문입니다 (그리고 §16.9의 지워진-DO 테스트 벡터가 이를 증명합니다).

16.14 테스트 벡터

W5는 교차 언어 픽스처 tlog_v1.json (§16.8)과 더불어 작업된 벡터를 출시합니다: kind=0x01kind=0x02 리프 → leaf_hash; 5-리프 누적 루트; 하나의 포함 증명; 하나의 일관성 증명; 하나의 AnchorBundle; 그리고 하나의 DataItem id. 이들은 §14.5 외부 래퍼 벡터와 나란히 존재하며 Rust (qub-core)와 TypeScript (Worker) 구현 모두에 의해 실행됩니다.

16.15 검토 결정 (W5 — 해결됨)

W5 외부 검토 (적대적 설계 패스 + 소유자 승인)는 완료되었습니다. 아래의 각 결정은 확정되었으며 위의 §16 본문에 반영되어 있습니다; 결속적 출시 제약은 끝부분에 다시 진술됩니다. 구현은 그 제약 하에서 진행될 수 있습니다.

  1. 기본 경로 (kind=0x02) 리프 정직성 — 해결됨. 명시된 대로 두-리프-종류 분할을 출시: kind=0x02body_hashdrand_round도 약속하지 않음. 바이트 맹목 경로에 *_body_hash 필드 없음 (이는 통합자에게 가장 읽기 쉬운 거짓 "검증됨" 신호일 것이며, §11이 번들에서 이미 제공하는 편의임). 로그 입증 qub에 대해 서버 봉인을 요구하지 않음 (그것은 평문을 Worker로 강제하고 암호 분쇄 모트를 파괴할 것임). 모든 자기 기술 단축 경로는 리프 필드가 아니라 .qub 번들 / 증명 봉투의 검증자 재계산 필드에 속함. 소유자 확인된 주장 천장: §16.11.
  2. 이중성 / 누락 책임 — 해결됨. 봉인 영수증 키는 LogProfile에 고정되고 anchor_owner에 의해 교차 서명됨 (이전의 "서명 키 없음" 모순을 닫음; §16.6). 출시 증인 모델: 고정된 영수증 + 모니터 방법론 + 이전-체인 워크 + 이중 자가 게시 헤드 (qub 소유 공개 GitHub 저장소, 소셜 최선 노력), 탐지 가능 + 영수증 발행됨으로 마케팅되며 결코 독립적으로 증언됨이 아님. 진정한 제3자 증인은 §15 거버넌스 범프로 연기됨.
  3. 고정된 앵커 소유자 신뢰 루트 + 회전 — 해결됨. LogProfile 고정 채택 (§16.6); 검증자는 anchor_tx.owner == anchor_owner를 검사하고 tx 데이터 → tx_id 결속을 로컬로 검증함. 회전 거버넌스는 재사용이 아니라 §15 구축할 확장 (§15.3 트리거 추가)임; 계획된 회전은 교차 서명하고, 손상 기반 회전은 포크 검사가 피해를 한정하는 가운데 §15 범프로 폴백함.
  4. 비공개 qub 리프 블라인딩 — 해결됨. 비공개 qub에 대해 블라인딩 유지 (ref = SHA3-256(qub_id ‖ log_blind_secret)), 공개 qub에 대해 원시 qub_id (이미 §16.2.1), 독립 결속으로서 chash. log_blind_secret은 상관/시빌 등급 비밀이며, 정방향으로만 회전 (§16.2.1).
  5. received_at — 해결됨. 리프에 유지하되, 약속되지만 명시적으로 비증거적; 어떠한 표면에서도 결코 증명이나 분쟁 보강으로 표면화되지 않음. 어떠한 모니터 정상성 검사도 운영자가 통제하는 anchored_at가 아니라 Arweave 블록 시간 T에 대해 비교함 (§16.6).
  6. 무료 계층 증명 가능 타이밍 — 해결됨 (소유자 승인). 내구성은 퇴행하지 않음; 증명 가능한 상한 약속 시각만이 앵커 블록 시간으로 거칠어짐. 무료 계층 카피는 수치 SLA를 사용하지 않음 ("…다음 로그 앵커에서 추가됨, 일반적으로 매일"); 정확한 시각 증명은 유료 T3 속성으로, 계층 비교 표면 + 약관에서 공개됨 (§16.1).
  7. Workers상의 누적 트리 — 해결됨. 단일 누적 RFC 9162 트리 + 프런티어 캐시 단일 작성자 LogDO (~1k writes/sec DO 천장 대비 여유로운 헤드룸; Merkle-of-shard-roots 샤딩은 그것에 근접할 때까지 연기). 차단 선결 조건: 좌표 키 기반 (level, index) R2 노드 저장소 + 지워진-DO 콜드 리프 테스트 벡터 (§16.9); < 300 ms는 두 직렬화된 DO에 대한 측정된 출시 게이트임 (§16.10).
  8. ANS-104 서명 방식 + deep-hash — 해결됨. RSA-PSS (sig 타입 1, 전용 앵커 지갑 JWK 재사용); Ed25519는 §15 PQ 경로로 연기. 손으로 만든 SHA-384 deep hash는 양방향 교차 구현 픽스처, 정적 전용 참조 번들러 상호 운용 검사, 공유 crypto.subtle 라운드 트립, 그리고 번들 후 Arweave 수용 모니터에 게이트됨 (§16.8).

결속적 출시 제약 (구현 + 제품/법무 검토로 가져갈 것):