From 25a8b905fc80357654570bb026b4c8ea90fe6d38 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 18:25:36 +0100 Subject: [PATCH] Accept verified DM originals after storage expiry --- src/dm/dm_media.c | 6 +++++- tests/test_dm_media.c | 47 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/dm/dm_media.c b/src/dm/dm_media.c index 2705d9cc..44918862 100644 --- a/src/dm/dm_media.c +++ b/src/dm/dm_media.c @@ -581,7 +581,8 @@ static void dm_file_done(void* arg, int error) { uint64_t expires = rc == SQLITE_ROW ? (uint64_t)sqlite3_column_int64(st, 4) : 0; if (len <= sizeof(body) && len) memcpy(body, sqlite3_column_blob(st, 0), len); sqlite3_finalize(st); - if (ready < 0 || !len || len > sizeof(body) || (custody && expires <= ntp_time_get_seconds(inst))) { + /* EXPIRED завершает копию M; endpoint может принять проверенный оригинал в актуальном поколении. */ + if (!len || len > sizeof(body) || (custody && (ready < 0 || expires <= ntp_time_get_seconds(inst)))) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_media: completed task no longer admissible custody=%d ready=%d expiry=%llu", custody, ready, (unsigned long long)expires); goto fail; @@ -603,6 +604,9 @@ static void dm_file_done(void* arg, int error) { if (!custody) sqlite3_bind_blob(st, idx++, receipt, sizeof(receipt), SQLITE_STATIC); sqlite3_bind_blob(st, idx, job->id, 16, SQLITE_STATIC); if (dm_media_step(inst, st) || sqlite3_changes(inst->topo_sqlite_db) != 1) goto fail; + if (!custody && ready < 0) + DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_media: recovered expired file conv=%s generation=%llu bytes=%llu", + job->conv, (unsigned long long)job->generation, (unsigned long long)job->size); if (custody) { uint8_t stored[24]; memcpy(stored, job->id, 16); memcpy(stored + 16, &expires, 8); dm_media_send(inst, group, peer, DM_MEDIA_STORED, stored, sizeof(stored)); diff --git a/tests/test_dm_media.c b/tests/test_dm_media.c index 39b73ed2..0e91fa31 100644 --- a/tests/test_dm_media.c +++ b/tests/test_dm_media.c @@ -1,6 +1,6 @@ /* test_dm_media — реальные A–S–B и S–M, независимые роли supernode/storage. * Прямая доставка, offline custody, restart S/M при выключенном A, TTL, - * квота, точные квитанции, поздние STORE/STORED и отзыв активных операций. + * квота, точные квитанции, поздние STORE/STORED, возврат оригинала после EXPIRED и отзыв активных операций. */ #include #include @@ -45,6 +45,7 @@ struct media_test { uint8_t body[2048], id[16], receipt[DM_MEDIA_RECEIPT_SIZE]; size_t body_len; unsigned phase, ticks, heartbeats, work_ticks; + uint64_t recovery_start; int failed; void* timer; }; @@ -604,13 +605,49 @@ static void tick(void* arg) { DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM expiry test: release checks=%u blocked=%u lost EXPIRED=%u restart=1 recovered PASS", release_checks, expiry_blocks, expiry_notices); #endif + utun_instance_destroy(a); t->nodes[A] = NULL; + if (start_node(t, B)) { failure(t, "receiver return after TTL"); return; } t->phase++; - uasync_stop(t->ua); return; + } + break; + case 9: + if (value(b, "SELECT count(*) FROM dm_files WHERE dir=0 AND seq=3 AND ready=-1 AND receipt IS NULL") == 1) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM recovery test: B received EXPIRED while A offline; returning original author"); + if (start_node(t, A)) { failure(t, "source return after EXPIRED"); return; } + t->phase++; + } + break; + case 10: + if (live(a, t->ids[B]) && live(b, t->ids[A])) { + t->recovery_start = ntp_time_get_seconds(b); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM recovery test: A/B routes restored; waiting for verified original"); + t->phase++; + } + break; + case 11: + if (file_correct(t) && value(a, "SELECT count(*) FROM dm_files WHERE dir=1 AND seq=3 AND receipt IS NOT NULL") == 1 && + value(s, "SELECT count(*) FROM dm_media_jobs WHERE seq=3 AND receipt IS NOT NULL AND body IS NULL") == 1) { + if (dm_media_verify_receipt(a, t->receipt, t->body, t->body_len)) { + failure(t, "recovered file receipt does not match original body"); return; + } + control(t, B, t->ids[S], DM_MEDIA_EXPIRED, t->id, 16); + if (!file_correct(t) || value(m, "SELECT count(*) FROM dm_custody") || + value(b, "SELECT count(*) FROM dm_files WHERE dir=0 AND seq=3 AND ready=1 AND receipt IS NOT NULL") != 1) { + failure(t, "late EXPIRED changed verified file or revived custody"); return; + } + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "DM recovery test: exact original, signed receipt, S cleanup, late EXPIRED PASS"); + t->phase++; uasync_stop(t->ua); return; + } + if (ntp_time_get_seconds(b) >= t->recovery_start + 16) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM recovery test: timeout A_to_B=%d B_to_A=%d B_ready=%d B_receipt=%d", + live(a, t->ids[B]), live(b, t->ids[A]), value(b, "SELECT ready FROM dm_files WHERE dir=0 AND seq=3"), + value(b, "SELECT count(*) FROM dm_files WHERE dir=0 AND seq=3 AND receipt IS NOT NULL")); + failure(t, "available original cannot complete receive after EXPIRED"); return; } break; } if (t->failed) return; - if (t->ticks > 6000) { failure(t, "timeout"); return; } + if (t->ticks > 9000) { failure(t, "timeout"); return; } t->timer = uasync_set_timeout(t->ua, 100, t, tick, "dm_media_test"); } @@ -666,6 +703,6 @@ int main(void) { printf("DM media allocations: %zu->%zu\n", allocations, u_get_allocated_count()); if (allocations != u_get_allocated_count()) t.failed = 1; printf("test_dm_media: phase=%u heartbeats=%u %s artifacts=%s\n", t.phase, t.heartbeats, - !t.failed && t.phase == 9 ? "PASS" : "FAIL", t.dir); - return t.failed || t.phase != 9; + !t.failed && t.phase == 12 ? "PASS" : "FAIL", t.dir); + return t.failed || t.phase != 12; }