From 75ab57798edd472b64d9701afe540c86c0f29c4d Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 22:29:37 +0100 Subject: [PATCH] dm: define durable storage request identities and scoped source expiry --- src/dm/dm_mailbox_media.h | 9 +++++---- src/dm/dm_media.h | 15 ++++++++++----- src/dm/dm_schema.c | 9 ++++++++- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/dm/dm_mailbox_media.h b/src/dm/dm_mailbox_media.h index 30d41da27..857bb71d6 100644 --- a/src/dm/dm_mailbox_media.h +++ b/src/dm/dm_mailbox_media.h @@ -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 общей группы. diff --git a/src/dm/dm_media.h b/src/dm/dm_media.h index 25b27abdc..9fd296e1b 100644 --- a/src/dm/dm_media.h +++ b/src/dm/dm_media.h @@ -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. При отказе созданные таблицы/каталоги остаются. diff --git a/src/dm/dm_schema.c b/src/dm/dm_schema.c index b3f3e5ed7..ba4a2b64f 100644 --- a/src/dm/dm_schema.c +++ b/src/dm/dm_schema.c @@ -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");