| … | … | @@ -925,10 +925,47 @@ up to `SELF_ANNOUNCE_UPDATE_MS` instead of ~3 seconds. |
| 925 | 925 | - a ping round on a 3s schedule with tag→response accounting |
| 926 | 926 | - re-announce immediately when answered < min(held, 3) |
| 927 | 927 | |
| 928 | | ⏳ Also still absent: the three-generation `_serverRelays` cycling and |
| 929 | | `refresh-chain.js`'s reverse hash chain (we always send `refresh` empty). |
| 930 | | Neither is required for the record to stay alive; both are needed for full |
| 931 | | parity. |
| 928 | ### 12.2 The rest of B, priced by reading (2026-09-10) |
| 929 | |
| 930 | The two remaining pieces were carried as open parity gaps. Reading |
| 931 | `persistent.js` and `index.js` prices both, and neither is a correctness defect. |
| 932 | |
| 933 | **The refresh chain is an OPTIMISATION, and §9.2's open question is closed.** |
| 934 | §9.2 asked whether an empty `refresh` makes a node treat a re-announce as a new |
| 935 | record. It does not. `onannounce` (persistent.js:100-150) validates the |
| 936 | signature and calls `this.records.add(k, publicKey, record)` on every full |
| 937 | announce, identically, whether or not `refresh` is present. All `refresh` does |
| 938 | is `this.refreshes.set(hash(refresh), {k, record, announceSelf})`, registering a |
| 939 | token so a LATER renewal can arrive as a tiny announce with no peer and no |
| 940 | signature at all — `_onrefresh` (`:72-97`) then re-adds the cached record and |
| 941 | rotates the token. |
| 942 | |
| 943 | So sending it empty costs a full signed announce per renewal — peer record plus |
| 944 | a 64-byte signature, and an Ed25519 verify at each of ~20 holders — where JS |
| 945 | pays about forty bytes and a hash lookup. Real, and bandwidth rather than |
| 946 | behaviour. |
| 947 | |
| 948 | ⛔ It also carries a failure mode the thing it replaces does not: the chain is a |
| 949 | fixed number of blocks built backwards from a seed, and a peer that exhausts it |
| 950 | without falling back to a full announce stops renewing SILENTLY. That is a |
| 951 | correctness risk taken on to save bandwidth, which is the wrong direction to |
| 952 | trade in — so it is priced here and not built. |
| 953 | |
| 954 | **Generation cycling costs at most one record TTL.** JS explicitly unannounces |
| 955 | at any node that took generation 1's announce but not generation 2's |
| 956 | (announcer.js:192-196). We do not, so such a node keeps our record until it |
| 957 | expires on its own. That bound is stated in the reference: |
| 958 | `defaultMaxAge = 20 * 60 * 1000` (index.js:614) — **20 minutes**. |
| 959 | |
| 960 | Our cadences sit inside it: `TOPIC_REFRESH_INTERVAL_MS` 10 min ± 2 min jitter, |
| 961 | `SELF_ANNOUNCE_UPDATE_MS` 5 min. So a record never lapses while we want it, and |
| 962 | a holder we stop refreshing carries a redundant copy for at most 20 minutes. |
| 963 | That is an extra copy, not a wrong answer — a lookup through it still returns a |
| 964 | peer that is there. The case where it WOULD be a wrong answer is the peer going |
| 965 | away, and that is the liveness ping (§12), which is built. |
| 966 | |
| 967 | ⚠ Not claimed: that either would be invisible on the public DHT at scale. What |
| 968 | is claimed is the price, from the reference's own constants. |
| 932 | 969 | |
| 933 | 970 | ## 13. Slice 9 — defect D fixed: associate the topic before the early returns |
| 934 | 971 | |