Skip to content

Kontinuum Node — Database Schemas

SQLite schemas для server-side и admin-side databases. P0 prerequisite перед началом имплементации state.rs.

Audience: node developers начинающие work на persistence layer.

Связанные документы:


Обзор

Два разных SQLite DB-файла per node deployment:

DatabaseФайлУправляется
Server DBserver/data/node.dbkontinuum-node-server процесс
Admin DBadmin/data/admin.dbkontinuum-node-admin процесс

Технологии:

  • SQLite через rusqlite 0.38 (bundled — без system-зависимости).
  • Migrations через rusqlite_migration 1.0 (forward-only, обе DB).
  • Encryption at rest — SQLCipher для Server DB; filesystem-level (LUKS / dm-crypt) для Admin DB.
  • WAL mode включён для concurrent reads (PRAGMA journal_mode=WAL).
  • Connection pool через r2d2 (max 10 connections per process, configurable).

Server DB schema

node_identity (singleton)

Идентичность ноды + текущее состояние cert.

sql
CREATE TABLE node_identity (
    id                      INTEGER PRIMARY KEY CHECK (id = 1),
    node_id                 BLOB NOT NULL UNIQUE,         -- 32 bytes blake3(pubkey)
    pubkey                  BLOB NOT NULL,                -- 32 bytes Ed25519
    signing_key_encrypted   BLOB NOT NULL,                -- AEAD(privkey, master_key)
    created_at              INTEGER NOT NULL              -- Unix epoch sec
);

Retention: permanent. Encrypted at column level через master_key (Argon2id from BIP39 — см. kontinuum-app::vault::recovery).

cert_log

История всех cert'ов выпущенных Tier 0 для этой ноды.

sql
CREATE TABLE cert_log (
    cert_id                 BLOB PRIMARY KEY,              -- blake3(cert body)
    node_id                 BLOB NOT NULL,
    tier                    INTEGER NOT NULL,              -- 0 | 1 | 2
    tenancy                 TEXT NOT NULL,                 -- 'multi' | 'single'
    operator                TEXT NOT NULL,                 -- 'org' | 'byo:vps' | 'byo:home'
    served_identity         BLOB,                          -- NULL для multi-tenant
    capabilities            TEXT NOT NULL,                 -- JSON array
    capacity_storage_bytes  INTEGER NOT NULL,
    capacity_dht_records    INTEGER NOT NULL,
    capacity_bandwidth_mbps INTEGER NOT NULL,
    issued_at               INTEGER NOT NULL,
    valid_until             INTEGER NOT NULL,
    tier0_pubkey            BLOB NOT NULL,
    signature               BLOB NOT NULL,
    state                   TEXT NOT NULL,                 -- 'valid' | 'revoked' | 'expired' | 'degraded'
    revoked_at              INTEGER
);

CREATE INDEX idx_cert_state ON cert_log(state);
CREATE INDEX idx_cert_valid_until ON cert_log(valid_until);

Retention: permanent (audit trail).

crl_cache

Cached Certificate Revocation List entries from Tier 0. TTL'd cache.

sql
CREATE TABLE crl_cache (
    revoked_node_id         BLOB PRIMARY KEY,
    reason                  TEXT,
    revoked_at              INTEGER NOT NULL,
    tier0_pubkey            BLOB NOT NULL,
    signature               BLOB NOT NULL,
    ttl_until               INTEGER NOT NULL               -- max 60 sec from receive
);

CREATE INDEX idx_crl_ttl ON crl_cache(ttl_until);

Retention: TTL до ttl_until (60 sec); background GC every 1 min.

peers

Discovered peers cache (через DHT FindNode / NodeHello).

