fix(bootstrap): age-bound and refresh close-group cache - #149
Conversation
fd822d0 to
22326b5
Compare
…he, reject future timestamps - Bind cached addresses to their expected PeerId at dial time; reject and disconnect on handshake identity mismatch so address reassignment cannot import trust from the wrong peer. - Clear/replace the on-disk cache when the authoritative close group is empty instead of leaving stale entries intact. - Reject cache timestamps more than five minutes in the future (also when max-age enforcement is disabled) so wall-clock skew cannot bypass TTL.
Testnet validation — all cache-hygiene cases PASSFault-injection run on a dev testnet, 2026-08-10 (V2-894). Binary: Topology: backbone of genesis + 5 bootstraps + 45 WAN nodes, plus a probe tier of 18 single-service VMs (12 probes, 3 untouched controls, 3 spares) joined as an extension. One Case 1 — a stale snapshot is rejected as a whole
Zero Case 2 — fresh-but-dead cached peers fall back to configuredOutbound UDP dropped to only the cached endpoints (38/38/39 of them), leaving configured endpoints reachable.
The full chain holds on every probe: fresh cache accepted → every cache dial fails, correctly source-labelled → configured succeeds in the same start, no second restart. Case 3 — total unreachability is reported explicitlyFull non-loopback UDP partition, both directions, held 18 minutes. The inbound half matters: with zero outbound successes the node adopts inbound peers after 5 s, which would have rescued bootstrap and voided the window. All three probes: The reachability summary is unambiguous and is the signal that correctly identifies this state — see the note below. Case 5 — periodic refresh survives SIGKILLProbes untouched from deployment (read-only cache copies only).
One correction to a stated assumptionThe test plan assumed "the dialler dedups addresses: an endpoint dialled as Not a defect — the counters stay internally consistent and the case is still scoreable — but worth knowing, because it means an endpoint present in both sets costs two dial attempts on every bootstrap. If dedup was intended, it isn't happening. Practical consequence for anyone reproducing this: with only 2 configured bootstrap endpoints, an earlier attempt found both already in the probe's cached close group at Note on interpreting the replication diagnosticsUnder total partition (case 3), the CoverageAll four cases pass on all 3 probes each. Controls healthy at six boundaries with zero restarts; |
Resolves the src/network.rs conflict in P2PNode::start(). Upstream deleted the DHT_BRIDGE peer-address-update task wholesale (relay address publication is now owned by the reachability acquisition driver); this branch had left that block untouched and appended the periodic close-group-cache save task immediately after it. Resolution takes upstream's deletion and keeps the cache-save task, which is independent of the relay bridge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Linear issue
V2-884 — Age-bound and refresh the saorsa-core close-group bootstrap cache
Risk tier
Compatibility
close_group_cache.jsonformat is unchanged. Old cache files remain readable. Old serializedNodeConfigvalues that omit the new field receive the one-hour default; explicitnullpreserves disabled TTL.NodeConfig::close_group_cache_max_ageand a builder setter. This is source-breaking for downstream code that constructs the publicNodeConfigwith a struct literal; builder and deserialization callers remain compatible.Semver impact
Test evidence
cargo check --all-targets --all-features— passed.cargo test --lib— 475 passed; unrelated floating-point tolerance testadaptive::trust::tests::test_unit_weight_equivalencefailed once at a 1.78e-10 difference, then passed 5/5 isolated reruns.cargo clippy --all-targets --all-features -- -D warnings— passed.cargo fmt --all -- --checkandgit diff --check— passed.The proposed T2 classification still requires dev-testnet evidence before release. Detailed matrix: V2-894.
New dependency
none
ADR
ADR-016 — Age-Bounded, Periodically Refreshed Close-Group Cache — Proposed; human acceptance pending.
Mitigation / rollback
Disable age enforcement with
close_group_cache_max_age: null, disable cache persistence by omittingclose_group_cache_dir, or revert this PR; configured bootstrap peers remain the fallback path.