qub-protokollspecifikation
qub är ett protokoll för kryptografiska temporala åtaganden: ett system för att försegla ord till ett framtida datum och bevisa, när det datumet kommer, exakt vad som sades och när.
Tre primitiver får det att fungera. drand är en decentraliserad slumpbaken — avslöjandedatumet upprätthålls av fysik, inte av någon parts goda vilja. Permanent offentlig lagring är ett manipuleringssäkert offentligt arkiv — ingen part kan redigera eller radera en qub när den har förseglats. ML-DSA-65 är en postkvantsignatur — varje qub är knuten till ett nyckelpar vars hemliga del aldrig lämnar författarens enhet.
Tillsammans gör dessa primitiver ett uttalande som är tidslåst, manipuleringsuppenbart och tillskrivbart — ett kvitto vars värde växer i takt med att världens förmåga att fabricera det förflutna förbättras.
Resten av detta dokument är den normativa specifikationen som krävs för interoperabla implementationer.
qub-protokollspecifikation
| Fält | Värde |
|---|---|
| Version | 1.0 (protokollversion 0x01, yttre omslag version 0x01) |
| Datum | 2026-05-01 |
| Status | Utkast |
| Granskat till | 2026-05-01 |
Detta dokument är den normativa protokollspecifikationen för qub-systemet för tidsbestämda åtaganden. Det definierar datastrukturer, serialiseringsregler, härledningsformler och verifieringsprocedurer som krävs för interoperabla implementationer.
Omfattning: protokollskiktet är avsiktligt språkneutralt — qub-kroppen är ogenomskinlig klartext / markdown / paktbyte, och språkmedveten rendering är läsarens ansvar (qub.social-webbapp, <qub-embed>-iframe, MCP-klienter, etc.).
1. Notation och konventioner
| Notation | Betydelse |
|---|---|
u8, u64, i64 |
Osignerade/signerade heltal med specificerad bitbredd |
[u8; N] |
Bytearray med fast längd om N byte |
Vec<u8> |
Bytearray med variabel längd |
Option<T> |
Värde av typ T, eller frånvarande |
String |
UTF-8-textsträng, NFC-normaliserad |
| ` | |
SHA3-256(x) |
NIST SHA3-256-hash av bytesträngen x (FIPS 202) |
ceil(x) |
Takfunktion: minsta heltal ≥ x |
| CBOR | Concise Binary Object Representation (RFC 8949) |
| big-endian | Mest signifikant byte först |
Alla heltal i pre-image-konstruktioner kodas som big-endian bytearrayer med fast bredd (i64 → 8 byte, u8 → 1 byte) om inte annat anges.
Alla tidsstämplar är Unix-sekunder i UTC.
2. Datastrukturer
2.1 ComposeQub (skaparens tillstånd i minnet)
Serialiseras inte till CBOR. Skrivs inte till permanent lagring. Lokal för skaparappen.
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 (dekrypterad nyttolast)
Serialiseras med kanonisk CBOR (§3). Krypterad inuti SealedQub. Detta är strukturen som bevisar innehållsintegritet efter dekryptering.
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 — när verkligheten avkunnar sin dom (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
}
Baslinje (osignerad text-qub): version = 0x01, content_type = 0x01, sig_alg = 0x00, alla Option-fält frånvarande.
Andra v1-konfigurationer: content_type = 0x03 (paktkropp, se §6.1); sig_alg = 0x01 (ML-DSA-65) med author_signature och author_pubkey närvarande (se §9.3); cosigner_pubkey och cosigner_signature närvarande tillsammans för motundertecknade pakter (se §9.7); reply_to satt till föräldra-qubbens qub_id för svarskedjor (se §9.3 för signaturomfångskonsekvenserna).
2.3 SealedQub (kanoniskt trådformat)
Serialiseras med kanonisk CBOR (§3). Skrivs till permanent lagring. Detta är artefakten på kedjan.
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 — visas på verdict-watch-CTA
// före avslöjande; speglar QubEnvelope.outcome_at;
// bundet till qub_id via §4.1-pre-imagen.
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 (läsarens applikationstillstånd)
Serialiseras inte till CBOR. Lokal för läsarappen. Konstrueras efter framgångsrik dekryptering och verifiering.
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 — bärs vidare från QubEnvelope.outcome_at / SealedQub.outcome_at; driver avslöjandesidans verdiktbevakningsblock (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. Kanonisk CBOR-profil
All serialisering av SealedQub och QubEnvelope MUST följa denna profil. Två implementationer som ges samma logiska struktur MUST producera identiska byte.
3.1 Kodningsregler
| Regel | Specifikation |
|---|---|
| Standard | RFC 8949 §4.2.1 (Core Deterministic Encoding Requirements) |
| Nyckelordning i kartor | Sorterade efter kodad bytelängd först (kortare före längre), sedan lexikografiskt (byte-för-byte för kodningar av samma längd) |
| Heltalskodning | Kortaste form: 0–23 i initial byte; 24–255 i 2 byte; 256–65535 i 3 byte; etc. |
| Längdkodning | Endast bestämda längder. Inga arrayer, kartor, bytestrings eller textstrings av obestämd längd (additional info = 31 är förbjudet). |
| Taggar | Inga CBOR-taggar (major-typ 6 är förbjudet). |
| Flyttal | Inga flyttal (major-typ 7 värden 0xF9–0xFB är förbjudna). |
| Textstrings | UTF-8-kodade, NFC-normaliserade (Unicode Normalization Form C). |
| Bytestrings | Råa byte. Ingen base64-kodning i CBOR-skiktet. |
| Dubblettnycklar | Avvisa med fel. Tolkare MUST NOT tyst acceptera dubblettnycklar i kartor. |
| Okända nycklar | Avvisa med fel. Tolkare MUST NOT tolerera kartnycklar utanför typens kanoniska nyckeluppsättning — två distinkta kanoniska bytesträngar får aldrig avkodas till samma värde (encode(decode(x)) == x), och för signerade nyttolaster vore en extra nyckel dolt innehåll som båda signaturerna åtar sig. Schemautveckling sker genom version, aldrig genom extra nycklar. |
| Enkla värden | Endast true (0xF5), false (0xF4) och null (0xF6) är tillåtna. |
| Frivilliga fält | Frånvarande frivilliga fält utelämnas helt från CBOR-kartan (kodas inte som null). Närvarande frivilliga fält inkluderas i sorterad nyckelordning. |
3.2 Verifierade kanoniska nyckelordningar
Dessa nyckelordningar är normativa. Implementationer MUST avge nycklar i exakt denna ordning. Debug-assertioner SHOULD verifiera ordning i icke-release-byggen.
QubEnvelope (version 0x01, osignerad, alla frivilliga fält frånvarande):
"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)
Härledning av QubEnvelopes nyckelordning: varje nyckel är en CBOR-textsträng. Kodad längd = 1 byte header + stränglängd (för strängar under 24 byte). Sortera först efter total kodad längd, sedan lexikografiskt för nycklar av samma längd.
SealedQub (version 0x01, publik, ingen mottagare):
"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 (paktkropp, 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 (rad i terms-arrayen):
"key" (4 encoded bytes)
"value" (6 encoded bytes)
PartyIdentifier (party_a / party_b-karta):
"label" (6 encoded bytes)
"contact" (8 encoded bytes) ← only if present
3.3 Referens för bytekodning
| Typ | CBOR-kodning | Exempel |
|---|---|---|
| SHA3-256-hash (32 byte) | 0x58 0x20 + 32 byte |
body_hash, qub_id |
| Tidsstämplar (i64) | Major-typ 0 (positiv) eller 1 (negativ), kortaste kodning | Unix-sekunder |
| Version (u8, värde 1) | 0x01 (enskild byte) |
|
| Innehållstyp (u8, värde 1) | 0x01 (enskild byte) |
|
| sig_alg (u8, värde 0) | 0x00 (enskild byte) |
|
| ML-DSA-65-signatur (3 309 byte) | 0x59 0x0C 0xED + 3 309 byte |
author_signature, cosigner_signature |
| ML-DSA-65 publik nyckel (1 952 byte) | 0x59 0x07 0xA0 + 1 952 byte |
author_pubkey, cosigner_pubkey |
4. Normativa härledningar
4.1 qub_id
qub_id identifierar unikt en qub och binder QubEnvelope till SealedQub. Den härleds deterministiskt från envelopinnehåll.
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
Kodning av domänseparator: Strängen "QUB_ID_V2" är 9 ASCII-byte. En enskild 0x00-utfyllnadsbyte läggs till för att nå 10 byte för justering. Implementationer MUST använda exakt dessa 10 byte: [0x51, 0x55, 0x42, 0x5F, 0x49, 0x44, 0x5F, 0x56, 0x32, 0x00].
Kodning av outcome_at: V1.1 utvidgade pre-imagen från 92 till 100 byte för att vika in det frivilliga outcome_at-fältet i bindningen. Frånvarande outcome_at kodas som 8 nollbyte; protokollvalidatorerna avvisar outcome_at <= 0 överallt så att detta sentinelvärde inte kan kollidera med ett legitimt värde. Se §3.2 (trådformat) och den interna tasks/verdict-uplift-plan.md för verdiktmekaniken som motiverar detta fält.
Kodning av drand_round: V1.2 utvidgade pre-imagen från 100 till 108 byte för att vika in drand_round (mål-drand-rundan, §4.3) i bindningen, och höjde domänseparatorn till QUB_ID_V2. Detta binder tidslåsrundan in i qub-identiteten: en gateway kan inte binda om chiffertexten till en annan runda (t.ex. en redan passerad) än den som det visade unlock_at antyder. Upplåsningsproceduren (§8) verifierar dessutom att rundan som är inbakad i tlock-chiffertextens stanza matchar unlock_round(unlock_at), så att den visade upplåsningstiden bevisligen är den runda som styr dekrypteringen.
Egenskaper:
- En ändring i något fält i QubEnvelope (body, tidsstämplar, innehållstyp, version) producerar ett annat qub_id.
- qub_id beräknas före kryptering. Både QubEnvelope och SealedQub bär samma qub_id. Läsaren verifierar att de matchar efter dekryptering.
- qub_id beror inte på
sender_label,author_signatureellerauthor_pubkey. Detta innebär att samma innehåll förseglat vid samma tid producerar samma qub_id oavsett vem som signerar det. - Ändring av SealedQubs
title(med allt annat oförändrat) ändrarqub_idviatitle_hash. En gateway kan därför inte byta ut klartexttiteln som visas på nedräkningen utan att ogiltigförklara qub-identiteten. - Ändring av SealedQubs
outcome_at(med allt annat oförändrat) ändrarqub_idvia pre-imagen. En gateway kan inte byta ut det verdict-on-datum som visas på nedräkningen före avslöjande utan att ogiltigförklara qub-identiteten. - Ändring av
drand_round(med allt annat oförändrat) ändrarqub_idvia pre-imagen. En gateway kan inte binda om tidslås-chiffertexten till en annan runda utan att ogiltigförklara qub-identiteten; i kombination med stanza-runda-kontrollen vid upplåsning i §8 är det visadeunlock_atden runda som faktiskt styr dekrypteringen.
4.2 body_hash
body_hash = SHA3-256(body)
Där body är den råa Vec<u8>-innehållsnyttolasten. För text-qubs är detta den UTF-8-kodade qub-kroppen.
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
Där title är den frivilliga klartexttiteln som visas på läsarens nedräkning före avslöjande (se §3.2). NFC-normalisering körs vid hashtid så att sammanfattningen är stabil över visuellt likvärdiga kodpunktssekvenser. Sentinelvärdet med alla nollor är reserverat för det frånvarande fallet; en tom sträng avvisas vid den kanoniska CBOR-gränsen som en icke-kanonisk kodning av "frånvarande" (den kanoniska kodningen utelämnar fältet helt).
4.3 Mappning av upplåsningsrunda
drand_round = ceil((unlock_at - chain_genesis_time) / chain_period_seconds)
| Parameter | Källa | Exempel |
|---|---|---|
unlock_at |
Användarvald Unix-sekund UTC | 1735689600 (2025-01-01 00:00:00 UTC) |
chain_genesis_time |
drand chain info (genesis_time) |
1595431050 |
chain_period_seconds |
drand chain info (period) |
30 |
ceil()-operationen väljer första drand-rundan vars avslöjandetid är ≥ unlock_at. Detta säkerställer att qubben inte blir dekrypterbar före den valda upplåsningstiden.
Gränsfall: om (unlock_at - chain_genesis_time) är exakt jämnt delbart med chain_period_seconds är resultatet exakt den rundan — qubben låses upp precis vid den rundans avslöjandetid.
Validering: unlock_at MUST ligga i framtiden vid förseglingstid. unlock_at MUST NOT vara mer än 10 år från created_at (för att begränsa risken av långhorisont-drand-beroende; gränssnittet SHOULD varna för upplåsningsdatum bortom 2 år).
5. Newtypes för trådformat
Newtypes för trådformat ger kompileringstidssäkerhet mot att förväxla CBOR-byte med JSON, rå klartext eller andra bytekodningar.
| Typ | Innehåller | Producerad av | Konsumerad av |
|---|---|---|---|
SealedQubCbor |
Kanonisk CBOR av SealedQub | serialize_sealed_qub() |
Uppladdning till permanent lagring, läsarhämtning |
QubEnvelopeCbor |
Kanonisk CBOR av QubEnvelope | serialize_qub_envelope() |
tlock-krypteringsinmatning, tlock-dekrypteringsutmatning |
5.1 Konstruktionsregler
// 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 Validering vid konstruktion
from_encoded() SHOULD validera att indata börjar med ett giltigt CBOR-kartheader. Fullständig strukturell validering sker vid tolkningstid, inte konstruktionstid, för att undvika dubbeltolkning.
6. Register för innehållstyper
| Värde | Typ | Max kroppsstorlek | Anmärkningar |
|---|---|---|---|
0x00 |
Reserverad (ogiltig) | — | MUST NOT användas |
0x01 |
Klartext (UTF-8, begränsad Markdown) | 50 KB betald / 10 KB gratis | Se §10 för renderingsregler. Uppdelningen gratis / betald upprätthålls av uppladdningstjänsten; protokollskiktets hårda tak är 50 KB. |
0x02 |
Reserverad (framtida) | — | Tilldelad för en framtida innehållstyp; ogiltig i v1. Läsare MUST avvisa enligt regeln nedan. |
0x03 |
Pakt (bilateralt avtal, CBOR-kropp) | 100 KB | Kroppen är kanonisk CBOR PactTerms (§6.1). Motundertecknarsignering enligt §9.7. |
0x04 |
Verdikt (skaparens självbetygsättning, CBOR-kropp) | 8 KB | Kroppen är kanonisk CBOR VerdictBody (§6.2). Avges endast av den systemsidiga verdict-intentionen. Föräldrarelationen ligger på Parent-Tx-Id-Arweave-taggen, inte på kroppen. Se verdict-uplift-plan §3.4. |
Läsare MUST avvisa okända innehållstyper med ett tydligt användarsynligt felmeddelande. Läsare MUST NOT försöka rendera okända typer som text.
6.1 Paktkropp (content_type = 0x03)
En paktkropp är den kanoniska CBOR-kodningen av ett PactTerms-värde:
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)> }
Kanoniska CBOR-nyckelordningar för alla tre kartorna anges i §3.2. Totalt serialiserad pakt-CBOR MUST NOT överskrida 100 KB (matchar §6).
Schemadiskriminator. Den första raden i terms för en structured/v1-pakt MUST vara { key: "pact_schema", value: "structured/v1" }. Rader utan denna markör är "custom"-pakter och får ingen strukturerad validering eller schemamedveten rendering.
Frusna bekräftelseplatser. structured/v1-pakter bär exakt fyra bekräftelserader under dessa nycklar:
"initiator_standard_terms"
"initiator_capacity_terms"
"counterparty_standard_terms"
"counterparty_capacity_terms"
value för var och en är en av åtta frusna engelska strängar valda av paret (role, kind), där role ∈ { seller, buyer, provider, client } och kind ∈ { standard, capacity }. Strängarna själva är normativa protokolldata — båda parternas ML-DSA-65-signaturer förbinder sig till de exakta bytena via body_hash. De är INTE lokaliserade; den signerade kroppen är språkneutral. Varje formuleringsändring kräver en ny schemaversion (structured/v2).
De åtta strängarna, deras uppslagning (acknowledgement_for(role, kind)) och motiveringen för var och en är fastställda av referensimplementationen. Konforma implementationer MUST avge byte-identiska bekräftelsevärden; golden-fixture SHA3-256 body-hash-tester som täcker alla fyra rollkombinationer fångar all drift.
Visningsordning för läsare. Bekräftelsesträngarna innehåller fraser såsom "described above", som förutsätter att beskrivning / omfattning-raderna renderas före bekräftelserna. Läsare MUST rendera terms-arrayen i CBOR-ordning; omordning bryter prosasemantiken.
Motpartskontakt. När Part B:s contact är en giltig e-postadress skickar qub-uppladdningstjänsten automatiskt en granskning / motundertecknings-inbjudan via e-post vid stage-tid och binder den slutliga motunderskriften till verifiering av samma adress (§9.7). Pakter vars Part B-kontakt är frånvarande kan fortfarande motundertecknas, men endast via en kanal utanför bandet — tjänsten avvisar motunderskriftsförfrågningar som inte kan producera en matchande 15-minuters e-postverifieringsmarkör.
6.2 Verdiktkropp (content_type = 0x04)
En verdiktkropp är den kanoniska CBOR-kodningen av ett VerdictBody-värde:
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
}
Kanonisk CBOR-nyckelordning:
"outcome" (8 encoded bytes)
"reflection" (11 encoded bytes) ← only if present
"evidence_url" (13 encoded bytes) ← only if present
"verdict_version" (16 encoded bytes)
Totalt serialiserad verdikt-CBOR MUST NOT överskrida 8 KB (matchar registerraden ovan).
Utfallsenum. Wire-byten är intentionsneutral; de fyra kategorierna Right / Partial / Wrong / Unfalsifiable täcker varje verdiktbärande intentions utfallsrymd. Per-intentionsetiketter ("Rätt sagt" / "Höll det" / "Levererat" / "Bekräftad" för Right, etc.) är en läsarsidig renderingsfråga som löses mot föräldra-qubbens intention — wire förblir språk- och intentionsneutral. Värden utanför 1..=4 MUST avvisas vid avkodning.
Föräldralänkning. En verdikt-qub bär INTE föräldrareferensen i sin kropp. Föräldra-qubbens Arweave-transaktions-id avges som Parent-Tx-Id-lagringstaggen vid uppladdning (§7 lagringstaggsskikt). Detta håller kroppen som ett självständigt signerat uttalande av självbedömning; granskningskedjan ("rätt om vad?") etableras via Arweave-taggsökningen.
Säkerhet för bevis-URL (normativt). När evidence_url är närvarande MUST validerare (kompositionssidan, wire-sidan, Worker-kanten) upprätthålla:
- Endast HTTPS. Strängen MUST börja med bytesekvensen
https://. Varje annat schema —http,ftp,javascript,data,file, etc. — avvisas. - Längdtak. ≤ 2 048 byte (webbläsarens praktiska URL-gräns).
- NFC + kontroll av fientliga kodpunkter. Samma regel som för
titleochreflection— bidi-override / nollbredd / tag-block / BOM / C0 / C1-kodpunkter avvisas. Definitionen matchar Rust-crate::handle::contains_hostile_text_codepointoch TS-workers/api/src/utils/unicode.ts::isHostileCodepoint(håll i lås). - Inga blanksteg, inga ASCII-styrtecken. Blanksteg / DEL / byte under
0x20var som helst i URL:en avvisas — stänger\n/\t-injektionsvektorn som bidi-regeln inte täcker. - Icke-tomt värdsegment. Allt mellan
https://och första/,?eller#MUST vara icke-tomt.
Ingen serversidig hämtning. Workern MUST NOT proxyera, hämta eller förhandsgranska URL:en. Protokollet lagrar en sträng; renderingen sker läsarsidigt med rel="nofollow noopener noreferrer" target="_blank" och en synlig värd visad bredvid länktexten.
Reflektion. Frivillig skaparskriven reflektionstext ("vad förändrades, vad lärde du dig"). Samma NFC + kontroll av fientliga kodpunkter som för title. Tom / endast-blanksteg-inmatning faller till frånvarande vid konstruktion.
Schemaversion. v1 stöder endast verdict_version = 0x01. Framtida schemarevisioner bumpar denna byte och landar tillsammans med en ny protokollversion enligt §12.
7. Förseglingsprotokoll
Den fullständiga förseglingssekvensen. Varje steg är normativt.
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.
Lagringstaggskikt (utanför bandet). qub-uppladdningstjänsten fäster en avsiktligt liten uppsättning lagringstransaktionstaggar tillsammans med den inslagna nyttolasten. Content-Type=application/octet-stream krävs normativt. Referenstjänsten fäster dessutom tre frivilliga taggar när skaparen väljer att lyfta fram dem: Intent (allowlist-validerad kompositionsavsikt — t.ex. quote, reply, commitment), Author (skaparens §9.3-pubkey-fingeravtryck som 64-teckens gemen hex) och Parent-Tx-Id (föräldra-qubbens lagringstransaktions-ID för svarskedjor, 43-teckens base64url).
Author-taggen är opt-in per qub: referensskaparappen fäster den endast när användaren uttryckligen aktiverar offentlig tillskrivning vid förseglingstid. När växeln är av — standardläget — skrivs ingen Author-tagg och qubben är otillskriven på kedjan: inget i permanent lagring länkar uppladdningen till en skapares handtag, e-post eller andra qubs. När växeln är på löses Author-fingeravtrycket till skaparens valda @handle via §9.5-attesteringskedjan. Svarskedjerelationer och Intent är icke-identifierande. Det yttre omslaget (§13) skyddar den inre kroppen från chiffertextkorrelation — och förhindrar att en skördare känner igen och massdekrypterar qub-formade uppladdningar efter att deras drand-runda publiceras.
Referenstjänsten fäster avsiktligt INTE App-Name-, App-Version- eller Type-taggar: varje sådant ensamvärdesfilter skulle returnera hela qub-korpusen till en GraphQL-fråga, vilket är inkonsekvent med omslagets kroppsbara konfidentialitetsomfång.
En konform verifierare MUST NOT vara beroende av någon lagringstagg för §11 tredjepartsverifiering; body-hashen / qub_id / signaturen förbinder sig endast till den inre CBOR:en, aldrig till taggsetet.
8. Upplåsningsprotokoll
Den fullständiga upplåsningssekvensen. Varje steg är normativt.
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. Signering av författarskap
9.1 Motivering
qubs lagras i permanent lagring. Signaturer för författarskap måste förbli oförfalskbara på obestämd tid, varför v1.0 använder det postkvantsäkra schemat ML-DSA-65 (FIPS 204) snarare än ett klassiskt schema vars säkerhet kan försämras inom qubbens permanenta livstid.
9.2 Algoritmregister
sig_alg |
Schema | Nyckelstorlek | Signaturstorlek |
|---|---|---|---|
0x00 |
Ingen signatur (osignerad) | — | — |
0x01 |
ML-DSA-65 (FIPS 204) | 1 952 byte | 3 309 byte |
Läsare MUST avvisa okända sig_alg-värden.
9.3 Konstruktion av signerad pre-image
Två pre-image-versioner har funnits. Alla signaturer MUST använda V2, och verifierare MUST endast acceptera V2. Den äldre V1-pre-imagen (dokumenterad nedan som historisk referens) accepterades som en enbart-verifierings-fallback under migreringen till V2; den fallbacken har avvecklats och en signatur som endast är V1 avvisas nu.
V2 (aktuell — produceras av all ny författarsignering, och av båda signaturerna i paktens staging-/motundertecknarflöde):
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 följer samma frånvaro-sentinel-konvention som title_hash (§4.2.1): 32 nollbyte är inte en giltig SHA3-256-utdata, så "frånvarande" kan aldrig kollidera med en närvarande etikett. Alla fält är av fast bredd, så pre-imagen är otvetydig utan längdprefix.
V1 (äldre — AVVECKLAD; produceras inte längre och accepteras inte längre vid verifiering):
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-pre-imagen utelämnade sender_label och reply_to. Den accepterades som en enbart-verifierings-fallback under migreringen till V2; den fallbacken har sedan dess avvecklats — verifierare MUST endast acceptera V2-pre-imagen. Definitionen behålls här som historisk referens och för att förklara domänseparatorn nedan. En signatur som endast verifierar mot V1 MUST behandlas som ett verifieringsfel.
Domänseparatorer: "QUB_AUTHOR_SIG_V1" / "QUB_AUTHOR_SIG_V2" är 17 ASCII-byte vardera ([0x51, 0x55, 0x42, 0x5F, 0x41, 0x55, 0x54, 0x48, 0x4F, 0x52, 0x5F, 0x53, 0x49, 0x47, 0x5F, 0x56, 0x31/0x32]). Ingen utfyllnad. Den skilda separatorn domänseparerar de två konstruktionerna, så en signatur över en pre-image kan aldrig verifiera som den andra.
org_id_present-byte: byten efter unlock_at MUST vara 0x00. Referensimplementationen exponerar detta som konstanten ORG_ID_PRESENT_INDIVIDUAL = 0x00 i crates/qub-core/src/signing.rs; läsare som rekonstruerar sig_input för verifiering MUST avge samma byte.
Signaturomfång — vad som är och inte är täckt. V2-sig_input förbinder sig direkt till version, qub_id, body_hash, unlock_at, sender_label och reply_to (plus den fasta domänseparatorn och org_id_present-byten). qub_id härleds själv från version, content_type, created_at, unlock_at, outcome_at, drand_round och body_hash via §4.1-pre-imagen, så varje ändring av dessa fält producerar ett annat qub_id och ogiltigförklarar signaturen transitivt. Den autentiserade ytan är därför:
| Fält | Autentiserad av signatur | Hur |
|---|---|---|
version |
✓ | Direkt indata till sig_input |
qub_id |
✓ | Direkt indata |
body_hash |
✓ | Direkt indata |
unlock_at |
✓ | Direkt indata |
sender_label |
✓ | Direkt indata via sender_label_hash (V2-pre-image — den enda accepterade formen) |
reply_to |
✓ | Direkt indata via reply_to_or_zero (V2-pre-image — den enda accepterade formen) |
content_type |
✓ | Transitivt, via qub_id pre-image |
created_at |
✓ | Transitivt, via qub_id pre-image |
outcome_at |
✓ | Transitivt, via qub_id pre-image |
drand_round |
✓ | Transitivt, via qub_id pre-image (V1.2) |
body |
✓ | Transitivt, via body_hash = SHA3-256(body) |
author_pubkey |
— (implicit) | Nyckeln som verifierade signaturen är författaren per definition |
cosigner_pubkey / cosigner_signature |
— | Oberoende signerad över samma sig_input (se §9.7) |
drand_chain_id, tlock_ciphertext, visibility |
— | Yttre SealedQub-fält, inte inuti envelopen — täckta av sina egna strukturella invarianter (runda / kedjekonsistens) men inte av författarsignaturen. (drand_round binds nu transitivt via qub_id-pre-imagen — se ovan.) |
Varför V2 är den enda accepterade pre-imagen.
- Under den avvecklade V1-pre-imagen kunde en part med skrivåtkomst till de lagrade bytena byta
sender_label("Alice" → "Mallory") eller om-förälderreply_to— och kryptera om efter rundan — utan att ogiltigförklara författarsignaturen, eftersom inget av fälten fanns i den signerade pre-imagen. V2 täcker båda, så varje ändring av något av fälten vänder verifieringen till "misslyckad". Eftersom verifierare nu endast accepterar V2 är detta byte stängt för varje signatur: en signatur som inte binder något av fälten (dvs. endast verifierar mot V1) avvisas rakt av snarare än att nedgraderas till. author_pubkeyinuti envelopen förblir det sanna identitetsankaret — läsare MUST härleda visningsidentiteten frånauthor_pubkey(via §9.5-attesteringsskiktet) snarare än att lita påsender_label.
Implementationer som visar sender_label eller reply_to för slutanvändare MUST lyfta fram den autentiserade identiteten (pubkey-fingeravtryck, attestering) som den primära identitetssignalen, inte etiketten.
9.4 Verifieringsprocedur
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."
Signaturverifiering är den dyraste operationen (särskilt ML-DSA-65). Den SHOULD utföras efter att alla billigare kontroller (hash, qub_id, unlock_at) har passerat.
9.5 Identitetsattesteringar
Identitetsattesteringar — mappningen av author_pubkey till mänskligt igenkännbara identitetspåståenden såsom ett qub-handtag, en e-postadress, ett socialt handtag eller en passkey-uppgift — är en progressiv förbättring på läsarsidan och är inte obligatoriska för signaturverifiering. Läsare som löser attesteringar till en visningsidentitet MUST tillämpa företrädet:
handle > email > social > fingerprint
Fingeravtrycks-fallbacken är gemen hex av SHA3-256(author_pubkey); den är alltid tillgänglig för varje signerad qub. Läsare MAY förkorta den för visning — referensläsaren renderar qub: följt av de första och sista fyra byten (qub:<8 hex>…<8 hex>).
En konform verifierare kan slutföra varje kontroll i §9.4 utan att kontakta qub-API:et, utan något nätverk utöver permanent lagring och drand, och utan någon serversideuppslagning. Attesteringsupplösning är ett separat steg på bästa möjliga sätt som utförs endast efter att signaturverifiering har lyckats.
9.6 Storlekspåverkan
| Ed25519 | ML-DSA-65 | |
|---|---|---|
| Signatur | 64 byte | 3 309 byte |
| Publik nyckel | 32 byte | 1 952 byte |
| Totalt per qub | 96 byte | 5 261 byte |
| Lagringskostnadsdelta (vid ~5 USD/MB) | ~0,0005 USD | ~0,026 USD |
För en text-qub på 500–2 000 byte tredubblar ML-DSA-65 ungefär den lagrade storleken. Den absoluta kostnaden är försumbar.
9.7 Motundertecknarverifiering (bilaterala paktavtal)
För bilaterala avtal (content_type = 0x03) bevisar ett andra signaturskikt att båda parter samtyckte till samma villkor.
Envelopfält:
cosigner_pubkey: ML-DSA-65 publik nyckel för motundertecknaren (Part B).cosigner_signature: Signatur över sammasig_inputsom författaren (§9.3).
Båda fält MUST vara närvarande tillsammans eller båda frånvarande. Om exakt ett är närvarande MUST läsare rapportera ett integritetsfel.
Verifieringsprocedur:
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."
Egenskaper:
- Motundertecknaren signerar det identiska
sig_inputsom författaren — båda parter förbinder sig till sammaqub_id,body_hashochunlock_at(och, under V2, sammasender_label_hashochreply_to_or_zero). - För att låta motundertecknaren rekonstruera V2-pre-imagen utan åtkomst till de råa envelop-bytena upprätthåller staging-tjänsten vid stage-tid att en paktenvelops
sender_labelär lika medpact_terms.party_a.labeloch attreply_toär frånvarande. Båda gäller för varje referensklient-pakt; envelopar som bryter mot detta avvisas vid stage. qub_id-härledning (§4.1) inkluderar INTE motundertecknarfält. Att lägga till en motundertecknare till en befintlig envelop ändrar intequb_id.- En pakt kan vara endast författarsignerad (ensidigt åtagande), endast motundertecknad (ovanligt) eller båda (fullt bilateralt bevis).
Grind för e-postbindning (operativt). När en staged pakt bär en Part B-e-postkontakt (§6.1) MUST qub-uppladdningstjänsten avvisa motundertecknarförfrågan om inte en kortlivad e-postverifieringsmarkör finns som matchar både staging-ID:t och den normaliserade e-posthashen av den kontakten. Markören skrivs av /api/v1/auth/verify när magic-link-token bär en staging_id och den verifierade adressen matchar SHA-256(normalise_email(party_b.contact)) — där normalise_email(addr) bevarar lokaldelens skiftläge och gemener endast domändelen (enligt RFC 5321 §2.3.11), och SHA-256 här är NIST FIPS 180-4-hashen (skild från SHA3-256 som används i §4-härledningar) — och förfaller 900 sekunder (15 minuter) efter utfärdande. Detta är en operativ anti-imitations-grind, INTE en del av qub-beviset på kedjan — en tredjepartsverifierare som spelar upp §11 behöver endast permanent lagring och drand, utan någon serversideuppslagning. Markören finns endast på serversidan och är aldrig en del av den signerade kroppen.
Storlekspåverkan (ML-DSA-65 författare + motundertecknare):
| Komponent | Storlek |
|---|---|
| Författarsignatur | 3 309 byte |
| Författares publika nyckel | 1 952 byte |
| Motundertecknarsignatur | 3 309 byte |
| Motundertecknares publika nyckel | 1 952 byte |
| Totalt kryptoöverhang | 10 522 byte |
| Lagringskostnadsdelta | ~0,05 USD |
10. Markdown-rendering och sanering
Denna sektion är säkerhetskritisk. Läsaren renderar text-qubs (content_type = 0x01) med en begränsad Markdown-delmängd.
10.1 Tillåtna element
- Rubriker:
#till####(inte#####eller######) - Betoning: fet (
**), kursiv (*), genomstrykning (~~) - Listor: ordnade (
1.) och oordnade (-,*) - Blockcitat (
>) - Kod: inline-spans (```) och fenced blocks (`````)
- Horisontella linjer (
---) - Radbrytningar (två avslutande mellanslag eller blank rad)
- Stycken
10.2 Förbjudna element
| Element | Hantering |
|---|---|
Rå HTML (<div>, <script>, etc.) |
Tas helt bort. Ingen HTML passerar igenom. |
Bilder () |
Tas bort. Bildsyntax tas bort från utdata. |
Länkar ([text](url)) |
URL renderas som synlig klartext. Inte auto-länkad. Inte klickbar utan explicit användaråtgärd. |
| Farliga URL-scheman | javascript:, data:, vbscript:, file: — tas bort. |
| Iframes, embeds, objects | Tas bort. |
| HTML-entiteter | Avkodas till visningstecken endast om de är säkra. |
10.3 Implementation
Implementationer MUST använda en strikt allowlist-tolkare, inte en blocklist. Den rekommenderade metoden:
- Tolka Markdown med
pulldown-cmark(eller motsvarande). - Gå igenom AST:n och släng varje nod som inte finns i allowlist (§10.1).
- För länknoder: avge URL:en som synlig text, inte som ett klickbart
<a>-element. - Konvertera den filtrerade AST:n till en typad mellanrepresentation (t.ex. en
MarkdownNode-enum med endast säkra varianter). Rå HTML är strukturellt icke-representerbar i denna IR. - Rendera från den typade IR:n till mållagrets vy (t.ex. reaktiva vykomponenter, DOM-noder). Ingen HTML-strängkonkatenering eller
innerHTMLvid någon punkt.
Blocklist-metoder är ömtåliga eftersom nya Markdown-utvidgningar eller tolkningskvistar kan introducera ofiltrerade element. Den typade AST-metoden gör XSS strukturellt omöjligt — det finns ingen variant som kan bära godtycklig HTML.
10.4 Storleks- och strukturgränser
- Maximalt renderad rubrikdjup:
####(H4).#####och djupare renderas som fet text. - Ingen gräns på antal stycken (kroppsstorleksgränser i §6 är begränsningen).
- Fenced kodblock: ingen syntaxmarkering i MVP. Renderas som monospace preformaterad text.
11. Tredjepartsverifiering
Vilken tredje part som helst kan verifiera en publik qub utan qubs medverkan. Verifieringsproceduren:
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.
Vad verifiering bevisar:
| Bevis | Vad det fastställer |
|---|---|
| Åtagande | Chiffertexten existerade vid lagringsblockets tidsstämpel. |
| Integritet | Klartextkroppen matchar den åtagna hashen och har inte ändrats. |
| Tidsbestämning | Innehållet var oläsbart fram till drand-rundan, som motsvarar den valda upplåsningstiden (med förbehåll för tlock- och drand-säkerhetsantaganden). |
Vad verifiering INTE bevisar:
| Icke-bevis | Varför |
|---|---|
| Författarskap | sender_label är dekorativ. Utan sig_alg ≥ 0x01 kan vem som helst ha förseglat detta innehåll. |
| Avsikt | qubben bevisar innehåll och tidsbestämning, inte vad skaparen subjektivt menade. |
| Före-händelsetidsbestämning | Lagringsblockinkludering kan släpa efter faktisk uppladdning med minuter. Åtagandetidsstämpeln är blocktiden, inte det ögonblick användaren tryckte "försegla". |
12. Versionshantering
12.1 Protokollversion
version-fältet (u8) i både SealedQub och QubEnvelope identifierar huvudprotokollversionen.
- Läsare MUST avvisa okända huvudversioner med ett tydligt felmeddelande.
- Inom en känd huvudversion MUST tolkare avvisa okända kartnycklar (§3.1) — schemautveckling sker genom att en ny
versioninförs, inte genom att lägga till nycklar som befintliga tolkare skulle hoppa över. (Tidigare revisioner av denna specifikation tillät att okända frivilliga fält tolererades; den klausulen är återkallad — den gjordeencode(decode(x))icke-injektiv och öppnade en vektor för dolt signerat innehåll i paktnyttolaster.) - Innehållstyper (
content_type) och signaturscheman (sig_alg) är versionsgrindade: nya värden får endast införas tillsammans med en ny protokollversion eller en explicit registeruppdatering.
12.2 Versionshistorik
| Version | Värde | Beskrivning |
|---|---|---|
| v1 | 0x01 |
Publika text-qubs (content_type 0x01), bilaterala paktavtal (0x03, schemat structured/v1, ML-DSA-65 författare + motundertecknare), tlock, SHA3-256 |
12.3 Framåtkompatibilitet
En v1-läsare som stöter på en QubEnvelope med okända CBOR-kartnycklar (nycklar som inte finns i §3.2-kanonisk ordning) MUST avvisa den med ett avkodningsfel (§3.1). Framåtkompatibilitet vilar på version-fältet, inte på nyckeltolerans: framtida tillägg — även mindre metadata — levereras under ett nytt version-värde, som en v1-läsare avvisar med ett tydligt "nyare protokoll"-fel snarare än att tyst släppa innehåll som signaturerna åtar sig.
En v1-läsare som stöter på sig_alg = 0x01 (ML-DSA-65) men saknar ML-DSA-65-verifieringsstöd SHOULD visa qub-innehållet med ett meddelande om "signatur närvarande men inte verifierbar", inte avvisa qubben helt. Referensimplementationen idag avvisar varje sig_alg-värde annat än 0x00 och 0x01 eftersom v1-registret inte innehåller någon annan giltig algoritm — strikt avvisande och soft-fail är observationellt identiska tills en tredje algoritm registreras. Soft-fail-beteendet ovan blir bärande när §9.2 inför en ny post, och referensläsaren kommer att uppdateras för att soft-faila vid den tidpunkten.
12.4 Yttre omslagsversion
OuterWrappern som beskrivs i §13 bär sin egen version-byte, oberoende av SealedQub.version och QubEnvelope.version. De två versionsutrymmena utvecklas separat: en framtida postkvantsäker symmetrisk ersättning ökar omslagsbyten utan att röra den inre protokollversionen, och ett framtida protokollskiktstillägg (t.ex. ett nytt envelopfält) ökar den inre versionen utan att röra omslagsbyten.
OUTER_WRAPPER_VERSION_* |
Värde | Algoritm | Status |
|---|---|---|---|
OUTER_WRAPPER_VERSION_1 |
0x01 |
AES-256-GCM med 12-byte nonce, 16-byte autentiseringstagg, AAD bunden till qub_id |
v1-standard |
| — | 0x02–0xFF |
Reserverad | Framtid |
Läsare MUST avvisa okända omslagsversioner med ett tydligt felmeddelande. Protokollet håller avsiktligt omslagsversionsutrymmet smalt tills en konkret migrationsdrivare dyker upp (t.ex. NIST-vägledning som föredrar en annan AEAD); en 0x02-plats kommer att tilldelas i samma revision som introducerar algoritmen.
13. Yttre krypteringsomslag
13.1 Motivering
Protokollskikten (QubEnvelope → tlock → SealedQub) gör en förseglad qub tidslåst: kroppen är oläsbar fram till unlock_at och drand-rundsignaturen har publicerats. Efter upplåsning är emellertid rundsignaturen offentlig och den kanoniska CBOR-formen för SealedQub är igenkännbar, så en skördare som indexerade transaktioner i permanent lagring skulle kunna massdekryptera hela qub-korpusen.
Det yttre krypteringsomslaget stänger den kanalen genom att lägga in ett ytterligare symmetriskt AEAD-skikt mellan den kanoniska SealedQubCbor och de byte som skrivs till permanent lagring. Den 256-bitars nyckeln K lever endast i URL-fragmentet på leveranslänken och på användarenheter; webbläsare överför inte URL-fragment till servrar, så qub.social, varje lagringsgateway och varje CDN framför båda är observationellt blind för K. Varje qub i permanent lagring är därför en ogenomskinlig chiffertext vars klartext är oåterhämtningsbar utan URL:en som skaparen valde att dela.
Nettoeffekt:
- Uppräkningsimmunitet som standard. Inslagna byte i permanent lagring är byte-omöjliga att skilja från godtycklig chiffertext. En skördarstrategi av "GraphQL-fråga efter qub-formade uppladdningar, massdekryptera med offentliga drand-signaturer" avslutas inte med klartext.
- Krypto-strimling-integritetshållning. qub.social kan bokstavligen inte dekryptera sin egen korpus. Stämningar når chiffertext, inte klartext.
- Tvåskiktad konfidentialitetstrappa. Standard = länkstyrd åtkomst (denna sektion). Mottagarkrypterade privata qubs (en reserverad Fas 2-funktion, ännu inte specificerad) lager ovanpå som det andra skiktet.
13.2 Skiktning
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)
Försegling och upplåsning vid protokollskiktet (§7, §8) är oförändrade under omslagsgränsen; omslaget fästs vid anropsplatsen för seal() och tas bort vid anropsplatsen för unlock().
13.3 Datastruktur för 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
}
Fältinvarianter.
versionMUST vara lika med0x01för v1.0-omslagsbyte.qub_idMUST vara lika medqub_id-fältet i den SealedQub som återställs efter uppackning. Uppackningssteget upprätthåller inte detta direkt (AEAD AAD-bindningen gör manipulation på byte-nivå omöjlig), men upplåsningsskiktet kontrollerar relationen transitivt: om en skapare slår in enSealedQubCborvars inrequb_idinte matchar omslagetsqub_idmisslyckas §8 steg 11.nonceMUST vara 96 bitar (12 byte), genererad fräsch av en CSPRNG för varje inslagningsoperation. Återanvändning av en nonce under samma nyckel tillåter AEAD-nonce-återanvändningsattacker som återhämtar klartexten; producenter MUST behandla (key,nonce)-par som engångs.ciphertextär AES-256-GCM-utdata: chiffertextbyte konkatenerat med 16-byte autentiseringstaggen.ciphertext.len() == SealedQubCbor.len() + 16exakt.
CBOR-kodning. Kanonisk CBOR enligt §3, med samma nyckelordningsregel (sorterad efter kodad bytelängd stigande, sedan lexikografiskt). De fyra nycklarna är:
| Nyckel | Kodade byte | Ordning |
|---|---|---|
nonce |
6 | 1 |
qub_id |
7 | 2 |
version |
8 | 3 |
ciphertext |
11 | 4 |
Den första byten av OuterWrapper-CBOR:en är därför det bestämda kart-headern för en 4-posters karta (0xA4).
13.4 AAD-bindning till qub_id
Omslaget binder qub_id som AEAD ytterligare autentiserade data. Detta är det bärande strukturella försvaret mot tre attackklasser:
| Attack | Försvar |
|---|---|
Flytta chiffertext under ett annat qub_id-fält i omslaget |
AAD-omatchning → AEAD-autentisering misslyckas |
| Blanda URL-fragmentet för qub A med byten i permanent lagring för qub B | AAD-omatchning → AEAD-autentisering misslyckas |
Manipulera qub_id-fältet i omslaget efter uppladdning |
AAD-omatchning → AEAD-autentisering misslyckas |
Att bära qub_id i omslagets klartext försvagar inte uppräkningsimmuniteten meningsfullt — qub_id är själv en SHA3-256-hash av §4.1-pre-imagen utan återställbar pre-image från sammanfattningen, och en uppräknare som redan skördade omslagsbyten lär sig inget från det synliga qub_id som de inte kunde sluta sig till från själva uppladdningens existens.
13.5 Inslagnings- och uppackningsalgoritmer
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
Kollaps av felläge. Fel K, fel nonce, AAD-omatchning och manipulerad chiffertext producerar alla samma DECRYPT_FAILED-fel. Detta är en avsiktlig AEAD-egenskap: att skilja felläget skulle skapa en sidokanal som en fjärrangripare kunde sondera genom att skicka felformade omslag och tida svaret. Referensimplementationer MUST kollapsa alla AEAD-fel till en enskild felform.
13.6 Nyckelmaterial och distribution
Inslagningsnyckeln K är ett 256-bitars enhetligt slumpmässigt värde som genereras per qub av en CSPRNG. Referensimplementationerna hämtar det från:
- WASM-skapare:
getrandom(WebCrypto underwasm_js-backenden). - Serversidig förseglings-API-anropare: dess lokala CSPRNG; anroparen tillhandahåller och behåller
Ksomwrapper_key_b64url. Workern använderKi minnet för omslaget men MUST NOT lagra den. Detta gör att ett idempotent återförsök kan återhämta ett maskat svar med anroparens behållna kapabilitet i stället för att bero på en servergenererad engångshemlighet.
Distribution: K MUST kodas som URL-säker base64 (RFC 4648 §5, ingen utfyllnad) och läggas till leveranslänken som fragmentkomponenten:
delivery_url = <origin>/c/<arweave_tx_id>#<base64url(K)>
Fragmentet överförs aldrig till någon server av en konform webbläsare. Återställningskanaler (serverside-historikindex, opt-in automatisk e-postsändning) som persisterar den fullständiga leveranslänken — inklusive fragmentet — bortom användarens enhet är en uttrycklig avvägning mot den standardmässiga krypto-strimlings-hållningen och MUST gating på explicit användarsamtycke.
Fragmentförlust. Om en användare förlorar URL-fragmentet och inte har någon återställningskanal är qubben oläsbar. Detta är den bärande avvägningen i designen och MUST avslöjas för användaren vid förseglingstid. MVP:n förstärker förseglingstidsavslöjandet med uttrycklig "spara denna URL"-text och en verifierad-e-poståterställningskanal för användare som väljer att delta.
13.7 Utanför omfattning för denna sektion
- Författarskapssignering (§9) är oförändrad: signaturer beräknas inuti den inre
QubEnvelopeoch återställs efter uppackning → tlock-dekryptering → CBOR-tolkning. - Mottagarkrypterade privata qubs (en reserverad Fas 2-funktion, ännu inte specificerad) komponerar ovanpå detta omslag som ett andra konfidentialitetsskikt; båda skikten kan vara aktiva samtidigt.
- Pakter (§6, content_type
0x03) slås in exakt som text-qubs; omslaget är byte-blind för inre innehållstyp.
13.8 Offentliga qubs (utelämnande av omslag)
Det yttre omslaget är valfritt på leveransskiktet. En skapare kan försegla en qub som offentlig, varvid den kanoniska SealedQubCbor skrivs till permanent lagring direkt, utan något OuterWrapper-skikt och utan någon nyckel K:
SealedQubCbor bytes ──(public)──▶ uploaded to permanent storage as-is
SealedQubCbor bytes ──(private)─▶ AES-256-GCM(K, …) ▶ OuterWrapper ▶ uploaded
En offentlig qub är tidslåst men inte länkstyrd: den förblir oläsbar fram till att dess drand-runda publiceras (tlock-skiktet är oförändrat), men efter upplåsning kan vem som helst som har arweave_tx_id dekryptera den — inget URL-fragment krävs, eftersom det inte finns någon K. Detta är den avsiktliga avvägningen för ytor som servern måste driva: avslöjandenotifierings-e-post, tredjepartsinbäddningar och rikare SEO efter avslöjande behöver alla en länk som fungerar utan en hemlighet som servern aldrig håller (§13.6).
Följder som en producent MÅSTE ta hänsyn till:
- Ingen uppräkningsimmunitet. Offentliga qubs avstår per konstruktion från §13.1-egenskapen uppräkningsimmunitet. Referensuppladdningstjänsten stämplar en
Visibility: public-tagg i permanent lagring på dem (och endast dem) så att de är avsiktligt upptäckbara; privata qubs bär ingen sådan tagg och behåller sin byte-omöjlighet att skilja. - Klartexttitel exponerad vid förseglingstid. Fältet
titlei §3.2 är klartext inutiSealedQubCbor. Under omslaget är det dolt fram till att en läsare tillhandahållerK; utan omslaget är det världsläsbart i permanent lagring från uppladdningsögonblicket, före upplåsning. Konforma skapar-appar MÅSTE avslöja detta vid förseglingstid. - Detektion är strukturell. En konform läsare/inbäddning skiljer de två formerna genom tolkning: byte som tolkas som
OuterWrappertar uppackning-med-K-vägen; byte som tolkas som en barSealedQubCboraccepteras direkt. Ingen wire-flagga krävs, ochqub_idbinder inte synlighet — samma innehåll är byte-identiskt påSealedQub-skiktet vare sig det förseglas offentligt eller privat.
Privat (inslaget) förblir standard; offentligt är ett uttryckligt skaparval per qub.
14. Testvektorer
14.1 qub_id-härledning
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
Implementationer MUST producera identiska body_hash- och qub_id-värden för denna indata. Denna testvektor SHOULD vara den första enhetstest som skrivs. De kanoniska värdena ovan beräknades av referensimplementationen och MUST matcha bit-för-bit. Historiska pre-image-layouter (före lansering — inga aktiva qubs berodde på dessa): det 92-byte stora V1.0-qub_id var 3d9fc2390eab043d38a1669ed3b71be76f9eefe872b9569ab1aaa027b88392b0; det 100-byte stora V1.1-qub_id (efter invikning av outcome_at_or_zero) var b0d032898ad629795150fdcb3f84e518f59ed05b7a2a82bc24ebdb87f52144ed. V1.2 viker in drand_round och höjer domänseparatorn till QUB_ID_V2.
14.2 Mappning av upplåsningsrunda
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 Kanonisk CBOR-rundresa
Implementationer MUST verifiera att serialize(parse(serialize(qub))) == serialize(qub) för alla giltiga indata. Detta är ett egenskapstest, inte en enskild vektor.
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)
De kanoniska CBOR-byten och SHA3-256 body_hash beräknas av referensimplementationen. Implementationer MUST producera byte-identisk CBOR för denna indata.
Implementationer MUST också verifiera att serialize(parse(serialize(pact))) == serialize(pact) för alla giltiga PactTerms-indata (egenskapstest).
14.5 Tvärspråkliga vektorer för yttre omslag
Det yttre omslaget (§13) har en separat kanonisk fixtur i crates/qub-core/tests/vectors/wrapper_v1.json. Varje fall fixerar en tupel (key, nonce, qub_id, sealed_cbor) som ogenomskinlig hex-indata och hävdar en specifik expected_wrapper_hex-utdata. Båda referensimplementationerna konsumerar samma JSON-fil:
- Rust:
crates/qub-core/tests/wrapper_vectors.rs(cargo test -p qub-core --test wrapper_vectors). - TypeScript:
workers/api/src/crypto/__tests__/wrapper.test.ts(npm test).
Fixturen nålar för närvarande tre fall:
| Fall | Täckning |
|---|---|
basic-text-public |
Minsta realistiska SealedQub-form; inga frivilliga fält. Etablerar den kanoniska omslagsformen för en v1.0-typisk qub. |
with-recipient-pubkey |
SealedQub med recipient_pubkey satt (Fas 2-väg). Annan inre CBOR-nyckeluppsättning, annat qub_id. |
longer-body |
~4 KiB kropp — utövar fleruppdelade CBOR-längdsprefix inuti både den inre envelopen och den yttre chiffertexten. |
Implementationer MUST producera byte-identiskt expected_wrapper_hex för de registrerade indata. Att regenerera fixturen kräver QUB_REGEN_VECTORS=1 cargo test -p qub-core --test wrapper_vectors och är reserverat för avsiktliga formatändringar.
15. Styrning av kryptoprofil (framtid)
Denna sektion är informativ för v1 och blir normativ första gången en andra algoritm träder in i någon av qubs kryptografiska primitiver.
15.1 Nuvarande hållning
Protokoll v1 binder exakt en algoritm per primitiv:
- Signatur: ML-DSA-65 (
sig_alg = 0x01; 1952-byte publik nyckel, 3309-byte signatur) och osignerad (sig_alg = 0x00). §9.2-registret definierar inga andra värden; en v1-verifierare MUST avvisa varjesig_algutanför{0x00, 0x01}. En framtida Ed25519-post är förutsedd (§15.3) men är inte tilldelad i v1. - Tidslås: Endast drand quicknet — kedjehashen, den publika nyckeln, genesistiden och perioden är fasta nätverksparametrar som bärs av referensens
DrandTimelockProvider::quicknet()(crates/qub-core/src/tlock.rs) ochconfig/drand-endpoints.json. - Yttre omslag: Endast AES-256-GCM v1 (§13).
Verifierare hårdkodar för närvarande nyckel- och signaturlängder per primitiv. Ingen agilitetsyta exponeras av trådformatet.
15.2 Avsedd form
När en andra algoritm träder in i protokollet kommer verifieraren att konfigureras för en namngiven CryptoProfile (t.ex. ExqubV1) som listar den exakta uppsättningen tillåtna värden per primitiv — sig_algs, drand-kedjor, omslagsversioner, innehållstyper. Profilen fastställs vid verifieringstid, aldrig förhandlas i bandet. Varje värde utanför den aktiva profilen avvisas.
Detta garanterar att tillägg av ML-DSA-87 eller aktivering av Ed25519 inte retroaktivt kan försvaga befintliga verifierarkonfigurationer: en v1-verifierare förblir en v1-verifierare även efter att en v2-profil publiceras.
15.3 Utlösningsvillkor
Befordra §15 till normativ status när något av följande föreslås:
- En andra
sig_alg-byte (Ed25519-aktivering, ML-DSA-87 eller någon ny post i §9-registret). - En andra drand-kedja i produktionsanvändning.
- En andra yttre omslagsversion.
Tills dess är §15 en platshållare som fastställer migrationsformen så att framtida PR:er landar mot ett känt mål snarare än att åter-förhandla förhandlingsytan från grunden.
16. Transparensslogg och hållbarhetsnivåer (Design — granskning klar)
Status. Denna sektion är en designspecifikation. Trådformaten, hashningen och förtroendemodellen nedan är normativa för implementationen, men ingen transparensloggkod har levererats ännu. Den externa W5-granskningen är klar: §16.15 dokumenterar de avgjorda besluten och de bindande lanseringsvillkor som kom ur den. Implementation får fortgå under dessa villkor. §16 förblir framåtblickande i samma mening som §15 — den fixerar målet så att implementationen landar mot en fastställd design snarare än att åter-härleda förtroendemodellen i kodgranskning. Den är strikt additiv — varje befintlig qub behåller sin individuella Arweave-transaktion och det sker ingen ändring av trådformatet för
SealedQub/QubEnvelope.
16.1 Motivering och hållbarhetsnivåer
Idag vilar en qubs hållbarhet och dess temporala åtagande båda på en enda Arweave-transaktion per qub (§11). Det kopplar förseglingslatens till Arweave-finalitet, gör uppladdningen per qub till ett produktkostnadstak (ARWEAVE_DAILY_CEILING) och ger ingen manipulationsdetekterande ordning mellan qubbar. Transparensloggen lägger till två skikt under och kring den enda nivån:
| Nivå | Namn | Garanti | När |
|---|---|---|---|
| T1 | R2-först synkron ack | Hållbarhetsgolv — förseglade byte skrivs till hållbar lagring innan förseglingen returnerar (< 300 ms p95). |
Varje qub, synkront (§16.10). |
| T2 | Batchad transparensloggsinkludering | Universellt append-only, manipulationsdetekterande åtagande + total ordning, förankrat till Arweave. | Varje qub, uppskjutet + batchat (§16.5–16.7). |
| T3 | Arweave-permanens per qub | En individuell Arweave-transaktion för qubben. | Betald uppförsäljning, och fallback vid Arweave-otillgänglighet (§16.8). |
T2 gör Arweave per qub till ett val (T3) snarare än den enda hållbarhetsvägen. ARWEAVE_DAILY_CEILING avvecklas som ett produkttak och degraderas till en kretsbrytare enbart på den dedikerade förankringsplånboken (§16.7); användarförseglingar avvisas aldrig för att de överskrider det.
Hållbarhetshederlighet (avgjort — §16.15 Q6). Hållbarheten regredierar inte: T1-R2-skrivningen är synkron och skriv-en-gång, så en gratisnivå-qub som inte köpte T3 är fullt hållbar i samma ögonblick förseglingen returnerar. Det som grovkornas är den bevisbara övre gränsen för åtagandetiden: för en gratis-qub blir den förankringsblockets tid snarare än en transaktionsblocktid per qub. Vid låg volym — det realistiska tillstånd som råder tidigt efter lansering och under lågtrafik — är den fullständiga dagliga kadensen det typiska golvet, inte ett sällsynt gränsfall. Produktframställningen är därför en övre gräns utan någon utfäst numerisk latens — "förseglad och hållbar nu; en oberoende offentlig tidsstämpel läggs till vid nästa loggförankring (vanligtvis dagligen)" — och exakt-timme-åtagandebevis är en betald T3-egenskap, som avslöjas på nivåjämförelseytan och i villkoren (§16.11, §16.15 Q6). Varje tidsgräns är enbart en intern SLO, aldrig en marknadsförd SLA.
16.2 LogLeaf-struktur (två åtagna former)
En loggpost är ett LogLeaf, kodat som handskriven kanonisk CBOR enligt §3.1-profilen (bestämd längd, inga taggar, inga flyttal, kortast-formade heltal, NFC-text, frivilliga fält utelämnade när de saknas, nycklar ordnade efter kodad bytelängd stigande sedan lexikografiskt). Den kanoniska parse → re-encode → compare-vakten från §3.1 tillämpas på kodningsvägen före hashning (inte bara vid avkodning), så att två implementationer inte kan vara oense om leaf-byten genom en heltalsbredd- eller nyckelordningsskillnad. Alla heltal är u8 / u64 / i64; alla sammanfattningar är 32-byte bytesträngar (bstr[32]). Ett lagrat Arweave-transaktions-id är en rå 32-byte SHA-256-sammanfattning som bärs som bstr[32], aldrig en base64url-textsträng (matchar §3.3).
Leaf-strukturen har två former valda av en kind-byte, eftersom Workern på standarduppladdningsvägen är byte-blind: POST /api/v1/upload tar emot endast qub_id och unlock_at som obetrodda klientpåståenden — body_hash, drand_round, created_at och drand_chain_version är alla förseglade inuti det yttre §13-omslaget, vars nyckel Workern aldrig håller. Endast serverförseglingsvägen (POST /api/v1/seal) härleder body_hash / drand_round från klartext. En enda leaf-form som bär body_hash + drand_round skulle därför åta sig värden som operatören aldrig verifierade för majoriteten av riktiga qubbar. Uppdelningen håller varje åtaget värde hederligt:
| Nyckel | Kod. längd | Typ | Närvaro | Betydelse |
|---|---|---|---|---|
seq |
4 | u64 |
obligatorisk | Globalt 0-baserat leaf-index; positionen som inkluderingsbeviset åtar sig till. |
kind |
5 | u8 |
obligatorisk | 0x01 attesterad (serverförsegling) eller 0x02 påstådd (klientförsegling / byte-blind uppladdning). |
ref |
4 | bstr[32] |
obligatorisk | Leaf-referens-id. Attesterad → rå qub_id. Påstådd → det blindade id:t SHA3-256(qub_id ‖ log_blind_secret) (§16.2.1). |
chash |
6 | bstr[32] |
obligatorisk | Innehållsadress SHA3-256(stored_bytes) — den enda innehållskopplingen Workern alltid kan beräkna hederligt, på båda vägarna. |
unlock_at |
10 | i64 |
obligatorisk | Kopierad (attesterad) eller påstådd (påstådd); validerad > 0 innan den går in i leaf-strukturen. |
received_at |
12 | i64 |
obligatorisk | Workerns väggklocka vid R2-ack. Icke-evidentiell (operatörspåstådd; §16.6). Närvarande för självbeskrivning, aldrig ett bevis. Validerad > 0. |
body_hash |
10 | bstr[32] |
endast kind=0x01 |
Utelämnad på 0x02 — Workern saknar den under §13. |
drand_round |
12 | u64 |
endast kind=0x01 |
Utelämnad på 0x02. |
Ett kind=0x02-leaf åtar sig avsiktligt varken body_hash eller drand_round: det attesterar åtagandet och ordningen för en ogenomskinlig chiffertext på innehållsadressen chash, med påstående om qub_id och unlock_at — inte dess klartext eller runda. Klartext- och rundbenen för en påstådd qub kommer från den befintliga §11 .qub-buntverifieringen, inte från loggen (§16.11). drand_chain_version finns inte i leaf-strukturen (den ligger inuti omslaget på standardvägen); kedjegranulariteten lever på förankringen (§16.7). Kodningsdisciplin: avvisa ett enbart-nollor ref eller chash, och avvisa icke-positivt unlock_at / received_at, vilket speglar sentinelvakten outcome_at > 0 i cbor.rs.
16.2.1 Blindning av privata qubbar
Loggen får inte bli det uppräkningsorakel som det yttre §13-omslaget finns till för att förhindra (§13.1). För en privat (inslagen) qub åtar sig asserted-leaf-strukturen den blindade identifieraren SHA3-256(qub_id ‖ log_blind_secret), där log_blind_secret är en serverhållen hemlighet, och utelämnar body_hash. En tredje part kan inte knyta ett sådant leaf till ett specifikt qub_id; qubbens innehavare, som har leveranslänken och därför qub_id, kan beräkna om blindningen för att bekräfta sin egen inkludering. En publik qub (redan uppräkningsbar, bär redan Arweave-taggen Visibility: public enligt §13.8) åtar sig det råa qub_id. Detta är den enda platsen där fristående verifierbarhet avsiktligt ger vika för en bärande integritetsinvariant; den fristående kopplingen för privata qubbar är chash (§16.9).
Förvar av log_blind_secret (avgjort — §16.15 Q4). Blindningen skyddar leaf-okopplingsbarhet, inte klartextkonfidentialitet (det yttre §13-omslaget håller det oberoende). Vid en log_blind_secret-kompromiss, för varje qub_id som angriparen redan håller eller kan rekonstruera (varje qub vars bunt/URL den har, plus varje lågentropi- eller publikt qub_id), beräknar den om leaf-ref i en hash och kopplar det — detta är direkt koppling av en känd population, inte en uttömmande sökning över ett okänt utrymme. Klassificera log_blind_secret som en korrelations-/Sybil-gradig hemlighet i samma förvarsnivå som andra serverhemligheter, och rotera enbart framåt (en rotation blindar om framtida leaf-strukturer; den kan inte retroaktivt avkoppla redan förankrade).
16.3 Leaf- och nodhashning
RFC 6962 §2.1 domänseparerad hashning med SHA-256 utbytt mot 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
Domänprefixbyten 0x02 (postkedja, §16.4) och 0x03 (STH-hash, §16.6) är reserverade och disjunkta från dessa. De är enskilda byte och kan därför inte kollidera med de befintliga 10-byte ASCII-domänseparatorerna (QUB_ID_V2, etc.). Trädet är det RFC 6962 vänster-fulla obalanserade trädet (varje inre uppdelning vid den största tvåpotensen strikt mindre än subträdets leaf-antal), vilket låter inkluderings- och konsistensbevis dela en gemensam granskningsvägsalgoritm. Referensspecifikationen bär explicit vänster/höger-härledningspseudokod och nålar en icke-tvåpotens-(5-leaf-)testvektor så att höger-kantbefordringsfallet — som en 4-leaf-vektor döljer — utövas.
16.4 Hashkedjning (intern)
LogDO upprätthåller en intern postkedja enbart för kraschkonsistens. Den publiceras aldrig och är aldrig verifierarvänd:
entry_chain[seq] = SHA3-256(0x02 || entry_chain[seq-1] || leaf_hash[seq])
entry_chain[-1] = SHA3-256("QUB_TLOG_GENESIS_V1")
Den publicerade append-only-auktoriteten är den kumulativa Merkle-roten + dess förankring (§16.5–16.6), aldrig den råa ordning i vilken operatören råkar servera leaf-strukturer: kedjan beräknas om för varje serverad ordning, så endast den förankrade roten nålar den kanoniska positionen.
16.5 Kumulativt Merkle-träd och batchning
Det finns ett ständigt växande RFC 6962-träd över alla leaf-strukturer i seq-ordning — inte isolerade per-batch-träd. (En carry-leaf-kedjad per-batch-konstruktion avvisades: den är inte en äkta prefixrelation, så dess "konsistensbevis" är osunda.) Det kumulativa trädet ger äkta RFC 9162-konsistensbevis och låter en enda nyligen tillkommen förankring bevisa inkludering för vilken äldre qub som helst.
Det durabla objektet LogDO är den enda skribenten (blockConcurrencyWhile, speglar QuotaDO / EntitlementDO) — att lägga till i en delad logg är läs-ändra-skriv på delad tillstånd och MÅSTE därför gå genom en DO, aldrig KV. Den cachar trädets högerkantsfrontlinje (O(log n) hashar) så att stängning av en batch är O(batch). En batch är mängden leaf-strukturer som förankras tillsammans; dess utlösare är konfigurerbara, inte protokollfrysta: ett tree_size-framsteg på minst LOG_BATCH_MAX_LEAVES (standard 4096), eller att åldern når förankringskadensen, eller en framtvingad tömning när en betald T3-försegling landar. root_i är den kumulativa Merkle-trädhashen över leaf-strukturer 0 .. tree_size_i.
16.6 Signerat trädhuvud via Arweave-förankring
Arweave-förankringstransaktionen är det signerade trädhuvudet och ersätter en operatörssignatur för själva trädhuvudet: den dagliga förankringen behöver ingen qub-nyckel eftersom Arweave-transaktionens owner är signaturen. Vallgravstesen håller — det oföränderliga substratet, inte en qub-hållen hemlighet, är bärande för den förankrade roten.
Det finns exakt en varm qub-signeringsnyckel i designen, och den är nålad: kvittonyckeln per försegling (§16.10). Dess publika nyckel åtas i LogProfile (distribuerad med verifieraren) och korssigneras av anchor_owner, så att en verifierare validerar ett kvitto mot samma nålade rot som förankringen. Detta är lösningen på §16.15 Q2 — en onålad, operatörsroterbar kvittonyckel skulle vara förnekbar (operatören kunde förneka att nyckeln var deras), vilket skulle upphäva kvittots ansvarsvärde mot den operatörsnivåangripare som kvittot finns till för att avskräcka. Alltså: qub håller ingen onålad loggsigneringsnyckel; kvittonyckeln är nålad och korssignerad av anchor_owner.
SignedTreeHead är kanonisk CBOR (nycklar efter kodad längd): size:u64, root:bstr[32], batch:u64, prev:bstr[32] (föregående sth_hash; genesis = 32 nollbyte), log_id:bstr[32], first_seq:u64, anchored_at:i64. Dess hash är sth_hash = SHA3-256(0x03 || canonical_cbor(SignedTreeHead)).
Nålad förtroenderot. log_id = SHA3-256("QUB_TLOG_V1" || anchor_owner_address). En konform verifierare MÅSTE kräva anchor_tx.owner == LogProfile.anchor_owner, där anchor_owner (och kvittonyckelns publika nyckel) är inbakad i qub_core som LogProfile — vid sidan av quicknet-konstanterna som redan finns i DrandTimelockProvider::quicknet() — och distribuerad med verifierarbinären. Verifieraren MÅSTE också verifiera Arweave-transaktionens data → tx_id-bindning lokalt snarare än att lita på ett gateway-/raw/-svar. Detta stänger hålet för olovlig-plånboks-tvetydighet: "förankrad på Arweave" är meningslöst tills verifieraren nålar vilken plånbok.
Rotation är en §15-styrningsutvidgning, inte ett återbruk (avgjort — §16.15 Q3). §15.2:s profilyta uppräknar för närvarande endast sig_algs / drand-kedjor / omslagsversioner / innehållstyper, och §15.3:s utlösare listar ingen av dessa — LogProfile / anchor_owner är ännu inte i §15:s yta. Rotationsstyrning måste därför byggas: §15.3 utvidgas (nedan) för att lägga till LogProfile-utlösaren, och en rotation är en signerad LogProfile-höjning som levereras i en verifierar-uppdatering. En planerad rotation bär en utgående → inkommande korssignatur; en kompromissdriven rotation kan inte (den utgående nyckeln är obetrodd/otillgänglig just då) och faller tillbaka till den §15-styrda höjningen, med föregående-förankrings-forkkontrollen (nedan) som begränsar skadan under mellantiden.
Tvetydighetsfönster (förstklassig förtroendeparameter). Ett leaf är tvetydighetsbeständigt först när dess täckande förankring är Arweave-bekräftad. Fönstret är received_at → förankringsbekräftelse (≤ kadens + Arweave-finalitet). Inom det är de enda garantierna det nålade förseglingskvittot (§16.10) och qubs operationella integritet. Tre ansvarsartefakter gör detta hederligt snarare än bortviftat (vittnesmodellen är §16.15 Q2:s lösning):
- Nålat signerat förseglingskvitto — SCT-analogen som returneras i uppladdningssvaret (§16.10), signerat av den nålade,
anchor_owner-korssignerade kvittonyckeln. Ett leaf som tappas före sin förankring lämnar offret ett icke-förnekbart kvitto att publicera, vilket stänger hålet för tyst utelämnande. - Publicerad övervakningsmetodik + föregående-kedjevandring — förankringens
prev-kedja vandras huvud→genesis; en fork (två förankringar vid ensizemed olikaroot, eller en brutenprev) är publicerbart bevis på missuppförande. Tvetydighetsdetektion är ett uttalat operationellt åtagande, inte ett tyst antagande. - Dubbla självpublicerade huvuden — varje nytt huvud
{sth_hash, tree_size}postas till ett dedikerat qub-ägt offentligt, append-only GitHub-arkiv (det bärande manipulationsdetekterande självpubliceringsbenet), med ett socialt inlägg som best-effort-bekräftelse enbart. En misslyckad postning MÅSTE personsöka (inte misslyckas tyst).
Hederlighetsgräns (bindande villkor). Eftersom qub kontrollerar båda postningsytorna är detta självpublicerat, inte oberoende bevittnat. Ingen produkt-, marknadsförings- eller juridisk yta får hävda att loggen är "oberoende bevittnad"; det tillåtna påståendet är att tvetydighet är detekterbar och lämnar ett icke-förnekbart kvitto. Ett äkta oberoende tredjepartsvittne skjuts upp till en framtida §15-styrningshöjning.
received_at är operatörspåstådd och inget påstående får luta sig mot den — den ytas aldrig som bevis eller som tvistbekräftelse på någon produkt-/juridisk/API-/bevisåtergivningsyta. Arweave-förankringsblocktiden T är den enda förtroendefria tidsstämpeln (en övre gräns för "loggad senast"). Varje övervakningsförnuftskontroll på received_at MÅSTE jämföra mot T, inte mot det operatörskontrollerade STH-fältet anchored_at; en sådan kontroll är en vakt mot en hederlig operatörs klockfel enbart, inte en ansvarskontroll mot en illvillig operatör (§16.15 Q5).
16.7 Förankringstransaktionsformat och kadens
AnchorBundle är den kanonisk-CBOR Arweave-transaktionskroppen, skriven via §16.8-bundlaren: ver:u8, sth:bstr (kanoniska SignedTreeHead-byte), prev_anchor:bstr (föregående förankrings-transaktions-id i råa byte; utelämnad vid genesis), chain_hash:tstr (drand-kedjan i kraft — quicknet), och batchens leaf-CBOR-ström i seq-ordning så att förankringen är självständig: en övervakare härleder om root från kroppen med noll qub-beroende. (Om leaf-strömmen blir stor vid hög volym kan en framtida revision åta sig endast ett leaf-intervall genom referens; noterat, inte antaget i v1.)
Arweave-taggar är avsiktligt uppräkningsbara — loggen är menad att hittas, till skillnad från privata qubbar: App-Name: qub-tlog, Anchor-Format: 1, Log-Id: <hex>, Batch: <n>, Tree-Size: <n>, Root: <hex>, Prev-Anchor: <tx>, Content-Type: application/cbor. Taggar är obetrodda ledtrådar; CBOR-kroppen är den enda auktoriteten.
Kadens: dagligen som standard, omprövad med volymen (storleksutlösaren auto-förkortar den effektiva kadensen under belastning); en betald T3-försegling framtvingar en förankring så att betalande kunder aldrig väntar en dag. Förankringsplånboken är dedikerad och låghastig, separat från uppladdningsplånboken — den MÅSTE vara sin egen JWK (en distinkt nyckel, inte en logisk roll på uppladdningsplånboken) så att en uppladdningsplånbokskompromiss inte kan förfalska förankringar — med en hård budget för förankringstransaktioner per dag (det degraderade ARWEAVE_DAILY_CEILING). Förvarshållningen anges rakt ut: en smal-omfångad het nyckel med en snäv kretsbrytare och låg saldo, inte "kall" — en plånbok som auto-signerar dagligen kan inte vara kall, och specifikationen låtsas inte om annat.
16.8 ANS-104-bundlare
En egenutvecklad ANS-104 DataItem-kodare och deep-hash-signerare, ungefär 300 rader, enbart Web Crypto, noll npm-beroenden (båda Turbo-SDK:erna underkänns av §npm ci --ignore-scripts-leveranskedjegrinden). DataItem-bytelayout:
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
Signering är Arweave deepHash — en rekursiv SHA-384-sammanfattning (Arweaves trådkrav, crypto.subtle.digest("SHA-384")) över ["dataitem", "1", sig_type, owner, target, anchor, encoded_tags, data] — sedan RSA-PSS över deep-hashen med plånbokens JWK via crypto.subtle; id = base64url(SHA-256(signature)). SHA-384 här är karantänsatt som en enbart-Arweave-tråd-primitiv, aldrig en qub-förtroendeprimitiv (§15 dokumenterar stängslet; qub-förtroendehashning är SHA3-256 genomgående).
En kodväg betjänar tre konsumenter: betald T3-permanens per qub, fallbacken vid Arweave-otillgänglighet (köa DataItem:et, returnera R2-först-acken oavsett — detta stänger den nuvarande ARWEAVE_UNAVAILABLE 503-återvändsgränden), och skrivning av AnchorBundle. Signaturschema (avgjort — §16.15 Q8): v1 signerar med RSA-PSS (signaturtyp 1) och återbrukar den befintliga Arweave-plånboks-JWK-mekanismen (noll nytt långlivat nyckelförvar, vilket betjänar tesen "en hemlighet färre"); Ed25519 skjuts upp till §15-PQ-migrationsvägen.
Den handrullade deep-hashen är den högst riskfyllda, lägst naturligt täckta koden i W5, så dess grindning är icke-förhandlingsbar (§16.15 Q8):
- Den tvärspråkliga fixturen
tlog_v1.json(Rust + TS, §14.5-mönstretwrapper_v1.json) täcker deep-hash, DataItem-byte + id, leaf-hashar, en 5-leaf-rot + granskningsväg, en STH-hash, ett inkluderingsbevis och ett konsistensbevis — i både sign- och verify-riktningen (verify-riktningen är viktig eftersom §16.6:s lokala tx → tx_id-kontroll drar in deep-hashen i varje fristående verifierare, inte bara skribenten). - En engångs-interop-rundresa genom en referens-ANS-104-bundlare, konsumerad som enbart statisk testdata — aldrig ett npm-runtime-beroende (den enbart-Web-Crypto-/inga-installationsskript-hållningen står fast).
- Deep-hash- + RSA-PSS-vägen MÅSTE rundresa genom samma
crypto.subtle-primitiver som produktionen använder, så att den egenutvecklade kodaren är byte-kompatibel. - En pågående post-bunt-accept-övervakare bekräftar att varje förankrings-/fallback-DataItem faktiskt uppnår Arweave-accept, med ett larm + kretsbrytare — eftersom deep-hashen även betjänar fallback-kön vid Arweave-otillgänglighet, så att en tyst regression skulle fylla den kön med nätverksavvisade poster under just det avbrott den finns till för att täcka.
16.9 Inkluderings- och konsistensbevis
Båda är RFC 9162, SHA3-256, serverade som kanonisk CBOR.
InclusionProof — GET /api/v1/qub/:tx_id/proof: ver:u8, leaf:bstr (den exakta leaf-CBOR:en — verifieraren beräknar om leaf_hash själv och litar aldrig på en tillhandahållen 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. En enda otvetydig nyckellista, nålad av testvektor.
Fristående verifiering (ingen qub-server, utvidgar §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).
Bevisserverande lagring MÅSTE vara koordinatnycklad (avgjort — §16.15 Q7, blockerande förutsättning). Kall-leaf-bevisgenerering är korrekthetsneutral endast om R2-granskningsmaterialet är ett persistent Merkle-nodlager nycklat efter absolut trädkoordinat (level, index) — inte per-batch-noddeltan. Med ett koordinatnycklat lager är varje (leaf i, size N)-granskningsväg en mängd O(log N) direkta R2-GET med ingen omberäkning över batchgränser; med ett batchnycklat lager är den det inte, vilket är den lagringslayoutlucka denna lösning stänger. Leaf-kropparna är likaledes innehållsadresserbara efter seq. En W5-testvektor MÅSTE bevisa ett genesis-era kallt leaf mot en mycket senare rot med enbart R2 + Arweave och LogDO-lagringen utraderad, så att återvinningssäkerhetspåståendet i §16.13 är underbyggt snarare än påstått. De O(log N) sekventiella R2-GET hör hemma enbart på den asynkrona bevis-endpointen — aldrig på förseglings-hotvägen (§16.10) eller en per-tick-cron.
16.10 R2-först-ack-ordning
POST /api/v1/upload-sekvensen blir:
- Framhalv-grindar (auth, validering, idempotensshardnyckel) — oförändrade.
- Synkront
await QUB_CACHE.put(qub-cache/<tx_id>, wrappedBytes)— hållbarhetsgolvet; stänger också W1:s precache-kapplöpning (tidigare enctx.waitUntilefter Arweave-inlämningen). - Synkront
await LogDO.append(leaf)— en in-colo DO-RPC; den enda skribenten tilldelarseq, utvidgar postkedjan och uppdaterar frontlinjen. (RMW på delad tillstånd → DO, aldrig KV.)append-RPC:n gör endast det —O(batch)-Merkle-batchstängningsarbetet körs utanför denna RPC på LogDO-larmet, annars spikar append-p95 vid varLOG_BATCH_MAX_LEAVES:te försegling. - Returnera acken nu — med förseglingskvittot (signerat av den nålade kvittonyckeln, §16.6) och
{ tx_id, log_seq, anchor_status: "pending" }. Den flersekunders Arweave-utspridningen tas bort från den kritiska vägen. - En
ctx.waitUntilköar det uppskjutna arbetet: Arweave-inlämningen per qub (nu best-effort / betald; vid fel routas den till bundlar-fallback-kön snarare än att 503:a användaren) plus de befintliga provisoriska metaskrivningarna. Batchstängning och förankring körs oberoende från LogDO-larmet och den dagliga förankrings-cron:en. Ingenctx.waitUntilinuti en loop; den befintliga idempotensshardnyckeln bevaras.
Latensbudget (avgjort — §16.15 Q7). < 300 ms-p95-målet är en mätt lanseringsgrind, inte ett antagande. Den hederliga kritiska vägen är framhalv-KV-läsningarna + en R2-PUT + två serialiserade durabla objekt — den befintliga QuotaDO-förseglingskvotsdebiteringen och den nya LogDO-tillägget — så budgeten måste ta hänsyn till två in-colo-DO-rundresor, inte en. Leverera ett LogDO-latenslarm som speglar QuotaDO:s och behandla en p95-regression som en release-blockerare.
16.11 Förtroendemodell — det exakta påståendet, omfångat av leaf-typ
För kind=0x01 (attesterad): "Detta innehåll — kropp som matchar body_hash, identifierad av qub_id — åtogs till qubs append-only-logg vid position seq och existerade senast vid Arweave-blocktiden T; det var kryptografiskt oläsbart fram till drand-rundan R = unlock_round(unlock_at)." Detta är den fullständiga {tlock-rundbindning + Merkle-inkludering + förankrad rot}-tripeln.
För kind=0x02 (påstådd, standarden): "En ogenomskinlig chiffertext med innehållsadressen chash, med påstående om qub_id och unlock_at, åtogs till append-only-loggen vid position seq och existerade senast vid Arweave-blocktiden T." Rund- och kroppsbenen tillhandahålls av den befintliga §11 .qub-buntverifieringen (qub_core::unlock), inte av loggen; det loggen tillför utöver en bar transaktion per qub är manipulationsdetekterande ordning, en förtroendefri övre-gräns-åtagandetid och tvetydighetsbeständighet.
Båda påståendena utesluter, enligt §11: författarskap utan sig_alg ≥ 0x01, avsikt, och sub-förankrings-granularitetstidsbestämning. Inget av dem låter något påstående luta sig mot received_at.
Påståendetak (bindande lanseringsvillkor — avgjort §16.15 Q1). För gratis-/standard-(kind=0x02)-qubbar är det omfångade kind=0x02-påståendet ovan taket för vad någon produkt-, marknadsförings-, villkors- eller bevisåtergivningsyta får hävda. Ingen yta får påstå eller antyda att loggen bevisar innehållet eller upplåsningsrundan för en standard-qub — loggen bevisar ordning + en förtroendefri övre-gräns-åtagandetid för en ogenomskinlig chiffertext. Innehålls- och rundbevis kommer uteslutande från den befintliga §11 .qub-buntverifieringen, som är loggoberoende. Detta är en hård lanseringsblockerare på copy, inte en stilistisk preferens; det är lösningen som håller den byte-blinda standardvägen hederlig.
16.12 Versionshantering och W3-koordinering
Det sker ingen SealedQub-trådhöjning och därför ingen protokollversionshöjning (§12.1): loggen är en sidovagn som åtar sig befintliga fält och byte, så den går inte in i §12.2-protokollversionshistoriken. W3:s frivilliga drand_chain_version är orörd och förblir det enda frivilliga SealedQub-fältet. Loggen inför i stället sina egna oberoende versionsutrymmen — LOG_VERSION_1, ANCHOR_FORMAT_1, InclusionProof.ver — som speglar §12.4-omslagsversionsoberoendet (omslaget bär en versionsbyte oberoende av protokollversionen, och loggversionerna följer samma separation).
Bevisleverans hämtas som standard, med en frivillig medåkning. Ett bevis kan inte existera vid förseglingstid (förankringen har inte skrivits ännu), så förseglingstidens .qub-bunt förblir bevisfri. W7:s verifierare hämtar GET …/proof en gång, eller rekonstruerar i fullt offline-läge beviset från den offentliga AnchorBundle via en Arweave-fråga på Log-Id. .qub-bunten (W7) reserverar en frivillig inclusion_proof-medlem — frånvarande vid försegling, fylld av en post-förankrings-omexport för kall arkivering — som följer samma "frivillig, utelämnad som standard, additiv"-mönster som W3:s drand_chain_version.
16.13 Retention
Retentionsfönster för LogDO-öppna-svansen, R2-bevisserverande substratet, förankrings-kretsbrytarräknarna och bundlar-fallback-kön specificeras i docs/DATA-RETENTION.md. Princip: loggens heta per-post-lagring (LogDO) är återvinningsbar efter förankring; dess granskningsmaterial — det koordinatnycklade (level, index)-Merkle-nodlagret + de seq-adresserade leaf-kropparna (§16.9) + Arweave-förankringarna — är permanent. Att återvinna ett kallt leaf från DO:n ogiltigförklarar aldrig ett utfärdat bevis, eftersom ett bevis löses mot det permanenta R2-nodlagret och Arweave-förankringen, inte DO:n (och §16.9:s utraderad-DO-testvektor bevisar det).
16.14 Testvektorer
W5 levererar den tvärspråkliga fixturen tlog_v1.json (§16.8) plus utarbetade vektorer: ett kind=0x01- och ett kind=0x02-leaf → leaf_hash; den 5-leaf kumulativa roten; ett inkluderingsbevis; ett konsistensbevis; en AnchorBundle; och ett DataItem-id. Dessa lever vid sidan av §14.5-yttre-omslagsvektorerna och utövas av både Rust-(qub-core)- och TypeScript-(Worker)-implementationerna.
16.15 Granskningsbeslut (W5 — avgjorda)
Den externa W5-granskningen (en adversariell designgenomgång + ägargodkännande) är klar. Varje beslut nedan är fastställt och återspeglat i §16-texten ovan; de bindande lanseringsvillkoren upprepas i slutet. Implementation får fortgå under dem.
- Standardvägs-(
kind=0x02)-leaf-hederlighet — LÖST. Leverera tvåleaf-typ-uppdelningen som specificerad:kind=0x02åtar sig varkenbody_hashellerdrand_round. Inget*_body_hash-fält på den byte-blinda vägen (det skulle vara den mest läsbara falska "verifierad"-signalen för integratörer och är en bekvämlighet §11 redan tillhandahåller från bunten). Kräv inte serverförsegling för loggattesterade qubbar (det skulle tvinga klartext genom Workern och förstöra krypto-strimlings-vallgraven). Varje självbeskrivande kortslutning hör hemma i.qub-bunten / bevis-kuvertet som ett verifierar-omberäknat fält, aldrig ett leaf-fält. Ägarbekräftat påståendetak: §16.11. - Tvetydighets-/utelämnandeansvar — LÖST. Förseglingskvittonyckeln är nålad i
LogProfile+anchor_owner-korssignerad (stänger den tidigare "ingen signeringsnyckel"-motsägelsen; §16.6). Lanseringsvittnesmodell: nålat kvitto + övervakningsmetodik + föregående-kedjevandring + dubbla självpublicerade huvuden (qub-ägt offentligt GitHub-arkiv, socialt best-effort), marknadsfört som detekterbart + kvitterat, aldrig oberoende bevittnat. Ett äkta tredjepartsvittne skjuts upp till en §15-styrningshöjning. - Nålad förankringsägar-förtroenderot + rotation — LÖST. Anta
LogProfile-nålningen (§16.6); verifieraren kontrolleraranchor_tx.owner == anchor_owneroch verifierar tx-data → tx_id-bindningen lokalt. Rotationsstyrning är en §15-utvidgning att bygga (§15.3-utlösare tillagd), inte ett återbruk; planerade rotationer korssignerar, kompromissdrivna rotationer faller tillbaka till §15-höjningen med forkkontrollen som begränsar skadan. - Privat-qub-leaf-blindning — LÖST. Behåll blindning för privata qubbar (
ref = SHA3-256(qub_id ‖ log_blind_secret)), råttqub_idför publika qubbar (redan §16.2.1),chashsom den fristående kopplingen.log_blind_secretär en korrelations-/Sybil-gradig hemlighet, rotera-framåt enbart (§16.2.1). received_at— LÖST. Behåll den i leaf-strukturen, åtagen men explicit icke-evidentiell; aldrig ytad som bevis eller tvistbekräftelse på någon yta. Varje övervakningsförnuftskontroll jämför mot Arweave-blocktidenT, inte mot det operatörskontrolleradeanchored_at(§16.6).- Gratisnivå-bevisbar-tidsbestämning — LÖST (ägargodkännande). Hållbarheten regredierar inte; endast den bevisbara övre-gräns-åtagandetiden grovkornas till förankringsblocktiden. Gratisnivå-copy använder ingen numerisk SLA ("…läggs till vid nästa loggförankring, vanligtvis dagligen"); exakt-timme-bevis är en betald T3-egenskap, avslöjad på nivåjämförelseytan + i villkoren (§16.1).
- Kumulativt träd på Workers — LÖST. Enda kumulativa RFC 9162-träd + frontlinje-cachad enskild-skribent-LogDO (bekvämt utrymme mot ~1k skrivningar/sek-DO-taket; skjut upp Merkle-av-shard-rötter-shardning tills nära det). Blockerande förutsättning: koordinatnycklat
(level, index)-R2-nodlager + utraderad-DO-kall-leaf-testvektorn (§16.9);< 300 msär en mätt lanseringsgrind över två serialiserade DO:er (§16.10). - ANS-104-signaturschema + deep-hash — LÖST. RSA-PSS (sig-typ 1, återbrukar den dedikerade förankrings-plånboks-JWK:n); Ed25519 uppskjutet till §15-PQ-vägen. Den handrullade SHA-384-deep-hashen grindas på båda-riktningars tvär-impl-fixturen, en enbart-statisk referens-bundlar-interop-kontroll, den delade-
crypto.subtle-rundresan, och post-bunt-Arweave-accept-övervakaren (§16.8).
Bindande lanseringsvillkor (bär in i implementation + produkt-/juridisk granskning):
- Påståendetak (Q1/Q6). Ingen yta får säga att loggen bevisar en standard-qubs innehåll eller upplåsningsrunda; det tillåtna påståendet är ordnat, manipulationsdetekterande, med en förtroendefri övre-gräns-åtagandetid. Gratisnivå-tidsstämpel-copy bär ingen numerisk latens; exakt-timme-bevis är enbart betald T3.
- Vittneshederlighet (Q2). Marknadsför tvetydighet som detekterbar + kvitterad, aldrig oberoende bevittnad.
- Kvitto- + förankringsnycklar (Q2/Q8). Kvittonyckeln är nålad + korssignerad; förankringsplånboken är sin egen JWK distinkt från uppladdningsplånboken.
- Deep-hash-grind (Q8). Ingen förankring eller T3-tx levereras förrän båda-riktningars-fixturen + interop-kontrollen passerar; accept-övervakaren personsöker vid fel.
- Lagringsförutsättning (Q7). Koordinatnycklat nodlager + utraderad-DO-kall-leaf-vektor är förutsättningar för "återvinning ogiltigförklarar aldrig ett bevis"-garantin.