From 021c7902a568f596d20283cda3637c255a8b833f Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:07:06 -0400 Subject: [PATCH 1/3] fix(storage): a silent set-mate cannot hold the mirror sync; a cell read asks every seat at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node side: - The set client (set_client::pinned_set_client) had no connect or request timeout, and mirror_sync holds a node-wide one-at-a-time lock. A set-mate that accepted the connection and never answered held the sync and the lock open indefinitely, stalling every later mirror sync on the node. - It now connects within SET_MATE_CONNECT_TIMEOUT (5 s) and answers within SET_MATE_REQUEST_TIMEOUT (10 s), or the fetch fails, as an unreachable set-mate does: BAD_GATEWAY, with the lock released. These are transport liveness bounds; a fetch that times out stores nothing and orders nothing. - MAX_SYNC_CYCLES drops from 1024 to 128, so one sync of a member far behind stays well inside the SDK's 30 s per request. Every cycle is kept as it is fetched, so later syncs continue. SDK side: route_seats::read_cell asked the five seats one after another, to read values, to close cycles and to sync mirrors. A seat that does not answer cost one client timeout per seat in each loop. The three fan-outs now run at once (join_all), with the answers kept in route order, so the evidence Core evaluates is unchanged. Test: bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it (a set-mate that accepts and never answers; two syncs each answer BAD_GATEWAY within 3× the request bound). Mutation control: the set-client timeouts removed → red ("sync 0 hung on a set-mate that never answers"), restored. Release, on Postgres: bytecommit_chain 8/0. route_seats, faucet flows and the SoFi trade and pay node e2e tests 21/0. fmt, clippy -D warnings (dsm_sdk, dsm_storage_node), the real-code guard and the clockless gate are clean. --- .../dsm_sdk/src/sdk/route_seats.rs | 37 +++++++++++-------- .../src/api/objects/bytecommit.rs | 7 +++- dsm_storage_node/src/set_client.rs | 17 ++++++++- dsm_storage_node/tests/bytecommit_chain.rs | 33 +++++++++++++++++ 4 files changed, 75 insertions(+), 19 deletions(-) diff --git a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_seats.rs b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_seats.rs index 95dc8cfc9..eb3645deb 100644 --- a/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_seats.rs +++ b/dsm_client/deterministic_state_machine/dsm_sdk/src/sdk/route_seats.rs @@ -568,24 +568,29 @@ async fn committed_at( /// (§9 route chains, rule 4). pub async fn read_cell(seats: &S, cell: &RoutedCell) -> CellEvidence { let (route, namespace, key) = (cell.route(), cell.namespace(), cell.key()); - let mut values = Vec::with_capacity(route.seats().len()); - for seat in route.seats() { - values.push(seats.read_values(seat, namespace, key).await); - } - let mut cycles = Vec::with_capacity(values.len()); - for (seat, held) in route.seats().iter().zip(&values) { - let holds_any = held.as_ref().is_some_and(|v| !v.is_empty()); - cycles.push(if holds_any { - seats.close(seat).await - } else { - None - }); - } + // Every seat is asked at once, and the answers stay in route order: a + // seat that does not answer costs one timeout, not one per seat. + let values: Vec>>> = futures::future::join_all( + route + .seats() + .iter() + .map(|seat| seats.read_values(seat, namespace, key)), + ) + .await; + let cycles: Vec> = + futures::future::join_all(route.seats().iter().zip(&values).map( + |(seat, held)| async move { + if held.as_ref().is_some_and(|v| !v.is_empty()) { + seats.close(seat).await + } else { + None + } + }, + )) + .await; let members = seats.members(); if cycles.iter().any(Option::is_some) { - for member in &members { - seats.sync_mirror(member).await; - } + futures::future::join_all(members.iter().map(|member| seats.sync_mirror(member))).await; } let leader = route.leader(); let leader_cycle = cycles.first().copied().flatten(); diff --git a/dsm_storage_node/src/api/objects/bytecommit.rs b/dsm_storage_node/src/api/objects/bytecommit.rs index 461f778a2..f034d1528 100644 --- a/dsm_storage_node/src/api/objects/bytecommit.rs +++ b/dsm_storage_node/src/api/objects/bytecommit.rs @@ -44,8 +44,11 @@ use dsm::utils::text_id; const CYCLE_HEADER: &str = "x-cycle"; const ECHO_HEADER: &str = "x-dsm-node-id"; /// Upper bound on cycles fetched from one member in one sync, so one request -/// does bounded work; a later sync continues where this one stopped. -const MAX_SYNC_CYCLES: u64 = 1024; +/// does bounded work; a later sync continues where this one stopped, since +/// every cycle is kept as it is fetched. At this bound one sync of a member +/// far behind is 128 fetches, well inside what a client waits for one +/// request (the SDK's member client waits 30 s). +const MAX_SYNC_CYCLES: u64 = 128; pub fn create_router(state: Arc) -> Router<()> { Router::new() diff --git a/dsm_storage_node/src/set_client.rs b/dsm_storage_node/src/set_client.rs index 4023a72bf..c5be19ccc 100644 --- a/dsm_storage_node/src/set_client.rs +++ b/dsm_storage_node/src/set_client.rs @@ -11,6 +11,17 @@ use rustls::pki_types::pem::PemObject; use rustls::pki_types::CertificateDer; use rustls::{ClientConfig, RootCertStore}; use std::sync::Arc; +use std::time::Duration; + +/// How long a node waits to connect to a set-mate, and for one answer from +/// it. A set-mate that accepts a connection and never answers fails the +/// fetch within these bounds instead of holding the mirror sync, and with it +/// the node's one-at-a-time sync lock, open. They are transport liveness +/// bounds, as an unreachable set-mate is: a fetch that times out stores +/// nothing and orders nothing, so no protocol fact depends on them (storage +/// spec §1 rule 4). +pub const SET_MATE_CONNECT_TIMEOUT: Duration = Duration::from_secs(5); +pub const SET_MATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); /// A client pinned to `set_ca_pem`, the storage set's CA certificate. pub fn pinned_set_client(set_ca_pem: &[u8]) -> anyhow::Result { @@ -27,7 +38,11 @@ pub fn pinned_set_client(set_ca_pem: &[u8]) -> anyhow::Result { .with_root_certificates(root_store) .with_no_client_auth(); - Ok(Client::builder().use_preconfigured_tls(config).build()?) + Ok(Client::builder() + .use_preconfigured_tls(config) + .connect_timeout(SET_MATE_CONNECT_TIMEOUT) + .timeout(SET_MATE_REQUEST_TIMEOUT) + .build()?) } #[cfg(test)] diff --git a/dsm_storage_node/tests/bytecommit_chain.rs b/dsm_storage_node/tests/bytecommit_chain.rs index 1926afd68..361cf80d2 100644 --- a/dsm_storage_node/tests/bytecommit_chain.rs +++ b/dsm_storage_node/tests/bytecommit_chain.rs @@ -345,6 +345,39 @@ async fn a_node_in_no_set_refuses_a_mirror_sync() { assert_eq!(sync(&app(state)).await, StatusCode::CONFLICT); } +/// A set-mate that accepts the connection and never answers fails the sync +/// within the set client's bounds, instead of holding the sync, and the +/// node's one-at-a-time sync lock, open. The node answers BAD_GATEWAY, and +/// the next sync is answered too: the lock was released. +#[tokio::test] +async fn a_set_mate_that_never_answers_fails_the_sync_and_frees_it() { + let (silent, silent_url) = listener().await; + // Accepts every connection and answers none of them. + tokio::spawn(async move { + let mut held = Vec::new(); + while let Ok((socket, _)) = silent.accept().await { + held.push(socket); + } + }); + let b = app(node_state( + "bc_silent_b", + "dsm-node-b", + &["dsm-node-a", "dsm-node-b"], + &[("dsm-node-a", &silent_url)], + ) + .await); + let within = dsm_storage_node::set_client::SET_MATE_REQUEST_TIMEOUT * 3; + for attempt in 0..2 { + let answered = match tokio::time::timeout(within, sync(&b)).await { + Ok(status) => status, + Err(elapsed) => { + panic!("sync {attempt} hung on a set-mate that never answers: {elapsed}") + } + }; + assert_eq!(answered, StatusCode::BAD_GATEWAY, "sync {attempt}"); + } +} + /// A set-mate that does not answer at its configured endpoint fails the /// sync; the node does not report the sync as done. #[tokio::test] From 96fd51295ced166d20611262a4255cf497dee38e Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:14:02 -0400 Subject: [PATCH 2/3] =?UTF-8?q?docs(storage):=20record=20mirror=20liveness?= =?UTF-8?q?=20(=C2=A76.53)=20and=20five=20route-chain=20rows=20re-verified?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MR-STOR-0131, 0133, 0138 and 0094 move Missing → Met and 0134 Missing → Partial: each Missing note described a codebase before route chains. The cited tests pass at this tree. 0134 stays Partial because the writer never records a taken empty. MR-STOR-0140 cites the silent set-mate test, and the matrix records its mutation control. MR-STOR-0008 notes the set client's transport bounds. --- specs/requirements/CONFORMANCE_GAPS.md | 38 +++++++++++++++++------ specs/requirements/VERIFICATION_MATRIX.md | 1 + 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/specs/requirements/CONFORMANCE_GAPS.md b/specs/requirements/CONFORMANCE_GAPS.md index 1acd78662..d8fde72bd 100644 --- a/specs/requirements/CONFORMANCE_GAPS.md +++ b/specs/requirements/CONFORMANCE_GAPS.md @@ -1842,6 +1842,26 @@ Mutation controls, run on 2026-09-30 and restored byte for byte: Known cost: the first time a receiver meets a payer it validates the payer's segment twice, once in prevalidation and once when it validates its own admission's credit, because a frontier recorded at the target cannot start a verification of that target. Recording every coordinate validated on the way would reduce the second to one step. +### 6.53 Mirror liveness and five route-chain rows re-verified (`test/storage-batch-2`, 2026-10-01) + +**A silent set-mate held the mirror sync.** +- The node's set client had no connect or request timeout, and `mirror_sync` holds a node-wide one-at-a-time lock. +- A set-mate that accepted the connection and never answered held the sync, and with it every later sync on that node, open indefinitely. +- The set client now gives up after 5 s to connect and 10 s per request, failing the fetch as an unreachable set-mate does (BAD_GATEWAY, lock released). A sync fetches at most 128 cycles per member instead of 1024, so one call stays inside the SDK's 30 s request bound; later calls continue. +- Test: `dsm_storage_node::bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it`. +- Mutation control: the timeouts removed → red, "sync 0 hung on a set-mate that never answers". +- In the SDK, `route_seats::read_cell` now asks every seat at once (values, cycle closes, mirror syncs), with the answers kept in route order. A silent seat costs one client timeout instead of one per seat. + +**Five storage rows marked Missing described a codebase without route chains.** Re-verified against the specification and `main`; none has a manifest row. + +| Row | Was | Now | Why | +|---|---|---|---| +| MR-STOR-0131 | Missing (no link type) | Met | A link is the seat's arrival record of an entry that carries its position and the chain before it. | +| MR-STOR-0133 | Missing (copy count) | Met | A later link needs the leader link's chain, a higher position, and its seat's mirror of the leader. | +| MR-STOR-0138 | Missing (no Preserved) | Met | Core reads Preserved; every consumer treats it as not final. | +| MR-STOR-0094 | Missing (no SDK caller) | Met | Mirrors must agree; Core checks the chain link and the root itself. | +| MR-STOR-0134 | Missing (copy counters) | Partial | No-response empties are recorded and never count; taken empties are not built. | + ## 7 Totals | Spec | Rows | Met | Partial | Missing | Violated | Not code | Deferred | @@ -1849,9 +1869,9 @@ Known cost: the first time a receiver meets a payer it validates the payer's seg | DSM high-level (MR-DSM) | 276 | 94 | 96 | 39 | 0 | 29 | 18 | | SoFi (MR-SOFI) | 348 | 223 | 86 | 18 | 4 | 17 | 0 | | dBTC (MR-DBTC) | 135 | 0 | 0 | 0 | 0 | 0 | 135 | -| Storage node (MR-STOR) | 158 | 60 | 18 | 61 | 0 | 18 | 1 | +| Storage node (MR-STOR) | 158 | 64 | 19 | 56 | 0 | 18 | 1 | | Storage §14 lines added after the pin (STOR-014) | 11 | 9 | 1 | 1 | 0 | 0 | 0 | -| **All** | **928** | **386** | **201** | **119** | **4** | **64** | **154** | +| **All** | **928** | **390** | **202** | **114** | **4** | **64** | **154** | ## 8 Per-requirement results @@ -2508,7 +2528,7 @@ The deferral also covers MR-DSM-0198 and MR-DSM-0221–0237 (§6.1), and the dBT | MR-STOR-0005 | Partial | dsm_storage_node · crate-wide (no key/sign symbols) | no test found | True today; no gate or test fails if a key or signing path is added | | MR-STOR-0006 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | The legacy object store, PaidK, `device_auth` and the registry are deleted (#992, §6.28; re-examined 2026-09-27, §6.36 H). The node decodes nothing it holds and branches on no content. | | MR-STOR-0007 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | As MR-STOR-0006. | -| MR-STOR-0008 | Partial | dsm_storage_node · crate-wide (logical ticks, BIGSERIAL) | no test found | No clock reads; no enforcing test | +| MR-STOR-0008 | Partial | dsm_storage_node · crate-wide (logical ticks, BIGSERIAL) | no test found | No clock reads; no enforcing test. The set client now bounds how long a node waits for a set-mate (connect 5 s, request 10 s; §6.53). A fetch that times out stores nothing and orders nothing, so no protocol fact reads the clock. | | MR-STOR-0009 | Met | `dsm_storage_node::api::objects::bytecommit::mirror_sync`; `dsm_storage_node::set_client::pinned_set_client` | `dsm_storage_node::bytecommit_chain::a_set_mate_mirrors_by_fetching_from_the_member_itself`; `dsm_storage_node::bytecommit_chain::an_impostor_at_a_set_mates_endpoint_is_not_mirrored` | There is no gossip, election or vote between nodes. A node's one exchange with a set-mate is mirror sync: it fetches that member's ByteCommits from it, at the endpoint its own configuration names, and decides nothing (storage §14). | | MR-STOR-0010 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | As MR-STOR-0006. | | MR-STOR-0011 | Partial | dsm_storage_node · crate-wide | no test found | True today; not regression-proof | @@ -2594,7 +2614,7 @@ The deferral also covers MR-DSM-0198 and MR-DSM-0221–0237 (§6.1), and the dBT | MR-STOR-0091 | Missing | — | — | api/registry/scaling.rs, with GRACE_CYCLES and its tests, was deleted in #976, and no pruning grace period exists anywhere in the node now. | | MR-STOR-0092 | Missing | — | — | As MR-STOR-0087. | | MR-STOR-0093 | Met | `dsm_storage_node::db::pg::close_cycle`; `dsm::storage_cell::ByteCommit` | `dsm::storage_cell::tests::the_bytecommit_digest_matches_the_spec_construction`; `dsm_storage_node::bytecommit_chain::cycles_close_over_new_entries_and_commit_their_records` | Confirmed. | -| MR-STOR-0094 | Missing | — (no dsm_sdk caller of `record_is_committed`/`CellCommitProof`) | no test found | Confirmed via grep: zero hits in dsm_sdk. | +| MR-STOR-0094 | Met | `dsm::route_chain::evaluate`; `dsm_sdk::sdk::route_seats::read_cell` | `dsm::route_chain::tests::a_byte_commit_backs_a_link_only_when_it_follows_its_parent`; `dsm::route_chain::tests::a_leader_link_counts_only_once_a_byte_commit_commits_it`; `dsm_sdk::sdk::route_seats::tests::two_mirrors_holding_different_byte_commits_give_none` | Re-verified 2026-10-01 (§6.53). A mirrored ByteCommit is taken only when every answering mirror holds the same one, and any disagreement leaves it unestablished: a consistency rule, not a count. Core then checks the chain link to the parent ByteCommit and the root through the member's proof itself. | | MR-STOR-0095 | Missing | dsm_storage_node · api/registry/scaling.rs (signals stored, never checked against ByteCommits) | no test found | Confirmed. | | MR-STOR-0096 | Met | `dsm::storage_cell::entry_digest`; `dsm::storage_cell::running_hash_next`; `dsm_storage_node::db::pg::append_cell_entry` | `dsm::storage_cell::tests::the_running_hash_matches_the_spec_construction`; `dsm::storage_cell::tests::any_change_to_earlier_entries_breaks_later_records` | Confirmed. | | MR-STOR-0097 | Partial | dsm · storage_cell.rs · `ByteCommit::is_drain_proof`; dsm_storage_node · api/registry/scaling.rs · `submit_applicant` (stake_dlv stored opaque) | dsm::storage_cell::tests::`a_drain_proof_is_two_linked_empty_bytecommits` | Predicate exists and is tested in isolation; no stake-unlock caller wires it. | @@ -2631,16 +2651,16 @@ The deferral also covers MR-DSM-0198 and MR-DSM-0221–0237 (§6.1), and the dBT | MR-STOR-0128 | Missing | — | no test found | Confirmed; no Part III succession exists to refine SoFi membership. | | MR-STOR-0129 | Met | `dsm_storage_node::api::cells::put_cell`; `dsm_storage_node::api::cells::get_cell`; `dsm::storage_cell::ArrivalRecord` | `dsm_storage_node::cells_keep_everything::a_put_answers_with_the_arrival_record_a_verifier_replays` | Confirmed. | | MR-STOR-0130 | Met | `dsm_sdk::sdk::route_seats::write_recorded`; `dsm_sdk::sdk::route_seats::write_along` | `dsm_sdk::sdk::route_seats::tests::nothing_reaches_a_later_seat_before_the_leader_answers`; `dsm_sdk::sdk::route_seats::tests::a_seat_that_does_not_answer_is_recorded_empty_in_its_place`; `dsm_sdk::sdk::route_seats::tests::a_recorded_position_is_never_written_again` | Re-verified 2026-09-30 (§6.50): the writer goes leader first, each copy carries the chain built so far, and a stopped write continues from its own record, never from copies. `put_cell_leader_first`, which it cited, is deleted. | -| MR-STOR-0131 | Missing | — (no `RouteEntryV1`/link type in proto or Rust) | no test found | Confirmed via grep of `proto/dsm_app.proto` and repo-wide search. | +| MR-STOR-0131 | Met | `dsm::route_chain::evaluate`; `dsm_sdk::sdk::route_seats::write_recorded` | `dsm::route_chain::tests::an_entry_carries_one_slot_per_earlier_position_and_round_trips`; `dsm::route_chain::tests::a_copy_at_another_position_or_of_another_value_does_not_count`; `dsm::route_chain::tests::a_copy_whose_chain_does_not_begin_with_the_leader_link_does_not_count` | Re-verified 2026-10-01 (§6.53). A link is a seat's arrival record for the copy written there (`ChainSlot::Link`). The record names the member and the cell, and commits the entry, which carries its route position and the chain before it, so each link commits the previous one. | | MR-STOR-0132 | Met | `dsm::route_chain::evaluate` | `dsm::route_chain::tests::the_first_recognized_value_at_the_leader_holds_the_cell`; `dsm::route_chain::tests::junk_first_at_the_leader_blocks_nothing`; `dsm::route_chain::tests::a_leader_holding_no_recognized_value_leaves_the_cell_open` | Re-verified 2026-09-30 (§6.50): the leader link is the first recognized object naming the cell in the leader's log, and junk first at the leader blocks nothing. The superseded rule in `sofi/arith.rs` is deleted. | -| MR-STOR-0133 | Missing | dsm · sofi/arith.rs::`resolve` (copy count only) | arith::tests::`copies_count_wherever_the_value_sits_in_a_copys_list` (demonstrates the count-only mechanism this requirement replaces, not compliance) | Confirmed no chain-validity, position, or mirror-coverage check anywhere. | -| MR-STOR-0134 | Missing | dsm_sdk · sdk/storage_io.rs::`CellWrite{leader_reached,copies}` | no test found | Confirmed: aggregate counters only, no per-seat empty (`taken`/`no_response`) tracking. | +| MR-STOR-0133 | Met | `dsm::route_chain::evaluate` | `dsm::route_chain::tests::a_copy_whose_chain_does_not_begin_with_the_leader_link_does_not_count`; `dsm::route_chain::tests::a_copy_at_another_position_or_of_another_value_does_not_count`; `dsm::route_chain::tests::a_later_link_needs_its_seats_mirror_of_the_leader_link`; `dsm::route_chain::tests::a_later_link_needs_its_own_byte_commit_and_valid_carried_links` | Re-verified 2026-10-01 (§6.53). A later link counts only on a chain from the leader link, at a position above every earlier link's, and when its seat's own mirror of the leader's ByteCommits covers the leader link. The copy-count rule it cited (`sofi/arith.rs`) is deleted. | +| MR-STOR-0134 | Partial | `dsm_sdk::sdk::route_seats::write_along`; `dsm::route_chain::evaluate` | `dsm_sdk::sdk::route_seats::tests::a_seat_that_does_not_answer_is_recorded_empty_in_its_place`; `dsm::route_chain::tests::an_empty_neither_counts_nor_invalidates` | Re-verified 2026-10-01 (§6.53). A seat that does not answer is recorded as a no-response empty in its place, and Core counts no empty as a link. The writer never records a taken empty: nothing in the SDK produces `ChainSlot::Taken`, which would need the writer to read that another value's chain is already first at the seat. | | MR-STOR-0135 | Met | `dsm_storage_node::api::cells::put_cell`; `dsm_storage_node::api::cells::get_cell` | `dsm_storage_node::cells_keep_everything::a_second_value_at_a_key_is_kept_after_the_first_never_refused`; `dsm_storage_node::cells_keep_everything::an_identical_value_put_twice_is_held_twice` | Confirmed. | | MR-STOR-0136 | Met | `dsm_sdk::sdk::route_seats::write_along`; `dsm::route_chain::evaluate` | `dsm_sdk::sdk::route_seats::tests::a_seat_that_does_not_answer_is_recorded_empty_in_its_place`; `dsm::route_chain::tests::a_leader_link_counts_only_once_a_byte_commit_commits_it`; `dsm::route_chain::tests::a_later_link_needs_its_own_byte_commit_and_valid_carried_links` | Re-verified 2026-09-30 (§6.50): the writer carries each link into the next copy at once, with no cycle closed between seats, and Core counts a link only once a ByteCommit commits its arrival record. | | MR-STOR-0137 | Met | `dsm_sdk::sdk::route_seats::write_along`; `dsm_sdk::sdk::route_seats::continue_write` | `dsm_sdk::sdk::route_seats::tests::a_seat_that_does_not_answer_is_recorded_empty_in_its_place`; `dsm_sdk::sdk::route_seats::tests::a_recorded_position_is_never_written_again` | Re-verified 2026-09-30 (§6.50): a write records every one of the five positions, and a chain that stopped is continued along the remaining route. `put_cells_leader_first`, which it cited, is deleted. | -| MR-STOR-0138 | Missing | dsm · sofi/arith.rs::`CellResolution`/`ObjectResolution` (only LeaderHeld/Final, no Preserved) | no test found | Confirmed — third state absent from both enums. | +| MR-STOR-0138 | Met | `dsm::route_chain::evaluate`; `dsm::economic::peer_lineage::validate_peer_lineage` | `dsm::route_chain::tests::three_links_are_final_two_preserved_one_leader_held`; `dsm::sofi::conformance::tests::item_5_an_earlier_attempt_must_be_final` | Re-verified 2026-10-01 (§6.53). Core reads `Preserved` as a leader link plus one further link. Every consumer treats it as not final: the peer walk reports it incomplete, registration and SoFi conformance wait on it, and the reserve read stays at `LeaderHeld`. Receivers rely only on `Final`. | | MR-STOR-0139 | Missing | — | no test found | Confirmed; no loss-marker/pre-loss code anywhere. | -| MR-STOR-0140 | Met | `dsm_storage_node::api::objects::bytecommit::sync_one`; `dsm_storage_node::api::objects::bytecommit::fetch_commit` | `dsm_storage_node::bytecommit_chain::a_set_mate_mirrors_by_fetching_from_the_member_itself` | Confirmed. | +| MR-STOR-0140 | Met | `dsm_storage_node::api::objects::bytecommit::sync_one`; `dsm_storage_node::api::objects::bytecommit::fetch_commit`; `dsm_storage_node::set_client::pinned_set_client` | `dsm_storage_node::bytecommit_chain::a_set_mate_mirrors_by_fetching_from_the_member_itself`; `dsm_storage_node::bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it` | Confirmed. A set-mate that never answers fails the sync within the set client's bounds instead of holding it, so it cannot stop the node mirroring the others (§6.53). | | MR-STOR-0141 | Met | `dsm_storage_node::api::objects::bytecommit::fetch_commit`; `dsm_storage_node::db::pg::mirror_put` (echo-id check) | `dsm_storage_node::bytecommit_chain::an_impostor_at_a_set_mates_endpoint_is_not_mirrored` | Confirmed. | | MR-STOR-0142 | Met | `dsm_storage_node::db::pg::mirror_put` (ON CONFLICT DO NOTHING) | `dsm_storage_node::bytecommit_chain::a_rewritten_cycle_is_kept_beside_the_first` | Confirmed. | | MR-STOR-0143 | Not code | — | — | Proof obligation. G15: the existing finality tests and formal model prove the superseded copy rule and must be redone for route chains (ChatGPT CG-13). | diff --git a/specs/requirements/VERIFICATION_MATRIX.md b/specs/requirements/VERIFICATION_MATRIX.md index 80f6c1283..57a1a1333 100644 --- a/specs/requirements/VERIFICATION_MATRIX.md +++ b/specs/requirements/VERIFICATION_MATRIX.md @@ -91,6 +91,7 @@ Tests named `dsm_sdk::…` run on devices created as wallet creation creates the | MR-SOFI-0151, MR-SOFI-0144, SoFi §17.1 P conformance 2, 3, 8: P's key is its parent claim's — the verifying single-root claim P names at `(G, DevID, p, P.void_root)`, or the `F` whose id a conditional parent names — and `𝒞_E^pre` carries P's typed parent reference | `dsm` · sofi/conformance.rs · `parent_binds_the_key`, `fulfillment_conformance`; sofi/validation.rs · `validate`; `dsm_sdk` · sdk/sofi_sdk.rs · `pre_e_closure`; sdk/sofi_register.rs · `gather_conformance` | `dsm::sofi::conformance::tests::a_precommit_under_a_key_its_parent_claim_does_not_carry_does_not_conform`; `dsm::sofi::conformance::tests::a_parent_claim_of_another_position_or_root_does_not_conform`; `dsm::sofi::conformance::tests::a_parent_that_is_not_a_verifying_claim_does_not_conform`; `dsm::sofi::conformance::tests::a_parent_the_closure_does_not_reference_does_not_conform`; `dsm::sofi::conformance::tests::a_conditional_parent_binds_p_to_the_key_of_the_f_that_installed_it`; `dsm::sofi::validation::tests::a_precommit_naming_a_parent_its_closure_does_not_carry_is_invalid`; `dsm_sdk::handlers::node_e2e_tests::a_sofi_trade_executes_end_to_end` | Fourteen mutations, each red on its named test (2026-09-25): rule 2 in `validate` and in conformance; the single-root key; genesis, device, position, root; the conditional key, position and id; the claim signature; the producer closure emptied; the parent F not fetched; `C_p` routed by `P.void_root`. | — | | MR-STOR-0034, MR-STOR-0145: the spool keeps every envelope unopened; a reused message id is kept, not dropped | `dsm_storage_node` · api/transport/b0x.rs · `submit`, `spool_insert` | `dsm_storage_node::api::transport::b0x::tests::an_envelope_reusing_a_message_id_is_kept_after_the_first`; `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened` | The id dedupe restored → the first red; decode restored on submit → the second red (2026-09-25). | — | | MR-STOR-0009: mirror sync reports a failed set-mate, never "0 new" | `dsm_storage_node` · api/objects/bytecommit.rs · `mirror_sync` | `dsm_storage_node::bytecommit_chain::an_unreachable_set_mate_fails_the_sync`; `dsm_storage_node::bytecommit_chain::a_node_in_no_set_refuses_a_mirror_sync` | A partial sync answered 204 → the first red; a set-less sync answered 204 → the second red (2026-09-25). | — | +| MR-STOR-0140: a set-mate that never answers fails the sync within the set client's bounds and frees it | `dsm_storage_node` · set_client.rs · `pinned_set_client` (connect and request timeouts) | `dsm_storage_node::bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it` | Timeouts removed from the set client → red, "sync 0 hung on a set-mate that never answers" (2026-10-01). | — | | The inbox poller reads `storage.sync`'s local answer | `dsm_sdk` · sdk/inbox_poller.rs · `decode_sync_response` | `dsm_sdk::sdk::inbox_poller::tests::a_storage_sync_answer_as_the_router_frames_it_is_read` | The addressed decoder restored → red (2026-09-25). | — | | Owner ruling 2026-09-24: a contact is stored with its directory AK, its Kyber key and its `h_0`, or not at all | `dsm_sdk` · storage/client_db/contacts.rs · `store_contact` | `dsm_sdk::storage::client_db::contacts::tests::a_contact_without_its_keys_or_its_tip_is_refused` | The Kyber key not required → red (2026-09-25). | — | | A stored relationship tip is read or is an error, never "no tip" | `dsm_sdk` · storage/client_db/contacts.rs · `read_contact_tip` | `dsm_sdk::storage::client_db::contacts::tests::a_corrupt_stored_tip_is_an_error_not_an_absent_tip`; `dsm_sdk::storage::client_db::contacts::tests::a_device_id_that_is_not_32_bytes_is_an_error_not_an_absent_contact` | A short tip read as absent → red; a NULL tip read as absent → red (2026-09-25). | — | From d7521572eff4350607be87ea4a9602b6b2be093f Mon Sep 17 00:00:00 2001 From: Cryptskii <47649969+cryptskii@users.noreply.github.com> Date: Thu, 1 Oct 2026 02:22:21 -0400 Subject: [PATCH 3/3] test(storage): no node source reads a clock (MR-STOR-0008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every node source is parsed with syn, reading paths, `use` trees and macro bodies. Any clock or timer is refused: Instant, SystemTime, UNIX_EPOCH, chrono, tokio::time. A comment or string never counts. The self-test covers grouped, renamed and glob imports and a macro body; a first token-window draft missed the grouped import. Mutation controls, red and then restored: - a grouped-import tokio::time sleep in the spool; - Instant::now() in db/pg.rs. ci/no_clock_and_no_json.sh loses two node exemptions: the spool, which reads no clock, and a rate limiter that no longer exists. A clock read in the spool now fails it too. MR-STOR-0008 Partial → Met. --- Cargo.lock | 2 + ci/no_clock_and_no_json.sh | 2 - dsm_storage_node/Cargo.toml | 3 + dsm_storage_node/tests/no_clock_reads.rs | 175 ++++++++++++++++++++++ specs/requirements/CONFORMANCE_GAPS.md | 15 +- specs/requirements/VERIFICATION_MATRIX.md | 1 + 6 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 dsm_storage_node/tests/no_clock_reads.rs diff --git a/Cargo.lock b/Cargo.lock index 6ed2b7482..4d3631e8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,12 +1285,14 @@ dependencies = [ "dsm", "futures", "log", + "proc-macro2", "prost 0.14.4", "rand 0.8.6", "rcgen", "reqwest", "rustls", "rustls-pki-types", + "syn", "tokio", "tokio-postgres", "tokio-postgres-rustls", diff --git a/ci/no_clock_and_no_json.sh b/ci/no_clock_and_no_json.sh index b73156e59..be22f47bd 100755 --- a/ci/no_clock_and_no_json.sh +++ b/ci/no_clock_and_no_json.sh @@ -63,8 +63,6 @@ common_allow_globs=( # other operational controls. They remain subject to the JSON/encoding/version gates. clock_allow_globs=( "${common_allow_globs[@]}" - --glob '!**/api/infra/rate_limit.rs' # transport-layer DoS rate limiting (permitted) - --glob '!**/api/transport/b0x.rs' # transport-layer rate limiting (permitted) --glob '!**/jni/ble_events.rs' # BLE event buffering / runtime wakeups --glob '!**/deterministic_state_machine/dsm_sdk/src/sdk/bluetooth_transport.rs' # BLE retries / ACK timeouts / reconnect backoff --glob '!**/deterministic_state_machine/dsm_sdk/src/bluetooth/pairing_orchestrator.rs' # BLE handshake freshness / retry windows diff --git a/dsm_storage_node/Cargo.toml b/dsm_storage_node/Cargo.toml index a43d4be78..923bc9532 100644 --- a/dsm_storage_node/Cargo.toml +++ b/dsm_storage_node/Cargo.toml @@ -59,4 +59,7 @@ name = "storage_node" path = "src/main.rs" [dev-dependencies] +# Parse the node's own sources for clock reads (tests/no_clock_reads.rs). +proc-macro2 = "1" rcgen = "0.14.8" +syn = { version = "2", features = ["full", "visit"] } diff --git a/dsm_storage_node/tests/no_clock_reads.rs b/dsm_storage_node/tests/no_clock_reads.rs new file mode 100644 index 000000000..fa182fc2f --- /dev/null +++ b/dsm_storage_node/tests/no_clock_reads.rs @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +//! No storage-node source reads a clock or runs a timer: ordering inside a +//! node is its arrival sequence, and a cycle closes when asked, never on a +//! schedule (storage spec §1 rule 4, §14). Every file under `src/` is parsed: +//! paths and `use` trees are read whole, so a grouped or renamed import is +//! seen, and an unparsed macro body is read token by token. A name in a +//! comment or a string never counts. Bounding how long a client waits for a +//! set-mate is a `Duration` handed to the HTTP client; it reads nothing. + +mod common; + +use proc_macro2::{TokenStream, TokenTree}; +use std::path::{Path, PathBuf}; +use syn::visit::Visit; +use syn::UseTree; + +/// Names only a clock read, a timestamp or a timer needs. +const CLOCK_NAMES: [&str; 4] = ["Instant", "SystemTime", "UNIX_EPOCH", "chrono"]; + +#[derive(Default)] +struct Clocks { + found: Vec, +} + +impl Clocks { + fn path(&mut self, segments: &[String]) { + for name in segments { + if CLOCK_NAMES.contains(&name.as_str()) { + self.found.push(name.clone()); + } + } + if segments + .windows(2) + .any(|pair| pair[0] == "tokio" && pair[1] == "time") + { + self.found.push("tokio::time".to_string()); + } + } +} + +/// Every full path a `use` tree brings into scope. A glob ends at its +/// prefix, so `use tokio::*` is the path `tokio`, which brings `time` in. +fn use_paths(tree: &UseTree, prefix: Vec, out: &mut Vec>) { + match tree { + UseTree::Path(step) => { + let mut next = prefix; + next.push(step.ident.to_string()); + use_paths(&step.tree, next, out); + } + UseTree::Name(leaf) => { + let mut full = prefix; + full.push(leaf.ident.to_string()); + out.push(full); + } + UseTree::Rename(leaf) => { + let mut full = prefix; + full.push(leaf.ident.to_string()); + out.push(full); + } + UseTree::Glob(_) => { + let mut full = prefix; + if full == ["tokio"] { + full.push("time".to_string()); + } + out.push(full); + } + UseTree::Group(group) => { + for item in &group.items { + use_paths(item, prefix.clone(), out); + } + } + } +} + +/// A macro body's identifiers and punctuation in order, with every literal +/// as an empty token so nothing joins across it. +fn tokens(stream: TokenStream, out: &mut Vec) { + for tree in stream { + match tree { + TokenTree::Group(group) => tokens(group.stream(), out), + TokenTree::Ident(ident) => out.push(ident.to_string()), + TokenTree::Punct(punct) => out.push(punct.as_char().to_string()), + TokenTree::Literal(_) => out.push(String::new()), + } + } +} + +impl<'ast> Visit<'ast> for Clocks { + fn visit_path(&mut self, path: &'ast syn::Path) { + let segments: Vec = path.segments.iter().map(|s| s.ident.to_string()).collect(); + self.path(&segments); + syn::visit::visit_path(self, path); + } + + fn visit_item_use(&mut self, item: &'ast syn::ItemUse) { + let mut paths = Vec::new(); + use_paths(&item.tree, Vec::new(), &mut paths); + for path in paths { + self.path(&path); + } + } + + fn visit_macro(&mut self, mac: &'ast syn::Macro) { + let mut flat = Vec::new(); + tokens(mac.tokens.clone(), &mut flat); + let joined: Vec = flat + .split(|t| t == ":") + .filter(|run| !run.is_empty()) + .flat_map(|run| run.iter().cloned()) + .collect(); + self.path(&joined); + syn::visit::visit_macro(self, mac); + } +} + +fn sources(dir: &Path, found: &mut Vec) { + for entry in common::ok_or_panic(std::fs::read_dir(dir), "list a source directory") { + let path = common::ok_or_panic(entry, "read a source directory entry").path(); + if path.is_dir() { + sources(&path, found); + } else if path.extension().is_some_and(|ext| ext == "rs") { + found.push(path); + } + } +} + +fn clock_reads(source: &str, context: &str) -> Vec { + let file = common::ok_or_panic(syn::parse_file(source), context); + let mut clocks = Clocks::default(); + clocks.visit_file(&file); + clocks.found +} + +#[test] +fn a_clock_read_is_found_and_a_comment_or_string_is_not() { + let reads = + "fn f() { let t = std::time::Instant::now(); log::info!(\"{:?}\", SystemTime::now()); }"; + assert_eq!(clock_reads(reads, "parse reads"), ["Instant", "SystemTime"]); + let grouped = "use tokio::{sync::Mutex, time};"; + assert_eq!( + clock_reads(grouped, "parse a grouped import"), + ["tokio::time"] + ); + let renamed = "use std::time::Instant as Tick; fn f() { Tick::now(); }"; + assert_eq!(clock_reads(renamed, "parse a renamed import"), ["Instant"]); + let glob = "use tokio::*; fn f() { time::sleep(d); }"; + assert_eq!(clock_reads(glob, "parse a glob import"), ["tokio::time"]); + let in_macro = "fn f() { tokio::select! { _ = tokio::time::sleep(d) => {} } }"; + assert_eq!(clock_reads(in_macro, "parse a macro body"), ["tokio::time"]); + let quiet = + "// Instant::now()\n/// SystemTime\nfn f() -> &'static str { \"tokio::time chrono\" }"; + assert_eq!(clock_reads(quiet, "parse quiet"), Vec::::new()); +} + +#[test] +fn no_node_source_reads_a_clock() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut files = Vec::new(); + sources(&root, &mut files); + for expected in ["lib.rs", "main.rs", "api/transport/b0x.rs", "set_client.rs"] { + assert!(files.contains(&root.join(expected)), "{expected} not swept"); + } + let mut found = Vec::new(); + for file in &files { + let source = common::ok_or_panic(std::fs::read_to_string(file), "read a node source"); + for name in clock_reads(&source, &file.display().to_string()) { + found.push(format!("{}: {name}", file.display())); + } + } + assert_eq!( + found, + Vec::::new(), + "node sources that read a clock" + ); +} diff --git a/specs/requirements/CONFORMANCE_GAPS.md b/specs/requirements/CONFORMANCE_GAPS.md index d8fde72bd..d9baab7a0 100644 --- a/specs/requirements/CONFORMANCE_GAPS.md +++ b/specs/requirements/CONFORMANCE_GAPS.md @@ -1862,6 +1862,15 @@ Known cost: the first time a receiver meets a payer it validates the payer's seg | MR-STOR-0094 | Missing (no SDK caller) | Met | Mirrors must agree; Core checks the chain link and the root itself. | | MR-STOR-0134 | Missing (copy counters) | Partial | No-response empties are recorded and never count; taken empties are not built. | +**MR-STOR-0008 enforced.** +- Its "no enforcing test" was accurate for the node: `ci/no_clock_and_no_json.sh` scans line by line, and it exempted the spool (`api/transport/b0x.rs`) and a rate limiter that no longer exists. +- `dsm_storage_node::no_clock_reads` parses every node source with `syn`, reading `use` trees whole and macro bodies token by token, and refuses any clock or timer. +- Its self-test covers a grouped import, a renamed import, a glob, a macro body, and a comment and string that must not count. A first token-window draft missed `use tokio::{sync::Mutex, time}`; that self-test caught it. +- Mutation controls, each red and then restored: + - a `tokio::time` sleep imported through a group, in the spool; + - `std::time::Instant::now()` in `db/pg.rs`. +- The script's two node exemptions are removed. A clock read in the spool now also fails it (checked, then restored). + ## 7 Totals | Spec | Rows | Met | Partial | Missing | Violated | Not code | Deferred | @@ -1869,9 +1878,9 @@ Known cost: the first time a receiver meets a payer it validates the payer's seg | DSM high-level (MR-DSM) | 276 | 94 | 96 | 39 | 0 | 29 | 18 | | SoFi (MR-SOFI) | 348 | 223 | 86 | 18 | 4 | 17 | 0 | | dBTC (MR-DBTC) | 135 | 0 | 0 | 0 | 0 | 0 | 135 | -| Storage node (MR-STOR) | 158 | 64 | 19 | 56 | 0 | 18 | 1 | +| Storage node (MR-STOR) | 158 | 65 | 18 | 56 | 0 | 18 | 1 | | Storage §14 lines added after the pin (STOR-014) | 11 | 9 | 1 | 1 | 0 | 0 | 0 | -| **All** | **928** | **390** | **202** | **114** | **4** | **64** | **154** | +| **All** | **928** | **391** | **201** | **114** | **4** | **64** | **154** | ## 8 Per-requirement results @@ -2528,7 +2537,7 @@ The deferral also covers MR-DSM-0198 and MR-DSM-0221–0237 (§6.1), and the dBT | MR-STOR-0005 | Partial | dsm_storage_node · crate-wide (no key/sign symbols) | no test found | True today; no gate or test fails if a key or signing path is added | | MR-STOR-0006 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | The legacy object store, PaidK, `device_auth` and the registry are deleted (#992, §6.28; re-examined 2026-09-27, §6.36 H). The node decodes nothing it holds and branches on no content. | | MR-STOR-0007 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | As MR-STOR-0006. | -| MR-STOR-0008 | Partial | dsm_storage_node · crate-wide (logical ticks, BIGSERIAL) | no test found | No clock reads; no enforcing test. The set client now bounds how long a node waits for a set-mate (connect 5 s, request 10 s; §6.53). A fetch that times out stores nothing and orders nothing, so no protocol fact reads the clock. | +| MR-STOR-0008 | Met | `dsm_storage_node::db::pg::put_cell`; `dsm_storage_node::db::pg::close_cycle` | `dsm_storage_node::no_clock_reads::no_node_source_reads_a_clock`; `dsm_storage_node::no_clock_reads::a_clock_read_is_found_and_a_comment_or_string_is_not` | Enforced 2026-10-01 (§6.53). Arrival order is a database sequence and a cycle closes when asked. Every node source is parsed, and no path, `use` tree or macro body names a clock or a timer (`Instant`, `SystemTime`, `UNIX_EPOCH`, `chrono`, `tokio::time`). The set client's connect and request bounds are a `Duration` given to the HTTP client: a fetch that times out stores and orders nothing. | | MR-STOR-0009 | Met | `dsm_storage_node::api::objects::bytecommit::mirror_sync`; `dsm_storage_node::set_client::pinned_set_client` | `dsm_storage_node::bytecommit_chain::a_set_mate_mirrors_by_fetching_from_the_member_itself`; `dsm_storage_node::bytecommit_chain::an_impostor_at_a_set_mates_endpoint_is_not_mirrored` | There is no gossip, election or vote between nodes. A node's one exchange with a set-mate is mirror sync: it fetches that member's ByteCommits from it, at the endpoint its own configuration names, and decides nothing (storage §14). | | MR-STOR-0010 | Met | `dsm_storage_node::api::transport::b0x::router`; `dsm_storage_node::api::objects::immutable::put_immutable` | `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened`; `dsm_storage_node::api::objects::immutable::tests::proto_decodable_bytes_are_just_bytes` | As MR-STOR-0006. | | MR-STOR-0011 | Partial | dsm_storage_node · crate-wide | no test found | True today; not regression-proof | diff --git a/specs/requirements/VERIFICATION_MATRIX.md b/specs/requirements/VERIFICATION_MATRIX.md index 57a1a1333..d464a9265 100644 --- a/specs/requirements/VERIFICATION_MATRIX.md +++ b/specs/requirements/VERIFICATION_MATRIX.md @@ -92,6 +92,7 @@ Tests named `dsm_sdk::…` run on devices created as wallet creation creates the | MR-STOR-0034, MR-STOR-0145: the spool keeps every envelope unopened; a reused message id is kept, not dropped | `dsm_storage_node` · api/transport/b0x.rs · `submit`, `spool_insert` | `dsm_storage_node::api::transport::b0x::tests::an_envelope_reusing_a_message_id_is_kept_after_the_first`; `dsm_storage_node::api::transport::b0x::tests::bytes_the_node_cannot_read_are_kept_and_returned_unopened` | The id dedupe restored → the first red; decode restored on submit → the second red (2026-09-25). | — | | MR-STOR-0009: mirror sync reports a failed set-mate, never "0 new" | `dsm_storage_node` · api/objects/bytecommit.rs · `mirror_sync` | `dsm_storage_node::bytecommit_chain::an_unreachable_set_mate_fails_the_sync`; `dsm_storage_node::bytecommit_chain::a_node_in_no_set_refuses_a_mirror_sync` | A partial sync answered 204 → the first red; a set-less sync answered 204 → the second red (2026-09-25). | — | | MR-STOR-0140: a set-mate that never answers fails the sync within the set client's bounds and frees it | `dsm_storage_node` · set_client.rs · `pinned_set_client` (connect and request timeouts) | `dsm_storage_node::bytecommit_chain::a_set_mate_that_never_answers_fails_the_sync_and_frees_it` | Timeouts removed from the set client → red, "sync 0 hung on a set-mate that never answers" (2026-10-01). | — | +| MR-STOR-0008: no node source reads a clock or runs a timer | `dsm_storage_node` · every file under src/ (parsed: paths, `use` trees, macro bodies) | `dsm_storage_node::no_clock_reads::no_node_source_reads_a_clock`; `dsm_storage_node::no_clock_reads::a_clock_read_is_found_and_a_comment_or_string_is_not` | A grouped-import `tokio::time` sleep in api/transport/b0x.rs → red; `std::time::Instant::now()` in db/pg.rs → red (2026-10-01). | — | | The inbox poller reads `storage.sync`'s local answer | `dsm_sdk` · sdk/inbox_poller.rs · `decode_sync_response` | `dsm_sdk::sdk::inbox_poller::tests::a_storage_sync_answer_as_the_router_frames_it_is_read` | The addressed decoder restored → red (2026-09-25). | — | | Owner ruling 2026-09-24: a contact is stored with its directory AK, its Kyber key and its `h_0`, or not at all | `dsm_sdk` · storage/client_db/contacts.rs · `store_contact` | `dsm_sdk::storage::client_db::contacts::tests::a_contact_without_its_keys_or_its_tip_is_refused` | The Kyber key not required → red (2026-09-25). | — | | A stored relationship tip is read or is an error, never "no tip" | `dsm_sdk` · storage/client_db/contacts.rs · `read_contact_tip` | `dsm_sdk::storage::client_db::contacts::tests::a_corrupt_stored_tip_is_an_error_not_an_absent_tip`; `dsm_sdk::storage::client_db::contacts::tests::a_device_id_that_is_not_32_bytes_is_an_error_not_an_absent_contact` | A short tip read as absent → red; a NULL tip read as absent → red (2026-09-25). | — |