Kontinuum Node — Database Schemas
SQLite schemas для server-side и admin-side databases. P0 prerequisite перед началом имплементации
state.rs.
Audience: node developers начинающие work на persistence layer.
Связанные документы:
architecture.md— overview / glossaryprotocols.md— wire types, которые персистятсяoperations.md— admin DB используется control-plane'омimplementation-notes.md— edge-cases, influencing schema (transaction_id, pending_propagation)
Обзор
Два разных SQLite DB-файла per node deployment:
| Database | Файл | Управляется |
|---|---|---|
| Server DB | server/data/node.db | kontinuum-node-server процесс |
| Admin DB | admin/data/admin.db | kontinuum-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.
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 для этой ноды.
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.
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).
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).
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'ы.
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).
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).
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).
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).
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 где нода участник.
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).
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).
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).
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).
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).
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.
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).
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.
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:
| Table | Column | Encryption |
|---|---|---|
node_identity | signing_key_encrypted | AEAD(privkey, master_key) — обязательно |
mailbox_entries | encrypted_payload | NOT NEEDED (already E2E by sender) |
member_ops_log | op_payload | NOT NEEDED (signed plaintext, public) |
tenant_whitelist | tenant_identity_id | Optional (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
| Table | Retention | Cleanup mechanism |
|---|---|---|
node_identity | Permanent | None |
cert_log | Permanent (audit) | None |
crl_cache | TTL до ttl_until (60 sec) | Background GC every 1 min |
peers | Evict offline > 7d AND failures > 10 | Background GC every 1h |
dht_records | TTL до expires_at; quarantined > 60 sec → reject | Background GC every 1h + 1 min |
provider_records | Evict если verified_until < now AND blob не в pins | Background GC every 1h |
blob_pins | Delete при пустом pinned_by | On-update trigger |
mailbox_entries | GC по §10.2 + type-based TTL §10.3 | Background GC every 1h |
device_cursors | Keep all | None |
member_ops_log | Append-only, no deletion | None |
space_descriptors | Evict при Space deletion | On-Space-removal |
tenant_whitelist | Удалять при revoke | On-revoke |
tenant_quotas | Один актуальный per (host, tenant) | On-update replace |
replication_jobs | Удалять completed после 7d | Background GC every 1h |
challenge_scores | Keep all (small) | None |
reencryption_jobs | Удалять completed после 7d | Background GC every 1h |
billing_events | Keep 1 year, then archive | Yearly archive job |
partner_nodes | Permanent | None |
admin_audit_log | Permanent (compliance) | None |
Capacity planning
Estimated row sizes
| Table | Avg row size | Notes |
|---|---|---|
mailbox_entries | ~5 KB | Зависит от payload (до 1 MB hard cap) |
dht_records | ~0.5 KB | Signed record CBOR |
member_ops_log | ~0.3 KB | |
provider_records | ~0.2 KB | |
billing_events | ~2 KB | JSON payload |
Typical Tier 1 single-tenant scenario
User serving 1 identity с 50 paired contacts, ~100 active Spaces:
| Table | Rows estimate | Storage |
|---|---|---|
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
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. - [ ] Опционально
sqlcipherfeature forrusqlite(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
- SQLCipher vs column-level AEAD — measure performance impact в prototype. Если SQLCipher < 20% slowdown — go with whole-DB. Иначе — column-level.
- Архивация старых
billing_events— куда? S3 bucket? Cold-storage on disk? Зависит от compliance requirements. - Sharded DHT migration (v2.0+) — потребует schema-level изменений в
dht_records(sub-overlay attribute). Спланировать backward-compat сейчас vs migrate later. - Multi-DB вместо single — стоит ли split
dht_recordsпо namespace в отдельные DB files для performance? Decision deferred — measure first.