Kontinuum Node — Implementation Notes
Известные edge-cases и derived противоречия из v0.5 раунда правок (раунд 3 поиска). Решения переносятся в implementation phase — закрываются по мере написания кода.
Audience: node developers, начинающие имплементацию.
Связанные документы:
architecture.md— start hereprotocols.md— wire-level mechanics, на которых построены большинство известных edge-casesoperations.md— lifecycle и threat-model контексты
Section codes (§20) preserved from единой v0.6 спеки.
Большинство этих пунктов — производные от mechanism'ов, введённых в предыдущих раундах разрешения противоречий (transaction_id, atomic-pair quarantine, re-encryption priority, redirect mode, family-mode quotas). Каждый — implementation-level, не architectural.
20. Известные противоречия и нестыковки
Раунд 3 после правок v0.5. Большинство — derived от новых mechanism'ов, введённых в предыдущем раунде.
20.1 pending_propagation field не определён в struct
Где: §5.2 (struct SignedMemberOp) ↔ §5.2.2 (degraded mode).
§5.2.2 говорит: «помечает их pending_propagation = true». Но это поле отсутствует в struct SignedMemberOp в §5.2.
Решение: хранить как local-only state (не сериализуемый в DHT) — это purely local flag для tracking что op применена локально, но не propagated. Не нужно ставить в struct, но нужно явно сказать в §5.2.2 что это local state, не on-wire field.
20.2 DhtPut semantics для ops с transaction_id
Где: §5.2.2 ↔ §11(b).
§11(b) описывает DhtPut single-record semantics: «node принимает только записи, для которых она в k-ближайших к key. Проверяет подпись + freshness».
§5.2.2 предписывает: «Receiving нода видит op с transaction_id → quarantine, ждёт остальные op'ы». Это изменяет семантику DhtPut — ноды теперь не применяют ops сразу, а отложенно.
Решение: обновить §11(b) явно — «DhtPut с payload.transaction_id = Some(T) подлежит quarantine-rule §5.2.2; single ops с transaction_id = None применяются immediate».
20.3 Anti-entropy gossip и quarantined ops
Где: §5.2.2 ↔ §11(d).
Anti-entropy gossip exchange'т Merkle-tree корни partition'ов. Quarantined ops — это полученные, но не применённые записи. Должны ли учитываться в Merkle hash?
- Если да → ноды получают divergent state даже когда transactions ещё в quarantine; constant false-positive divergence.
- Если нет → quarantined ops не propagate через anti-entropy → если transaction частично потеряна в DHT propagation, она никогда не восстановится; зависит от owner-retries (если owner offline после партиального publish — transaction теряется).
Решение: anti-entropy hash включает quarantined ops, но MerkleLeaf обмен несёт transaction-membership-info (какие ops транзакции уже получены) — это позволяет ноде A заполучить недостающие op'ы транзакции у ноды B. Требует расширения wire-формата.
20.4 Re-encryption job vs hard delete (day 28+)
Где: §5.2.3 ↔ §13.3.
§5.2.3: «re-encryption job имеет priority над read-only freeze». §13.3 day 28+: «hard delete local data».
Что побеждает на day 28? Реальный конфликт — для shared blob'ов через friend-replication копии лежат на friend's ноде. Если local на orphan-byo удалена раньше re-encryption завершения → friend's нода держит un-rotated ciphertext, который kicked member может расшифровать со старым space_key.
Решение: на day 28 checkpoint — оставшиеся не-перешифрованные blob'ы помечаются force_delete_or_handoff и передаются friend-replica для завершения re-encryption job. Если handoff невозможен — blob'ы удаляются полностью (forward secrecy через destruction).
20.5 Pins при downgrade в free-tier
Где: §9.5 ↔ §14.3.
Сценарий: user был PRO → pin'нул blob'ы → PRO lapse → app переходит в Free-режим. По §9.5 free-tier toggle disabled. Но existing pins остаются в Space DHT.
Что с ними:
- Pin'ы продолжают работать (forced replica всё ещё требуется)?
- Auto-cleanup при downgrade?
- На чьей ноде живёт «forced replica» если paid infra ушла?
Решение: при cert lapse → existing pins user'а становятся dormant — replica остаётся в network у friend's нод (friend-replication §9.3), но не считается «owned forced replica». UI показывает «N pins inactive — buy relay or PRO to reactivate forced placement».
20.6 Ungraceful shutdown byo-ноды
Где: §13.3 ↔ §13.6.
§13.3 предполагает graceful flow: cert revocation → 14-day window для re-encryption / refresh URLs / update provider records. Но ungraceful shutdown (VPS terminated, power outage, hardware failure) обходит всю эту mechanics.
Provider records остаются stale, peer'ы получают timeout. External S3 blob'ы становятся unreachable через DHT lookups (хотя физически в bucket'е есть).
Решение: многослойная:
- External heartbeat detection через Tier 0 / Tier 1 ноды: long-offline (≥24h) ноды автоматически помечают provider records как
suspected_dead. - Friend-replica takeover — если у blob'а есть friend-replica, она становится primary serving point.
- Fallback external direct URL в Space DHT — Pin с external bucket'ом хранит резервный direct URL помимо node-mediated provider record.
20.7 Quota enforcement в family-mode — single source of truth
Где: §7.3 ↔ §5.2.3.
§7.3: «Per-tenant квота — задаёт owner». §5.2.3: «auth-shim применяется единообразно... включая admin API владельца ноды».
Кто enforce'ит tenant'овую квоту?
- Если admin host (node-level config) — admin может trivially изменить quota stealth-mode (например снизить до 0 = de-facto kick без удаления из whitelist).
- Если auth-shim (DHT-record signed owner'ом) — quota становится observable commitment'ом, изменения видимы tenant'у.
Без явного source of truth — admin host имеет hidden privilege.
Решение: quota хранится в Space DHT как signed-by-host-owner record. Auth-shim проверяет квоту перед POST /presign. Изменение квоты — observable op в DHT, tenant видит уведомление в app («Quota changed: was 50GB, now 10GB»).
20.8 Propagation race window для Revoke
Где: §5.2.2 ↔ §5.2.3.
Lazy 2PC через DhtPut + quarantine — transaction применяется на receiving нодах после полной propagation. Со seqno-based + k=20 в global DHT propagation может занять секунды-минуты.
Race window: kicked member может скачать blob через replica, ещё не получившую Revoke. Auth-shim ACL на ноде N1 → 403. Auth-shim ACL на ноде N2 (не получила) → 200.
Решение:
- Owner после публикации Revoke transaction делает immediate broadcast к всем известным replicas Space DHT (push вместо pull propagation).
- Auth-shim TTL-cache для membership: max 30 секунд → re-validate через DHT. Bounded race window.
- Receiving нода при получении одной из ops транзакции broadcast'ит к k-ближайшим (chain replication).
20.9 Direct Space — SelfLeave owner'a
Где: §5.2 ↔ §5.2.1.
§5.2: «Owner Direct Space = initiator. Второй member может только SelfLeave». Но что если owner SelfLeave'ит? Это:
- Owner-action — никто другой не может kick.
- Space теряет owner → orphan state.
- Кто next owner? Логично — второй member (он единственный остался).
Решение: при SelfLeave owner'a в Direct Space — owner field автоматически передаётся второму member'у. Если оба SelfLeave'ят одновременно (race) — Space становится orphan, eventual GC.
Для Shared Space (≥3 members): SelfLeave owner'a — открытый вопрос. Forced succession (next-most-trusted member) либо Space orphan'ится. Новый open question.
20.10 Mailbox cursor updates — housekeeping или user activity?
Где: §13.3 ↔ §11(f).
§13.3: housekeeping API calls разрешены кроме hard delete. MailboxCursorUpdate (§11f) — это update от device о position'е чтения mailbox'a.
- Если user activity — в read-only фазе cursor updates rejected → mailbox sync застывает → live cursors блокируют GC → mailbox растёт.
- Если housekeeping — sync продолжается.
Решение: MailboxCursorUpdate = housekeeping (always allowed кроме hard delete). Чтение нового mailbox content (через MailboxDeposit от sender'ов) = user-facing incoming write, который в read-only фазе RECEIVING стороны rejected. То есть mailbox не получает новые сообщения, но cursor sync для already-received continues.
20.11 Re-encryption read speed во время cold archive
Где: §5.2.3 ↔ §13.3 (day 14-21).
§13.3 day 14-21: «cold archive — reads медленные». §5.2.3: re-encryption job требует read access.
«Медленные reads» — только user-facing через auth-shim, или включая internal jobs? Если включая — re-encryption тормозится в N раз, может не успеть до hard delete (day 28).
Решение: «cold archive» применяется только к user-facing reads через auth-shim (peer requests). Internal jobs (re-encryption, anti-entropy, friend-replication) работают с normal storage performance. Зафиксировать в §13.3.
20.12 Obscure bucket naming — migration scenario
Где: §13.6.
Bucket name = cnt-${blake3(identity_id || creation_nonce)[:16]}. Где хранится creation_nonce?
Migration scenarios:
- User меняет relay-node → новая нода создаёт новый bucket с новым nonce → old bucket → orphan?
- User делает recovery через BIP39 → app генерирует identity_id (deterministic), но creation_nonce — нет → не может найти существующий bucket.
Решение: creation_nonce хранится в Space DHT (как часть Personal Space metadata) — это часть стабильного «identity infrastructure footprint». Migration сохраняет ту же nonce. Recovery восстанавливает nonce из Personal Space при reconnect'е.