dm: define durable storage request identities and scoped source expiry

This commit is contained in:
evgeny committed 2026-11-03 22:29:37 +01:00
1 parent 3d4dd893b6
commit 75ab57798e
3 files changed
+23 -10

No files matched your search

+5 -4
View File
@@ -3,7 +3,8 @@
* Жизненный цикл принадлежит dm_mailbox, control принимает dm_media. Все API в uasync.
* Координатор хранится в dm_media_jobs/dm_media_holders SQLite ядра; таймер — только триггер повторов.
* Таймер 1с обрабатывает до 8 задач, retry_at через 5с; выбранный M сохраняется до STORE.
* Выбор M: storage=yes в общей группе A/B, достижимый узел по node_id, кроме A/B и прежних назначений.
* Выбор M: storage=yes в допущенной общей группе S/M, маршрут до M выбирается независимо.
* assignment_id сохраняется до STORE и не меняется при смене маршрута/рестарте.
* S может одновременно быть M: local control проходит тот же обработчик без router-соединения к себе.
* SQLite и группы принадлежат ядру; instance должен пережить owner. Входные тела заимствованы до возврата.
* Из C++ этот внутренний C API включать внутри extern "C".
@@ -27,14 +28,14 @@ void dm_mailbox_media_destroy(struct dm_mb_media* media);
int dm_mailbox_media_register(struct dm_mb_media* media, uint64_t group, uint64_t recipient,
const uint8_t* msg, size_t len);
/* После проверки router-подписи и размера команды в dm_media: group/source — доверенные поля router.
* body заимствован на время вызова; RECEIPT=152, STORED=24, DELETE_ACK/EXPIRED=16, REJECT=17 байт.
* RECEIPT имеет формат dm_media.h; STORED=[UUID:16][expires:8], ACK/EXPIRED=[UUID:16], REJECT=[UUID:16][reason:1].
* body заимствован на время вызова; RECEIPT=152, STORED/RESERVED=41, DELETE_ACK/EXPIRED=32, REJECT=33 байта.
* Форматы — dm_media.h; каждый ответ M содержит сохранённые assignment_id и UUID.
* STORED/REJECT/ACK относятся к сохранённому назначению M; RECEIPT проверяется отдельно подписью B.
* EXPIRED/REJECT атомарно освобождают назначение и сбрасывают текущий holder; ошибка откатывает оба изменения.
* Таймер завершает stored-назначение по expires даже при offline B/потерянном EXPIRED, также после restart.
* После квитанции повторяется DELETE до 16 неосвобождённым M за проход cleanup.
* Тело удаляется только при отсутствии неосвобождённых назначений; ошибка сканирования откладывает очистку.
* Для неподтверждённого STORE повтор запрашивает результат из custody/tombstone M; квитанция B не отменяется. */
* STORE повторно запрашивает результат из custody/tombstone M, также после STORED; квитанция B не отменяется. */
void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t source,
uint8_t cmd, const uint8_t* body, size_t len);
/* Проверить 152-байтовую квитанцию и попытаться отправить доступным S общей группы.
+10 -5
View File
@@ -20,6 +20,7 @@ struct UTUN_INSTANCE;
#define DM_MEDIA_DESC_SIZE 56 /* UUID:16, plaintext_size:8 native-endian, SHA256(ciphertext):32. */
#define DM_MEDIA_RECEIPT_SIZE 152
#define DM_MEDIA_ASSIGNMENT_SIZE 16 /* Постоянный ID запроса S->M; повтор/смена маршрута сохраняют его. */
#define DM_MEDIA_MAX_SIZE (1024ULL * 1024 * 1024)
#define DM_MEDIA_KEYS_HEADER_SIZE 232 /* "DMKEY001" и DM_FILE_KEYS_SIZE байт. */
@@ -30,12 +31,15 @@ int dm_media_metadata_decode(const uint8_t* data, size_t size, struct attachment
/* ETCP_RT_ID_DM_MEDIA=0x38: [cmd:1][body], router-подпись обязательна; числа native-endian.
* OFFER: [recipient:8][holder:8][expires:8][каноническое file-сообщение]. A предлагает себя с expires=0.
* STORE: [recipient:8][file-сообщение] от S к M; STORED/RESERVED: [UUID:16][expires:8][delete_on_open:1].
* RECEIPT/DELETE: [domain:8][conv:8][seq:8][author:8][device:8][UUID:16][SHA256(message):32][sig:64].
* STORE: [assignment:16][recipient:8][file-сообщение] от S к M.
* STORED/RESERVED: [assignment:16][UUID:16][expires:8][delete_on_open:1].
* RECEIPT: [domain:8][conv:8][seq:8][author:8][device:8][UUID:16][SHA256(message):32][sig:64].
* DELETE: [assignment:16][RECEIPT:152]; назначение не заменяет подпись факта получателя.
* FILE_RECEIVED: DMFILE01/DMFILEG1; TAKEN: DMTAKE01/DMTAKEG1 для DEVICE/GROUPS.
* Устройство подписывает первые88 байт; DELETE переносит проверенный факт, M применяет свою сохранённую политику.
* DELETE_ACK/EXPIRED: [UUID:16]; REJECT: [UUID:16][reason:1], reason=1 — недостаток квоты.
* EXPIRED завершает промежуточное хранение, а не подтверждает доставку B. */
* DELETE_ACK/EXPIRED: [assignment:16][UUID:16]; REJECT: [assignment:16][UUID:16][reason:1], reason=1 — квота.
* SOURCE_GONE: [UUID:16][SHA256(message):32][holder:8] от S; прекращает использование только этого источника.
* EXPIRED относится только к назначению S/M. Получателю не объявляется удаление личного файла. */
#define DM_MEDIA_OFFER 1
#define DM_MEDIA_STORE 2
#define DM_MEDIA_STORED 3
@@ -44,9 +48,10 @@ int dm_media_metadata_decode(const uint8_t* data, size_t size, struct attachment
#define DM_MEDIA_DELETE_ACK 6
#define DM_MEDIA_EXPIRED 7
#define DM_MEDIA_REJECT 8
#define DM_MEDIA_RESERVED 9 /* UUID:16, expires:8, delete_on_open:1; ciphertext ещё не подтверждён. */
#define DM_MEDIA_RESERVED 9 /* assignment:16, UUID:16, expires:8, delete_on_open:1; ciphertext ещё не подтверждён. */
#define DM_MEDIA_HAVE 10 /* UUID:16, SHA256:32, probe_id:8, cipher_size:8. */
#define DM_MEDIA_HAVE_ACK 11 /* Точное эхо HAVE; RTT измеряет запросившее устройство. */
#define DM_MEDIA_SOURCE_GONE 12
/* После dm_core_init с SQLite/config/router/ua и media_async ядра: таблицы, каталоги, file_transfer, bind и таймер.
* 0=успех, -1=ошибка, в том числе повторный init живого owner. При отказе созданные таблицы/каталоги остаются.
+8 -1
View File
@@ -55,12 +55,19 @@ int dm_media_schema_init(struct UTUN_INSTANCE* inst) {
"supernode INTEGER NOT NULL,expires INTEGER NOT NULL,ready INTEGER NOT NULL DEFAULT 0,"
"receipt BLOB,retry_at INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE IF NOT EXISTS dm_custody_done(id BLOB PRIMARY KEY,body_hash BLOB NOT NULL,receipt BLOB);"
"CREATE TABLE IF NOT EXISTS dm_custody_requests(supernode INTEGER NOT NULL,assignment_id BLOB NOT NULL,"
"id BLOB NOT NULL,body_hash BLOB NOT NULL,rejected INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(supernode,assignment_id));"
"CREATE TABLE IF NOT EXISTS dm_media_jobs(id BLOB PRIMARY KEY,author INTEGER NOT NULL,recipient INTEGER NOT NULL,"
"seq INTEGER NOT NULL,body BLOB,body_hash BLOB NOT NULL,holder INTEGER NOT NULL DEFAULT 0,"
"stored INTEGER NOT NULL DEFAULT 0,expires INTEGER NOT NULL DEFAULT 0,expired INTEGER NOT NULL DEFAULT 0,"
"receipt BLOB,retry_at INTEGER NOT NULL DEFAULT 0);"
"CREATE TABLE IF NOT EXISTS dm_media_holders(id BLOB NOT NULL,node_id INTEGER NOT NULL,"
"released INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(id,node_id))")) goto fail;
"assignment_id BLOB NOT NULL,released INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(id,node_id))")) goto fail;
found = schema_has_column(db, "dm_media_holders", "assignment_id");
if (found < 0) goto fail;
if (!found && (schema_exec(db, "ALTER TABLE dm_media_holders ADD COLUMN assignment_id BLOB") ||
schema_exec(db, "UPDATE dm_media_holders SET assignment_id=randomblob(16)"))) goto fail;
if (schema_exec(db, "CREATE UNIQUE INDEX IF NOT EXISTS dm_media_assignment_id ON dm_media_holders(assignment_id)")) goto fail;
const char* tables[] = {"dm_custody", "dm_custody_done", "dm_media_jobs"};
for (unsigned i = 0; i < 3; i++) {
found = schema_has_column(db, tables[i], "delete_on_open");