feat: track and persist session peer outcomes#106
Conversation
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`.
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.
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.
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.
|
Manki — Review complete Planner (28s) Review — 9 findings Judge — 8 kept · 0 dropped (50s) Review metadataConfig:
Judge decisions:
Timing:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v0.42-dev #106 +/- ##
=============================================
+ Coverage 67.93% 67.99% +0.06%
=============================================
Files 319 319
Lines 68002 68117 +115
=============================================
+ Hits 46195 46318 +123
+ Misses 21807 21799 -8
🚀 New features to boost your workflow:
|
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.
There was a problem hiding this comment.
The hardcoded ServiceFlags::NETWORK passed to mark_seen on every successful handshake will silently poison stored peer capabilities — defeating the capability-aware selection that is the stated goal of the parent issue #102. Everything else is polish.
📊 8 findings (1 required, 4 suggestion, 3 nit) · 230 lines · 629s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 628761,
"diffLines": 230,
"diffAdditions": 227,
"diffDeletions": 3,
"filesReviewed": 4,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Architecture & Design"
],
"findingsRaw": 9,
"findingsKept": 8,
"findingsDropped": 1,
"severity": {
"required": 1,
"suggestion": 4,
"nit": 3
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "836f45c301490f36a43ece4ff6dc715a130f2c25",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 3,
"findingsKept": 3,
"responseLength": 3276
},
{
"name": "Correctness & Logic",
"findingsRaw": 3,
"findingsKept": 3,
"responseLength": 3953
},
{
"name": "Architecture & Design",
"findingsRaw": 3,
"findingsKept": 3,
"responseLength": 3427
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 4,
"medium": 4,
"low": 0
},
"severityChanges": 8,
"mergedDuplicates": 1
},
"fileMetrics": {
"fileTypes": {
".rs": 4
},
"findingsPerFile": {
"dash-spv/src/network/manager.rs": 2,
"dash-spv/src/network/reputation.rs": 3,
"dash-spv/src/network/addrv2.rs": 3
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner Review — 13 findings Judge — 7 kept · 0 dropped (161s) Review metadataConfig:
Judge decisions:
Timing:
|
|
@manki review |
|
Review skipped — a review is currently in progress. Retry when it completes, or force now:
|
There was a problem hiding this comment.
consecutive_failures: u32 without #[serde(default)] will hard-fail deserialization on any upgrade from an existing reputations.json — Option<T> fields silently default, but u32 returns a missing-field error. Separately, record_failure_with_penalty is wired into every failure path in manager.rs but has zero test coverage despite the acceptance criteria requiring comprehensive tests. Two previous threads resolved: non-atomic failure recording (PRRT_kwDOQSlaXs57s9cf) via the new combined method, and addrv2 construction duplication (PRRT_kwDOQSlaXs57s9cZ) via the extracted make_addr_message helper.
📊 7 findings (1 required, 4 suggestion, 2 nit) · 311 lines · 410s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 410387,
"diffLines": 311,
"diffAdditions": 289,
"diffDeletions": 22,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Architecture & Design",
"Correctness & Logic",
"Testing & Coverage",
"Performance & Efficiency"
],
"findingsRaw": 13,
"findingsKept": 7,
"findingsDropped": 6,
"severity": {
"required": 1,
"suggestion": 4,
"nit": 2
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "d6e23009c816c41433e3daa20fa34a8f9821a0db",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 3,
"findingsKept": 1,
"responseLength": 3283
},
{
"name": "Architecture & Design",
"findingsRaw": 4,
"findingsKept": 2,
"responseLength": 4945
},
{
"name": "Correctness & Logic",
"findingsRaw": 2,
"findingsKept": 2,
"responseLength": 3359
},
{
"name": "Testing & Coverage",
"findingsRaw": 2,
"findingsKept": 2,
"responseLength": 4077
},
{
"name": "Performance & Efficiency",
"findingsRaw": 2,
"findingsKept": 2,
"responseLength": 2781
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 4,
"medium": 2,
"low": 1
},
"severityChanges": 7,
"mergedDuplicates": 6
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/reputation.rs": 5,
"dash-spv/src/network/reputation_tests.rs": 2
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner (21s) Review — 12 findings Judge — 9 kept · 0 dropped (144s) Review metadataConfig:
Judge decisions:
Timing:
|
|
@manki review |
There was a problem hiding this comment.
Deserialization clamping, missing last_tried update, and test coverage for record_failure_with_penalty are all addressed. The standout new concern is that consecutive_failures is bounded on the deserialization path but not at the two write sites — in-memory values will silently exceed 1,000 and snap back on restart, corrupting the backoff streak that #104 depends on.
📊 9 findings (7 suggestion, 2 nit) · 460 lines · 488s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 487686,
"diffLines": 460,
"diffAdditions": 412,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Correctness & Logic",
"Security & Safety",
"Architecture & Design",
"Testing & Coverage"
],
"findingsRaw": 12,
"findingsKept": 9,
"findingsDropped": 3,
"severity": {
"required": 0,
"suggestion": 7,
"nit": 2
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "dcb69ffe9a7bf03de1660e4b58bd96428d94e0a7",
"agentMetrics": [
{
"name": "Correctness & Logic",
"findingsRaw": 2,
"findingsKept": 1,
"responseLength": 2429
},
{
"name": "Security & Safety",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 3939
},
{
"name": "Architecture & Design",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 4000
},
{
"name": "Testing & Coverage",
"findingsRaw": 4,
"findingsKept": 4,
"responseLength": 4854
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 5,
"medium": 4,
"low": 0
},
"severityChanges": 9,
"mergedDuplicates": 0
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/reputation.rs": 5,
"dash-spv/src/network/reputation_tests.rs": 2,
"dash-spv/src/network/addrv2.rs": 2
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}…ailure-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.
…tion, \`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
|
Manki — Review complete Planner (26s) Review — 12 findings Judge — 6 kept · 0 dropped (130s) Review metadataConfig:
Judge decisions:
Timing:
|
…nalty` 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.
|
Manki — Review complete Planner (25s) Review — 8 findings Judge — 4 kept · 0 dropped (95s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
normalize_after_load correctly patches the last_tried/consecutive_failures invariant, but the unchecked subtraction panic in clamp_future_system_time and the missing persistence round-trip test for normalize_after_load both survive from the previous review. New nits on spurious zero-delta events and flaky timing assertions.
📊 4 findings (2 suggestion, 2 nit) · 591 lines · 444s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 443541,
"diffLines": 591,
"diffAdditions": 543,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Testing & Coverage"
],
"findingsRaw": 8,
"findingsKept": 4,
"findingsDropped": 4,
"severity": {
"required": 0,
"suggestion": 2,
"nit": 2
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "90bd2fd2bda76e6402f4b55d4f322e46c8868060",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 2,
"findingsKept": 0,
"responseLength": 2133
},
{
"name": "Correctness & Logic",
"findingsRaw": 3,
"findingsKept": 3,
"responseLength": 2774
},
{
"name": "Testing & Coverage",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 4243
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 2,
"medium": 2,
"low": 0
},
"severityChanges": 4,
"mergedDuplicates": 2
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/reputation.rs": 2,
"dash-spv/src/network/reputation_tests.rs": 2
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner (29s) Review — 7 findings Judge — 5 kept · 0 dropped (66s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
The unchecked subtraction flagged last review is gone — floor now uses checked_sub — but an analogous unchecked addition for the ceiling slipped in at the same line, and the storage round-trip test added for normalize_after_load only exercises the future-timestamp path, leaving the stale (>30 day) path untested.
📊 5 findings (2 suggestion, 3 nit) · 626 lines · 361s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 361388,
"diffLines": 626,
"diffAdditions": 578,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Testing & Coverage"
],
"findingsRaw": 7,
"findingsKept": 5,
"findingsDropped": 2,
"severity": {
"required": 0,
"suggestion": 2,
"nit": 3
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "a2c4a705ce8fc80938056d4a8b66f5f0ce3abef3",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 2,
"findingsKept": 1,
"responseLength": 1870
},
{
"name": "Correctness & Logic",
"findingsRaw": 1,
"findingsKept": 1,
"responseLength": 793
},
{
"name": "Testing & Coverage",
"findingsRaw": 4,
"findingsKept": 3,
"responseLength": 4216
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 4,
"medium": 1,
"low": 0
},
"severityChanges": 5,
"mergedDuplicates": 1
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/reputation.rs": 1,
"dash-spv/src/network/addrv2.rs": 1,
"dash-spv/src/network/reputation_tests.rs": 2,
"dash-spv/src/network/handshake.rs": 1
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner (28s) Review — 7 findings Judge — 5 kept · 0 dropped (82s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
Acceptance criteria are substantially met — last_tried/last_success/consecutive_failures are tracked and wired in. Two gaps remain: mark_seen preserves stale gossip service flags instead of updating them from the direct handshake observation, and the normalize_after_load path that preserves consecutive_failures on a valid last_tried has no test, leaving a silent regression risk.
📊 5 findings (2 suggestion, 3 nit) · 655 lines · 604s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 604383,
"diffLines": 655,
"diffAdditions": 607,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Testing & Coverage"
],
"findingsRaw": 7,
"findingsKept": 5,
"findingsDropped": 2,
"severity": {
"required": 0,
"suggestion": 2,
"nit": 3
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "71d37487cc00d92f1cece6854e5c939235741981",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 1,
"findingsKept": 0,
"responseLength": 1060
},
{
"name": "Correctness & Logic",
"findingsRaw": 3,
"findingsKept": 3,
"responseLength": 3061
},
{
"name": "Testing & Coverage",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 4549
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 4,
"medium": 1,
"low": 0
},
"severityChanges": 5,
"mergedDuplicates": 0
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/addrv2.rs": 2,
"dash-spv/src/network/reputation_tests.rs": 3
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}…_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.
…ture_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.
|
Manki — Review complete Planner (21s) Review — 8 findings Judge — 5 kept · 0 dropped (94s) Review metadataConfig:
Judge decisions:
Timing:
|
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.
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.
There was a problem hiding this comment.
Both previously open threads remain unaddressed: mark_seen still preserves existing services without explanation, and the normalize_after_load positive branch (valid last_tried preserving consecutive_failures) still has no test. The acceptance criteria explicitly requires comprehensive test coverage for each new field transition — that bar is not yet met.
📊 5 findings (3 suggestion, 2 nit) · 615 lines · 931s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 931175,
"diffLines": 615,
"diffAdditions": 567,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Correctness & Logic",
"Security & Safety",
"Testing & Coverage"
],
"findingsRaw": 8,
"findingsKept": 5,
"findingsDropped": 3,
"severity": {
"required": 0,
"suggestion": 3,
"nit": 2
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "fe827b182bb4e68ef88d8defc36732a4ab95d3f9",
"agentMetrics": [
{
"name": "Correctness & Logic",
"findingsRaw": 2,
"findingsKept": 1,
"responseLength": 3682
},
{
"name": "Security & Safety",
"findingsRaw": 2,
"findingsKept": 2,
"responseLength": 1774
},
{
"name": "Testing & Coverage",
"findingsRaw": 4,
"findingsKept": 4,
"responseLength": 6153
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 2,
"medium": 3,
"low": 0
},
"severityChanges": 5,
"mergedDuplicates": 2
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/addrv2.rs": 1,
"dash-spv/src/network/reputation_tests.rs": 4
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner (28s) Review — 9 findings Judge — 4 kept · 0 dropped (90s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
Substantial new test coverage in reputation_tests.rs (+334 lines) likely addresses the two open test-coverage threads. The manager.rs lifecycle wiring — specifically that handshake-negotiated services are stored (not silently defaulted to NETWORK) and that consecutive_failures resets on success — remains untested at the unit level, which the issue's acceptance criteria explicitly require.
📊 4 findings (1 required, 1 suggestion, 2 nit) · 651 lines · 380s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 380457,
"diffLines": 651,
"diffAdditions": 603,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Testing & Coverage",
"Architecture & Design"
],
"findingsRaw": 9,
"findingsKept": 4,
"findingsDropped": 5,
"severity": {
"required": 1,
"suggestion": 1,
"nit": 2
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "1f9661254e00ca78432d49d692bacaac4778dea1",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 1,
"findingsKept": 0,
"responseLength": 1516
},
{
"name": "Correctness & Logic",
"findingsRaw": 2,
"findingsKept": 0,
"responseLength": 2411
},
{
"name": "Testing & Coverage",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 4221
},
{
"name": "Architecture & Design",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 3345
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 3,
"medium": 1,
"low": 0
},
"severityChanges": 4,
"mergedDuplicates": 0
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/manager.rs": 1,
"dash-spv/src/network/reputation_tests.rs": 1,
"dash-spv/src/network/reputation.rs": 2
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner (27s) Review — 13 findings Judge — 2 kept · 0 dropped (86s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
reputation.rs now has thorough unit coverage for each field transition (+334 lines in reputation_tests.rs), satisfying most acceptance criteria. The gap flagged in the previous review persists: the orchestration in connect_to_peer — capturing peer_services, routing to mark_seen, and switching both failure paths to record_failure_with_penalty — has no unit test, only integration coverage against dashd.
📊 2 findings (1 required, 1 suggestion) · 651 lines · 410s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 410373,
"diffLines": 651,
"diffAdditions": 603,
"diffDeletions": 48,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Correctness & Logic",
"Testing & Coverage",
"Architecture & Design"
],
"findingsRaw": 13,
"findingsKept": 2,
"findingsDropped": 11,
"severity": {
"required": 1,
"suggestion": 1,
"nit": 0
},
"verdict": "REQUEST_CHANGES",
"prNumber": 106,
"commitSha": "0d2b54bad6fc2042b6014193dc77500fefe0d98e",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 3,
"findingsKept": 1,
"responseLength": 3255
},
{
"name": "Correctness & Logic",
"findingsRaw": 3,
"findingsKept": 1,
"responseLength": 3323
},
{
"name": "Testing & Coverage",
"findingsRaw": 4,
"findingsKept": 0,
"responseLength": 4533
},
{
"name": "Architecture & Design",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 3618
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 1,
"medium": 1,
"low": 0
},
"severityChanges": 2,
"mergedDuplicates": 3
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/manager.rs": 1,
"dash-spv/src/network/reputation.rs": 1
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}|
Manki — Review complete Planner Review — 12 findings Judge — 5 kept · 0 dropped (103s) Review metadataConfig:
Judge decisions:
Timing:
|
There was a problem hiding this comment.
Acceptance criteria are substantially met — new fields, lifecycle hookups, and 334 lines of new tests land. The most noteworthy new concern is unwrap_or(ServiceFlags::NETWORK) on the handshake success path: if peer_services is ever None post-handshake, capability-aware selection (the next sub-issue) will silently score peers against the wrong service flags with no diagnostic signal.
📊 5 findings (2 suggestion, 3 nit) · 675 lines · 513s
Review stats
{
"model": "claude-sonnet-4-6",
"reviewTimeMs": 512954,
"diffLines": 675,
"diffAdditions": 613,
"diffDeletions": 62,
"filesReviewed": 5,
"agents": [
"Security & Safety",
"Architecture & Design",
"Correctness & Logic",
"Testing & Coverage",
"Performance & Efficiency"
],
"findingsRaw": 12,
"findingsKept": 5,
"findingsDropped": 7,
"severity": {
"required": 0,
"suggestion": 2,
"nit": 3
},
"verdict": "APPROVE",
"prNumber": 106,
"commitSha": "6d1febfa6a5fec11a0de37ad0ba48a40771c0952",
"agentMetrics": [
{
"name": "Security & Safety",
"findingsRaw": 3,
"findingsKept": 2,
"responseLength": 3102
},
{
"name": "Architecture & Design",
"findingsRaw": 3,
"findingsKept": 1,
"responseLength": 3343
},
{
"name": "Correctness & Logic",
"findingsRaw": 2,
"findingsKept": 1,
"responseLength": 2127
},
{
"name": "Testing & Coverage",
"findingsRaw": 3,
"findingsKept": 0,
"responseLength": 3364
},
{
"name": "Performance & Efficiency",
"findingsRaw": 1,
"findingsKept": 1,
"responseLength": 919
}
],
"judgeMetrics": {
"confidenceDistribution": {
"high": 3,
"medium": 2,
"low": 0
},
"severityChanges": 5,
"mergedDuplicates": 0
},
"fileMetrics": {
"fileTypes": {
".rs": 5
},
"findingsPerFile": {
"dash-spv/src/network/manager.rs": 1,
"dash-spv/src/network/reputation.rs": 2,
"dash-spv/src/network/addrv2.rs": 2
}
},
"reviewerModel": "claude-sonnet-4-6",
"judgeModel": "claude-sonnet-4-6"
}* 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`
* 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`
Summary
last_success,last_tried, andconsecutive_failurestoPeerReputation, wired into the connect/handshake lifecycle: attempts bumplast_tried, successful handshakes setlast_successand resetconsecutive_failures, and both handshake and connect failures incrementconsecutive_failures.AddrV2.timeon successful handshake viaAddrV2Handler::mark_seen, which preserves richer gossip services on known entries and uses the peer's actual handshake-negotiated services for first observations.record_failure_with_penalty, used from both the handshake-failure and connect-failure paths.mark_seenbehavior, and the atomic failure helper.Backward compatibility with older
reputations.jsonfiles is explicitly out of scope per #102. Stale local state can be discarded on upgrade.Foundational work for #104 (capability-aware selection scoring) and #105 (capability-driven churn).
Closes #103
Part of #102