Commit bda8a95
authored
feat: track and persist session peer outcomes (#106)
* feat: add session outcome fields to `PeerReputation`
Adds `last_success`, `last_tried`, and `consecutive_failures` to `PeerReputation`, plus a `record_connection_failure` method on `PeerReputationManager`. All new fields use `#[serde(default)]` so existing `reputations.json` files load without migration.
`record_connection_attempt` now sets `last_tried` and `record_successful_connection` sets `last_success` and resets `consecutive_failures`.
* test: cover `PeerReputation` session outcome transitions
Adds unit tests for default values, `last_tried` on attempt, `last_success` plus `consecutive_failures` reset on success, failure streak increment preserving `last_success`, and legacy `reputations.json` decoding with missing fields.
* feat: bump `AddrV2.time` on successful handshake
Adds `AddrV2Handler::mark_seen` to refresh the stored timestamp for a directly observed peer, preserving existing services for known entries and inserting a fresh entry otherwise. `connect_to_peer` now calls `mark_seen` after a successful handshake so the `peers.dat` time reflects first-hand observation instead of gossip.
* feat: track peer connection outcomes in network manager
Calls `record_connection_failure` on both the TCP connect failure and the handshake failure paths in `PeerNetworkManager::connect_to_peer`, so the `consecutive_failures` streak reflects every unsuccessful attempt.
* refactor: drop backward-compat shims from `PeerReputation`
Removes `#[serde(default)]` on the new session outcome fields and the legacy-JSON load test. Backward compatibility of `reputations.json` across versions is no longer a requirement, so the shims and test are dead weight.
* refactor: thread peer-advertised services into `mark_seen`
* docs: clarify `last_connection` vs `last_tried` on `PeerReputation`
* refactor: extract `make_addr_message` helper in `addrv2`
* refactor: defensively set `last_tried` in `record_connection_failure`
* refactor: add atomic `record_failure_with_penalty` and use at failure sites
* chore: apply `cargo fmt`
* refactor: clamp `consecutive_failures` on deserialization
* refactor: extract `apply_score_change` and always update `last_tried` on failure
* test: cover `record_failure_with_penalty` directly
* refactor: clamp \`consecutive_failures\` at runtime and consolidate failure-field updates
Extract private `record_failure_fields` that applies `last_tried = now` and
`consecutive_failures.saturating_add(1).min(MAX_CONSECUTIVE_FAILURES)`. Both
`record_connection_failure` and `record_failure_with_penalty` now delegate to it,
eliminating duplicated mutations and capping the in-memory streak at the same 1000
limit enforced by the deserializer.
* test: cover \`consecutive_failures\` clamp, \`last_success\` preservation, \`mark_seen\` eviction
- Assert `last_success` is unchanged after `record_failure_with_penalty`
- Deserialize a `PeerReputation` with `consecutive_failures: 99999` and assert it clamps to `MAX_CONSECUTIVE_FAILURES`
- Fill `AddrV2Handler` to capacity and assert `mark_seen` stays bounded and includes the new entry
* refactor: remove unused `record_connection_failure`
* test: stabilize eviction test and cover runtime `consecutive_failures` saturation
* refactor: document and assert non-negative contract on `record_failure_with_penalty`
* test: cover `last_tried` preservation on success and update on failure
* fix: clamp negative `score_change` in `record_failure_with_penalty`
* test: cover happy-path attempt to success lifecycle
* refactor: tighten `clamp_future_system_time` bounds and enforce load invariants
Add a 30-day lower bound to `clamp_future_system_time` so stale or corrupted timestamps
(including epoch 0) are discarded on load, in addition to future ones.
Add `PeerReputation::normalize_after_load` and call it from the storage load path.
It resets `consecutive_failures` to 0 whenever `last_tried` is `None`, preventing
the inconsistent state where a non-zero failure streak has no temporal anchor.
* test: cover `clamp_future_system_time` edge cases
Add three tests: future timestamp rejected, epoch-zero rejected (exercising the new
lower bound), and recent-past timestamp preserved.
* docs: clarify zero-`score_change` contract on `record_failure_with_penalty`
A value of 0 is a deliberate no-op for the reputation score but still records
the failure counter and timestamp, which is useful for failures that should be
tracked without contributing toward a ban.
* fix: use `checked_sub` in `clamp_future_system_time` to avoid panic on broken clocks
* test: cover `normalize_after_load` via storage round-trip
* fix: use `checked_add` in `clamp_future_system_time` to avoid panic on far-future clocks
* test: also cover stale-timestamp path in `normalize_after_load` round-trip
* refactor: drop 30-day stale-timestamp floor from `clamp_future_system_time`
Remove `TIMESTAMP_MAX_AGE` and the `floor` computation that rejected timestamps older than 30 days. The future-timestamp guard (10-second tolerance) is the only meaningful constraint. Update the `normalize_after_load` doc comment to drop the "stale" reference.
* test: remove obsolete stale-timestamp tests and consolidate `clamp_future_system_time` coverage
Delete `test_normalize_after_load_via_storage_round_trip_stale` (tested the removed stale-floor path). Merge the three `test_clamp_future_system_time_*` tests into a single `test_clamp_future_system_time` covering future rejection and recent-past acceptance.
* fix: \`mark_seen\` now overwrites \`services\` on existing entries
Since round 1 the caller passes the actual handshake-negotiated services, so
preserving the gossip-sourced value was inverted. The handshake-observed value
is authoritative and is now written on both new and existing entries.
Rename \`test_mark_seen_bumps_time_and_preserves_services\` to
\`test_mark_seen_bumps_time_and_updates_services\` and update its assertion to
expect the handshake services, not the original gossip services.
* test: cover positive \`normalize_after_load\` branch
Add \`test_normalize_after_load_preserves_failures_when_last_tried_valid\`
to assert that a valid (non-future) \`last_tried\` and a non-zero
\`consecutive_failures\` are both preserved through the load round-trip,
complementing the existing reset-path test.
* refactor: move `record_failure_fields` to `impl PeerReputation`
* refactor: move `apply_score_change` to `impl PeerReputation`1 parent 853d3e3 commit bda8a95
5 files changed
Lines changed: 613 additions & 62 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
29 | 42 | | |
30 | 43 | | |
31 | 44 | | |
| |||
134 | 147 | | |
135 | 148 | | |
136 | 149 | | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
141 | 154 | | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
148 | 168 | | |
149 | 169 | | |
150 | | - | |
151 | | - | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
152 | 180 | | |
153 | 181 | | |
154 | 182 | | |
| |||
187 | 215 | | |
188 | 216 | | |
189 | 217 | | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
190 | 296 | | |
191 | 297 | | |
192 | 298 | | |
| |||
199 | 305 | | |
200 | 306 | | |
201 | 307 | | |
202 | | - | |
| 308 | + | |
203 | 309 | | |
204 | 310 | | |
205 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
301 | 301 | | |
302 | 302 | | |
303 | 303 | | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
304 | 309 | | |
305 | 310 | | |
306 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
265 | 265 | | |
266 | 266 | | |
267 | 267 | | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
268 | 272 | | |
269 | 273 | | |
270 | 274 | | |
| |||
290 | 294 | | |
291 | 295 | | |
292 | 296 | | |
293 | | - | |
294 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
295 | 300 | | |
296 | 301 | | |
297 | 302 | | |
| |||
311 | 316 | | |
312 | 317 | | |
313 | 318 | | |
314 | | - | |
315 | 319 | | |
316 | | - | |
| 320 | + | |
317 | 321 | | |
318 | 322 | | |
319 | 323 | | |
| |||
328 | 332 | | |
329 | 333 | | |
330 | 334 | | |
331 | | - | |
332 | 335 | | |
333 | | - | |
| 336 | + | |
334 | 337 | | |
335 | 338 | | |
336 | 339 | | |
| |||
0 commit comments