sql
CREATE TABLE peers (
    peer_id                 TEXT PRIMARY KEY,              -- libp2p PeerId (base58)
    node_id                 BLOB,                          -- nullable до handshake
    multiaddrs              TEXT NOT NULL,                 -- JSON array of multiaddr strings
    tier                    INTEGER,
    capabilities            TEXT,                          -- JSON array
    geo_zone                TEXT,
    last_seen               INTEGER NOT NULL,
    consecutive_failures    INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_peers_last_seen ON peers(last_seen);
CREATE INDEX idx_peers_tier ON peers(tier);

Retention: evict при last_seen < now - 7d AND consecutive_failures > 10.

dht_records

Per-namespace key-value storage для всех DHT shards (Global + Personal + Shared spaces).

sql
CREATE TABLE dht_records (
    dht_namespace           BLOB NOT NULL,                 -- 32 bytes DhtId
    record_key              BLOB NOT NULL,                 -- 32 bytes
    signed_record           BLOB NOT NULL,                 -- CBOR-encoded Signed<X>
    record_type             TEXT NOT NULL,                 -- 'node' | 'identity' | 'crl' | 'pub-space' | 'cnt' | 'member-op' | ...
    transaction_id          BLOB,                          -- atomic-group marker (§20.1)
    transaction_total       INTEGER,
    transaction_index       INTEGER,
    received_at             INTEGER NOT NULL,
    expires_at              INTEGER,                       -- TTL, NULL = permanent
    applied                 INTEGER NOT NULL DEFAULT 0,    -- 0 = quarantined, 1 = applied
    PRIMARY KEY (dht_namespace, record_key)
);

CREATE INDEX idx_dht_namespace ON dht_records(dht_namespace);
CREATE INDEX idx_dht_expires ON dht_records(expires_at);
CREATE INDEX idx_dht_pending ON dht_records(applied) WHERE applied = 0;
CREATE INDEX idx_dht_transaction ON dht_records(transaction_id) WHERE transaction_id IS NOT NULL;

Retention: TTL до expires_at. Pending (applied=0) старше 60 sec → reject (cleanup every 1 min).

provider_records

cnt:{blob_hash} → [providers] — где физически лежат blob'ы.

sql
CREATE TABLE provider_records (
    blob_hash               BLOB NOT NULL,
    provider_node_id        BLOB NOT NULL,
    region                  TEXT NOT NULL,
    provider_kind           TEXT NOT NULL,                 -- 'LocalRelayFree' | 'OrgPaid' | 'OrgPaidWithCDN' | 'ExternalPRO' | 'ExternalPROwithCDN' | 'ExternalDirect'
    stored_at               INTEGER NOT NULL,
    verified_until          INTEGER NOT NULL,              -- PoS confirm expiry
    signature               BLOB NOT NULL,
    bucket_url              TEXT,                          -- для ExternalDirect (redirect-mode §13.6)
    PRIMARY KEY (blob_hash, provider_node_id)
);

CREATE INDEX idx_provider_verified ON provider_records(verified_until);
CREATE INDEX idx_provider_blob ON provider_records(blob_hash);

Retention: evict при verified_until < now AND blob_hash не в blob_pins.

blob_pins

User-requested pins для гарантированной репликации (§9.5).

sql
CREATE TABLE blob_pins (
    blob_hash               BLOB PRIMARY KEY,
    target_rf               INTEGER NOT NULL,              -- replication factor target
    paid_until              INTEGER NOT NULL,
    pinned_by               TEXT NOT NULL,                 -- JSON array of identity_ids
    pin_target              TEXT NOT NULL                  -- 'blob' | 'space'
);

CREATE INDEX idx_pin_paid_until ON blob_pins(paid_until);

Retention: delete при пустой pinned_by (никто больше не pin'ит).

mailbox_entries

Per-identity inbox log (см. protocols.md §10.1).

sql
CREATE TABLE mailbox_entries (
    identity_id             BLOB NOT NULL,
    seqno                   INTEGER NOT NULL,
    enqueued_at             INTEGER NOT NULL,
    msg_type                TEXT NOT NULL,                 -- 'Publish' | 'Share' | 'Ack' | 'FileChunk' | ...
    encrypted_payload       BLOB NOT NULL,                 -- already E2E encrypted by sender
    sender_signature        BLOB NOT NULL,
    ttl_override            INTEGER,                       -- per-type TTL override
    delivered_to_devices    TEXT NOT NULL DEFAULT '[]',    -- JSON array of device_ids
    PRIMARY KEY (identity_id, seqno)
);

CREATE INDEX idx_mailbox_enqueued ON mailbox_entries(enqueued_at);
CREATE INDEX idx_mailbox_identity ON mailbox_entries(identity_id);

Retention: GC по §10.2 — все live cursors passed seqno ИЛИ entry older than type-based TTL (§10.3). Background job every 1h.

device_cursors

Per-device read positions в mailbox (см. protocols.md §10.1).

sql
CREATE TABLE device_cursors (
    device_id               BLOB NOT NULL,
    identity_id             BLOB NOT NULL,
    last_seen_seqno         INTEGER NOT NULL,
    last_heartbeat          INTEGER NOT NULL,
    device_signature        BLOB NOT NULL,                 -- device-signed cursor (anti-forge)
    PRIMARY KEY (device_id, identity_id)
);

CREATE INDEX idx_cursor_identity ON device_cursors(identity_id);
CREATE INDEX idx_cursor_heartbeat ON device_cursors(last_heartbeat);

Retention: keep all (small). Cursor «застывает» через 7d offline, не блокирует GC, но запись остаётся.

member_ops_log

Append-only Space DHT membership history (см. protocols.md §5.2.1).

sql
CREATE TABLE member_ops_log (
    space_id                BLOB NOT NULL,
    op_seqno                INTEGER NOT NULL,
    op_type                 TEXT NOT NULL,                 -- 'Add' | 'Revoke' | 'KeyRotation' | 'RoleChange' | 'SelfLeave'
    op_payload              BLOB NOT NULL,                 -- CBOR-encoded MemberOp
    transaction_id          BLOB,                          -- atomic-group marker
    transaction_total       INTEGER,
    transaction_index       INTEGER,
    signed_by               BLOB NOT NULL,
    signature               BLOB NOT NULL,
    applied_at              INTEGER,                       -- NULL = quarantined
    pending_propagation     INTEGER NOT NULL DEFAULT 0,    -- local-only flag (§20.1, §5.2.2 degraded mode)
    PRIMARY KEY (space_id, op_seqno)
);

CREATE INDEX idx_memberops_space ON member_ops_log(space_id);
CREATE INDEX idx_memberops_pending ON member_ops_log(applied_at) WHERE applied_at IS NULL;
CREATE INDEX idx_memberops_transaction ON member_ops_log(transaction_id) WHERE transaction_id IS NOT NULL;

Retention: append-only, no deletion (membership history — security audit trail).

space_descriptors

Метаданные Spaces где нода участник.

sql
CREATE TABLE space_descriptors (
    space_id                BLOB PRIMARY KEY,
    space_dht_id            BLOB NOT NULL UNIQUE,
    creator_identity        BLOB NOT NULL,
    space_kind              TEXT NOT NULL,                 -- 'personal' | 'shared' | 'direct'
    current_key_version     INTEGER NOT NULL DEFAULT 1,
    creation_nonce          BLOB NOT NULL,                 -- для bucket naming (§20.12)
    created_at              INTEGER NOT NULL,
    replication_set         TEXT NOT NULL                  -- JSON array of node_ids
);

CREATE INDEX idx_space_kind ON space_descriptors(space_kind);

Retention: evict при Space удалении (owner SelfLeave + RF=0 across network).

tenant_whitelist

Family-mode access control (§7.3).

sql
CREATE TABLE tenant_whitelist (
    host_node_id            BLOB NOT NULL,                 -- наша нода
    tenant_identity_id      BLOB NOT NULL,
    added_at                INTEGER NOT NULL,
    PRIMARY KEY (host_node_id, tenant_identity_id)
);

Retention: удаляется при revoke owner'ом host'a.

tenant_quotas

Per-tenant квоты (§20.7 — signed by host owner для tenant observability).

sql
CREATE TABLE tenant_quotas (
    host_node_id            BLOB NOT NULL,
    tenant_identity_id      BLOB NOT NULL,
    max_storage_bytes       INTEGER NOT NULL,
    max_mailbox_bytes       INTEGER NOT NULL,
    max_mailbox_messages    INTEGER NOT NULL,
    effective_at            INTEGER NOT NULL,
    signature               BLOB NOT NULL,                 -- signed by host owner
    PRIMARY KEY (host_node_id, tenant_identity_id)
);

Retention: одна актуальная row per (host, tenant); old versions архивируются в tenant_quota_history (TODO if needed).

replication_jobs

Pending replication tasks (для repair-loop §9.6).

sql
CREATE TABLE replication_jobs (
    blob_hash               BLOB NOT NULL,
    target_node_id          BLOB NOT NULL,
    scheduled_at            INTEGER NOT NULL,
    attempts                INTEGER NOT NULL DEFAULT 0,
    last_attempt_at         INTEGER,
    last_error              TEXT,
    completed_at            INTEGER,                       -- NULL = pending
    PRIMARY KEY (blob_hash, target_node_id)
);

CREATE INDEX idx_repl_pending ON replication_jobs(completed_at, scheduled_at) WHERE completed_at IS NULL;

Retention: удалять через 7d после completed_at.

challenge_scores

Reputation tracking для PoS challenges (Tier 2 verification).

sql
CREATE TABLE challenge_scores (
    peer_node_id            BLOB PRIMARY KEY,
    score                   INTEGER NOT NULL DEFAULT 0,
    total_challenges        INTEGER NOT NULL DEFAULT 0,
    failed_challenges       INTEGER NOT NULL DEFAULT 0,
    last_challenge_at       INTEGER,
    last_update_at          INTEGER NOT NULL
);

CREATE INDEX idx_scores_score ON challenge_scores(score);

Retention: keep all (small dataset, value over time).

reencryption_jobs

Background re-encryption tasks после Revoke (§5.2.3).

sql
CREATE TABLE reencryption_jobs (
    space_id                BLOB NOT NULL,
    blob_hash               BLOB NOT NULL,
    old_key_version         INTEGER NOT NULL,
    new_key_version         INTEGER NOT NULL,
    scheduled_at            INTEGER NOT NULL,
    completed_at            INTEGER,                       -- NULL = pending
    error                   TEXT,
    PRIMARY KEY (space_id, blob_hash, old_key_version)
);

CREATE INDEX idx_reenc_pending ON reencryption_jobs(scheduled_at) WHERE completed_at IS NULL;

Retention: удалять completed после 7d (audit trail short).


Admin DB schema

billing_events

Webhooks log от Kontinuum Billing.

sql
CREATE TABLE billing_events (
    event_id                TEXT PRIMARY KEY,              -- idempotency key
    event_type              TEXT NOT NULL,                 -- 'subscription.created' | '.renewed' | '.cancelled' | '.lapsed'
    subscription_id         TEXT NOT NULL,
    node_id                 BLOB,
    identity_id             BLOB,
    payload_json            TEXT NOT NULL,
    received_at             INTEGER NOT NULL,
    processed_at            INTEGER,                       -- NULL = pending
    error                   TEXT
);

CREATE INDEX idx_billing_pending ON billing_events(processed_at) WHERE processed_at IS NULL;
CREATE INDEX idx_billing_subscription ON billing_events(subscription_id);

Retention: keep 1 year, then archive to cold storage.

partner_nodes

Partner-operated nodes registry (org-internal).

sql
CREATE TABLE partner_nodes (
    node_id                 BLOB PRIMARY KEY,
    partner_account_id      TEXT NOT NULL,
    partner_contact_email   TEXT,
    partner_region          TEXT,
    onboarded_at            INTEGER NOT NULL,
    revenue_share_pct       REAL NOT NULL DEFAULT 0,
    status                  TEXT NOT NULL                  -- 'active' | 'suspended' | 'terminated'
);

CREATE INDEX idx_partner_status ON partner_nodes(status);

Retention: permanent (audit, business records).

admin_audit_log

Все actions выполненные через admin REST API / operator chat bot / Directus.

sql
CREATE TABLE admin_audit_log (
    audit_id                INTEGER PRIMARY KEY AUTOINCREMENT,
    admin_identity          TEXT NOT NULL,                 -- org operator identity
    action                  TEXT NOT NULL,                 -- 'cert.issue' | 'cert.revoke' | 'node.drain' | ...
    target_resource         TEXT,                          -- node_id, cert_id, etc.
    payload_json            TEXT,
    ip_address              TEXT,
    timestamp               INTEGER NOT NULL
);

CREATE INDEX idx_audit_admin ON admin_audit_log(admin_identity);
CREATE INDEX idx_audit_timestamp ON admin_audit_log(timestamp);
CREATE INDEX idx_audit_action ON admin_audit_log(action);

Retention: permanent (compliance + security audit).


Migration framework

Структура

kontinuum-node/
├── server/
│   └── migrations/
│       ├── v001_initial.sql
│       ├── v002_*.sql
│       └── ...
└── admin/
    └── migrations/
        ├── v001_initial.sql
        ├── v002_*.sql
        └── ...

Каждая migration — pure SQL, forward-only в production. Rollback testable в dev через separate down/v00X.sql (не используется в prod).

v001_initial.sql

Создаёт все P0 tables этого документа. Run at first node startup (idempotent через CREATE TABLE IF NOT EXISTS или migration-tracking table).

Compatibility policy

  • Drop column — forbidden в production (создаём новую table, копируем, переименовываем).
  • Add column — OK, обязательно с DEFAULT или nullable.
  • Add index — OK.
  • Change column type — через rename + new column + copy.

Multi-version coexistence

Нода работает только с одной schema version. Wire-protocol version negotiation между нодами разных schema versions — через NodeHello.protocol_version (§11a). Несовместимые — reject.


Encryption at rest

Server DB

SQLCipher для transparent whole-DB encryption. Master key — derived from BIP39 (Argon2id), persisted in node's vault (filesystem, chmod 600).

Если SQLCipher impacts performance > 20% (measure during prototype) — fallback на column-level AEAD для sensitive columns:

TableColumnEncryption
node_identitysigning_key_encryptedAEAD(privkey, master_key) — обязательно
mailbox_entriesencrypted_payloadNOT NEEDED (already E2E by sender)
member_ops_logop_payloadNOT NEEDED (signed plaintext, public)
tenant_whitelisttenant_identity_idOptional (privacy concern если DB stolen)

Admin DB

Filesystem-level encryption (LUKS / dm-crypt) — admin.db менее sensitive (org-internal data, no user data). Disk-encryption sufficient.


Retention summary

TableRetentionCleanup mechanism
node_identityPermanentNone
cert_logPermanent (audit)None
crl_cacheTTL до ttl_until (60 sec)Background GC every 1 min
peersEvict offline > 7d AND failures > 10Background GC every 1h
dht_recordsTTL до expires_at; quarantined > 60 sec → rejectBackground GC every 1h + 1 min
provider_recordsEvict если verified_until < now AND blob не в pinsBackground GC every 1h
blob_pinsDelete при пустом pinned_byOn-update trigger
mailbox_entriesGC по §10.2 + type-based TTL §10.3Background GC every 1h
device_cursorsKeep allNone
member_ops_logAppend-only, no deletionNone
space_descriptorsEvict при Space deletionOn-Space-removal
tenant_whitelistУдалять при revokeOn-revoke
tenant_quotasОдин актуальный per (host, tenant)On-update replace
replication_jobsУдалять completed после 7dBackground GC every 1h
challenge_scoresKeep all (small)None
reencryption_jobsУдалять completed после 7dBackground GC every 1h
billing_eventsKeep 1 year, then archiveYearly archive job
partner_nodesPermanentNone
admin_audit_logPermanent (compliance)None

Capacity planning

Estimated row sizes

TableAvg row sizeNotes
mailbox_entries~5 KBЗависит от payload (до 1 MB hard cap)
dht_records~0.5 KBSigned record CBOR
member_ops_log~0.3 KB
provider_records~0.2 KB
billing_events~2 KBJSON payload

Typical Tier 1 single-tenant scenario

User serving 1 identity с 50 paired contacts, ~100 active Spaces:

TableRows estimateStorage
mailbox_entries~5 000~25 MB
dht_records~5 000~2.5 MB
member_ops_log~10 000~3 MB
provider_records~50 000~10 MB
peers~1 000~0.5 MB
Other< 1 MB-
Total DB-~40 MB

Заметно меньше чем blob storage (~10 GB cap из §7.2). DB не bottleneck — blob storage является.

Growth projection

При 10x users (PRO multi-tenant family с 10 семьями × 5 устройств) — ~400 MB DB. Всё ещё OK.


Connection pooling

r2d2 setup

rust
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;

let manager = SqliteConnectionManager::file("server/data/node.db")
    .with_init(|conn| {
        conn.execute_batch("
            PRAGMA journal_mode = WAL;
            PRAGMA synchronous = NORMAL;
            PRAGMA foreign_keys = ON;
            PRAGMA busy_timeout = 5000;
        ")
    });

let pool: Pool<SqliteConnectionManager> = r2d2::Pool::builder()
    .max_size(10)              // configurable via node.toml
    .build(manager)?;

Concurrency model

  • WAL mode → multiple readers + 1 writer concurrent.
  • Writer transactions через single Arc<Mutex<Connection>> или короткие pool.get() + commit.
  • Long-running queries (GC jobs, scans) — отдельный pool / низкий priority.

Implementation checklist

Для starting persistence layer:

  • [ ] Добавить rusqlite_migration = "1.0", r2d2, r2d2_sqlite в server/Cargo.toml.
  • [ ] Опционально sqlcipher feature for rusqlite (test performance impact).
  • [ ] Создать server/migrations/v001_initial.sql со всеми Server DB tables.
  • [ ] Создать admin/migrations/v001_initial.sql со всеми Admin DB tables.
  • [ ] Implement state::AppState с pool handles обеих DB.
  • [ ] Implement encryption layer для signing_key_encrypted (минимум).
  • [ ] Smoke test: первый run создаёт все tables; повторный run — no-op.
  • [ ] Smoke test: migration version bumping работает.

Open implementation questions

  1. SQLCipher vs column-level AEAD — measure performance impact в prototype. Если SQLCipher < 20% slowdown — go with whole-DB. Иначе — column-level.
  2. Архивация старых billing_events — куда? S3 bucket? Cold-storage on disk? Зависит от compliance requirements.
  3. Sharded DHT migration (v2.0+) — потребует schema-level изменений в dht_records (sub-overlay attribute). Спланировать backward-compat сейчас vs migrate later.
  4. Multi-DB вместо single — стоит ли split dht_records по namespace в отдельные DB files для performance? Decision deferred — measure first.