feat(dht)!: add trust quarantine thresholds#119
Conversation
c5f7387 to
8721feb
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a trust-based quarantine policy for DHT routing, adding separate thresholds for (1) swap eligibility, (2) quarantine/automatic-avoidance, and (3) new-peer admission/readmission, and propagates that policy through routing-table admission, lookup candidate selection, and DHT maintenance flows.
Changes:
- Extend
AdaptiveDhtConfig/DhtNetworkConfigto include quarantine thresholds and validate/enforce them throughout DHT operations. - Add close-group quarantine enforcement (evict low-trust close peers when RT has surplus above K) and filter quarantined/below-threshold peers from local lookup results and automatic maintenance paths.
- Update integration tests and documentation to reflect the new trust threshold semantics and breaking config changes.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/trust_flow.rs | Updates tests to build AdaptiveDhtConfig with new fields via ..Default::default(). |
| tests/sybil_protection.rs | Updates docs/comments and config construction to align with quarantine + swap semantics. |
| src/network.rs | Updates node config docs and builder toggle to enable/disable all adaptive thresholds coherently. |
| src/dht/core_engine.rs | Adds quarantine thresholds/state and enforcement logic + extensive unit tests for threshold behavior. |
| src/dht_network_manager.rs | Applies quarantine filtering to lookup/bootstrap/refresh/self-lookup paths and re-runs quarantine after admissions. |
| src/adaptive/dht.rs | Adds new config fields, validation, propagation into the manager config, and triggers quarantine enforcement after trust updates. |
| README.md | Updates high-level trust system description to reflect quarantine model. |
| docs/trust-signals-api.md | Re-documents API semantics around the new thresholds and behavior. |
| docs/SECURITY_MODEL.md | Updates security model docs to match quarantine-based routing enforcement. |
| docs/ROUTING_TABLE_DESIGN.md | Updates routing-table design invariants/threshold descriptions to reflect quarantine + admission gating. |
f19343d to
179e4f6
Compare
| pub(crate) async fn re_evaluate_admission( | ||
| &mut self, | ||
| candidate: NodeInfo, | ||
| candidate_ips: &[IpAddr], | ||
| trust_score: &impl Fn(&PeerId) -> f64, | ||
| ) -> Result<Vec<RoutingTableEvent>> { | ||
| self.check_new_peer_admission(&candidate.id, trust_score(&candidate.id))?; | ||
| let mut routing = self.routing_table.write().await; | ||
| match self.add_with_diversity(&mut routing, candidate, candidate_ips, trust_score, false)? { |
| /// Enable or disable trust-based routing-table enforcement. | ||
| /// | ||
| /// When `false`, peers are never swapped out of the routing table | ||
| /// based on trust scores. Trust scores are still tracked but have | ||
| /// no enforcement effect. | ||
| /// When `false`, trust scores are still tracked but have no routing-table | ||
| /// enforcement effect. | ||
| /// |
| ### Quarantine Reasons | ||
|
|
||
| Routing-table quarantine decisions are represented by events such as: | ||
|
|
||
| ```rust | ||
| pub enum EvictionReason { | ||
| ConsecutiveFailures(u32), // Communication failures | ||
| LowTrust(String), // EigenTrust score below threshold | ||
| FailedAttestation, // Data challenge failure | ||
| CloseGroupRejection, // Consensus-based removal | ||
| Stale, // No activity timeout | ||
| pub enum QuarantineReason { | ||
| LowTrustCloseGroup, // Close-group trust below threshold |
dirvine
left a comment
There was a problem hiding this comment.
Hermes review
Verdict: changes requested before merge — one correctness issue in the stale-revalidation retry path.
Blocking
-
src/dht/core_engine.rs:3051/src/dht_network_manager.rs:4995-5007—re_evaluate_admission()applies the new-peer/readmit gate before checking whether the candidate was already admitted while stale-peer pings were in flight.add_node()correctly only callscheck_new_peer_admission()when the peer is not already in the routing table; the retry path should preserve the same invariant.Why it matters:
revalidate_and_retry_admission()removes stale non-responders first, collects thosePeerRemoved/KClosestPeersChangedevents, then callsre_evaluate_admission(...)?. If a concurrent task admitted the same candidate during the revalidation window and its current trust is belowquarantine_readmit_threshold, the retry can return an error even though the candidate is now an existing peer. That drops the already-collected removal events without broadcasting them, so routing-event consumers can miss removals that already happened in the table.Suggested minimal fix:
- Make
re_evaluate_admission()mirroradd_node():then continue intolet peer_already_known = self.has_node(&candidate.id).await; if !peer_already_known { self.check_new_peer_admission(&candidate.id, trust_score(&candidate.id))?; }
add_with_diversity(). - Consider making
revalidate_and_retry_admission()robust against future retry failures by ensuring removal events for already-evicted stale peers are still broadcast/logged even if candidate re-admission fails.
- Make
CI / local checks
Local checks on head 179e4f60aacf73f300a64e783c562cc06d3b4800:
cargo fmt --all -- --check✅cargo clippy --all-features -- -D clippy::panic -D clippy::unwrap_used -D clippy::expect_used✅cargo test --all-features quarantine --lib --no-fail-fast✅cargo test --all-features --lib --no-fail-fast✅cargo test --all-features --test trust_flow --test sybil_protection --no-fail-fast✅
GitHub checks: build/lint/unit/mutation are green. Security Audit is red due quinn-proto 0.11.14 / RUSTSEC-2026-0185 (upgrade to >=0.11.15), which appears dependency-wide rather than caused by this diff. claude-review failed because the Claude GitHub App is not installed.
The quarantine/admission design otherwise looks coherent: local FIND_NODE responses and automatic lookup/bootstrap/refresh paths are filtering through the quarantine policy, explicit sends remain unblocked, and the marker cap avoids unbounded growth. The stale-revalidation retry asymmetry above is the one I would fix before merging.
|
Additional notes from the parallel review pass:
|
| fn close_group_immediate_eviction_enabled() -> bool { | ||
| false | ||
| } |
| The trust system enables: | ||
| - **Sybil resistance**: Malicious nodes are downscored automatically | ||
| - **Binary blocking**: Peers below the block threshold are evicted and rejected | ||
| - **Self-healing**: Time decay moves blocked peers back toward neutral over days | ||
| - **Live eviction**: Peers below trust threshold are evicted from the routing table immediately | ||
| - **Close-group quarantine**: K-closest peers below the quarantine threshold are evicted when the routing table can retain at least K peers | ||
| - **Self-healing**: Time decay moves quarantined peers back toward neutral over days | ||
| - **Lazy swap-out**: Low-trust peers outside the close group are replaced when better candidates arrive |
| - `quarantine_threshold` (`0.20` by default): peers below this score are | ||
| skipped by lookup result selection and automatic lookup/dial paths. If such | ||
| a peer is currently in the K-closest-to-self set, it is evicted and | ||
| quarantined when the routing table can retain at least K peers. | ||
| - `quarantine_readmit_threshold` (`0.45` by default): a quarantined peer can |
| | `TRUST_PROTECTION_THRESHOLD` | Trust score above which a peer resists swap-closer eviction | `0.7` | | ||
| | `BLOCK_THRESHOLD` | Trust score below which a peer is evicted and blocked | `0.15` | | ||
| | `EMA_ALPHA` | EMA smoothing factor — weight of each new observation (higher = faster response) | `0.3` | | ||
| | `DECAY_LAMBDA` | Per-second exponential decay rate toward neutral (0.5) | `4.198e-6` | | ||
| | `SWAP_THRESHOLD` | Trust score below which a peer is eligible for replacement when a better candidate needs the slot | `0.35` | | ||
| | `QUARANTINE_THRESHOLD` | Trust score below which automatic lookup/dial paths avoid the peer, and close-group peers are evicted when the routing table can retain at least K peers | `0.20` | | ||
| | `QUARANTINE_READMIT_THRESHOLD` | Trust score required for new routing-table admission/readmission after quarantine | `0.45` | | ||
| | `EMA_ALPHA` | EMA smoothing factor — weight of each new observation (higher = faster response) | `0.124` | |
| 5. **Authenticated membership**: Only peers that have completed transport-level authentication are eligible for routing table insertion. Unauthenticated peers MUST NOT enter `LocalRT`. | ||
| 6. **IP diversity**: No enforcement scope (per-bucket or routing-neighborhood) may exceed `IP_EXACT_LIMIT` nodes per exact IP or `IP_SUBNET_LIMIT` nodes per subnet, except via explicit loopback or testnet overrides. | ||
| 7. **Trust blocking**: Peers with `TrustScore(self, P) < BLOCK_THRESHOLD` MUST be evicted from the routing table and MUST NOT be re-admitted until their trust score recovers above `BLOCK_THRESHOLD`. | ||
| 7. **Trust quarantine and admission**: Peers with `TrustScore(self, P) < QUARANTINE_THRESHOLD` MUST be skipped by local lookup result selection, FIND_NODE responses, and automatic lookup/dial candidate selection. If such a peer is in the K-closest-to-self set, it MUST be evicted and quarantined until `TrustScore(self, P) >= QUARANTINE_READMIT_THRESHOLD` whenever eviction leaves `LocalRT(self)` with at least K peers. If eviction would shrink `LocalRT(self)` below K peers, the peer remains in the table but is still avoided by automatic lookup policy. Any new routing-table peer MUST have `TrustScore(self, P) >= QUARANTINE_READMIT_THRESHOLD`; existing routing-table peers between the two thresholds may remain in the table, including after moving into the K-closest set. | ||
| 8. **Trust protection (staleness-gated)**: A peer with `TrustScore(self, P) >= TRUST_PROTECTION_THRESHOLD` **AND** `last_seen` within `LIVE_THRESHOLD` MUST NOT be evicted by swap-closer admission. A peer whose `last_seen` exceeds `LIVE_THRESHOLD` receives no trust protection regardless of score — stale peers MUST NOT hold slots against live candidates. | ||
| 9. **Deterministic distance**: `Distance(A, B)` is symmetric, deterministic, and consistent across all nodes. Two nodes compute the same distance between the same pair of keys. | ||
| 10. **Atomic admission**: IP diversity checks, capacity checks, swap-closer evictions, trust score reads, and insertion MUST execute within a single write-locked critical section to prevent TOCTOU races. All `TrustScore` queries during admission (steps 4, 8) MUST occur while the routing table write lock is held. | ||
| 10. **Atomic admission**: IP diversity checks, capacity checks, swap-closer evictions, trust score reads, and insertion MUST execute within a single exclusive admission critical section to prevent TOCTOU races. Implementations may use a routing-table write lock or an outer DHT-engine write guard that serializes admission. |
Add close-group quarantine below 0.20 with natural readmission at 0.45. Keep 0.35 as lazy swap eligibility, avoid quarantined peers in automatic lookups, and keep explicit sends unblocked. BREAKING CHANGE: AdaptiveDhtConfig now includes quarantine_threshold and quarantine_readmit_threshold fields.
Implement stricter trust gating for K-closest set admission and readmission, adding support for filtering newly promoted peers below the readmission threshold. Update routing table logic and wire compatibility to stabilize behavior across nodes. Extend related tests and documentation. BREAKING CHANGE: Adjusts close-group thresholds affecting trust-based peer routing and admission policies.
Gate all new routing-table admissions at quarantine_readmit_threshold while allowing existing routing-table peers above the quarantine threshold to stay and move into the close group. BREAKING CHANGE: new peers below quarantine_readmit_threshold are no longer admitted to the routing table, even for non-close routing slots.
581c34b to
13cf699
Compare
| - **Binary blocking**: Peers below the block threshold are evicted and rejected | ||
| - **Self-healing**: Time decay moves blocked peers back toward neutral over days | ||
| - **Live eviction**: Peers below trust threshold are evicted from the routing table immediately | ||
| - **Close-group quarantine**: K-closest peers below the quarantine threshold are evicted when the routing table can retain at least K peers |
| - `quarantine_threshold` (`0.20` by default): peers below this score are | ||
| skipped by lookup result selection and automatic lookup/dial paths. If such | ||
| a peer is currently in the K-closest-to-self set, it is evicted and | ||
| quarantined when the routing table can retain at least K peers. |
| | `EMA_ALPHA` | EMA smoothing factor — weight of each new observation (higher = faster response) | `0.3` | | ||
| | `DECAY_LAMBDA` | Per-second exponential decay rate toward neutral (0.5) | `4.198e-6` | | ||
| | `SWAP_THRESHOLD` | Trust score below which a peer is eligible for replacement when a better candidate needs the slot | `0.35` | | ||
| | `QUARANTINE_THRESHOLD` | Trust score below which automatic lookup/dial paths avoid the peer, and close-group peers are evicted when the routing table can retain at least K peers | `0.20` | |
| /// operations. Trust scores are computed live — low-trust peers are | ||
| /// swapped out when better candidates arrive. | ||
| /// swapped out when better candidates arrive, and bad close-group peers | ||
| /// are quarantined when the routing table has enough peers. |
| if quarantined_peers.len() >= MAX_QUARANTINED_PEERS { | ||
| Self::prune_redundant_quarantined_peers( | ||
| quarantined_peers, | ||
| quarantined_peer_order, | ||
| quarantine_threshold, | ||
| quarantine_readmit_threshold, | ||
| trust_score, | ||
| ); | ||
| } | ||
| if quarantined_peers.len() >= MAX_QUARANTINED_PEERS { | ||
| return; | ||
| } |
| let trust_score = |peer_id: &PeerId| engine.score(peer_id); | ||
| let rt_events = { | ||
| let mut dht = self.dht.write().await; | ||
| dht.enforce_close_group_trust_gate(&trust_score).await | ||
| }; |
| - `quarantine_threshold` (`0.20` by default): peers below this score are | ||
| skipped by lookup result selection and automatic lookup/dial paths. If such | ||
| a peer is currently in the K-closest-to-self set, it is evicted and | ||
| quarantined when the routing table can retain at least K peers. |
| Routing-table quarantine decisions are represented by events such as: | ||
|
|
||
| ```rust | ||
| pub enum EvictionReason { | ||
| ConsecutiveFailures(u32), // Communication failures | ||
| LowTrust(String), // EigenTrust score below threshold | ||
| FailedAttestation, // Data challenge failure | ||
| CloseGroupRejection, // Consensus-based removal | ||
| Stale, // No activity timeout | ||
| pub enum QuarantineReason { | ||
| LowTrustCloseGroup, // Close-group trust below threshold |
| 5. **Authenticated membership**: Only peers that have completed transport-level authentication are eligible for routing table insertion. Unauthenticated peers MUST NOT enter `LocalRT`. | ||
| 6. **IP diversity**: No enforcement scope (per-bucket or routing-neighborhood) may exceed `IP_EXACT_LIMIT` nodes per exact IP or `IP_SUBNET_LIMIT` nodes per subnet, except via explicit loopback or testnet overrides. | ||
| 7. **Trust blocking**: Peers with `TrustScore(self, P) < BLOCK_THRESHOLD` MUST be evicted from the routing table and MUST NOT be re-admitted until their trust score recovers above `BLOCK_THRESHOLD`. | ||
| 7. **Trust quarantine and admission**: Peers with `TrustScore(self, P) < QUARANTINE_THRESHOLD` MUST be skipped by local lookup result selection, FIND_NODE responses, and automatic lookup/dial candidate selection. If such a peer is in the K-closest-to-self set, it MUST be evicted and quarantined until `TrustScore(self, P) >= QUARANTINE_READMIT_THRESHOLD` whenever eviction leaves `LocalRT(self)` with at least K peers. If eviction would shrink `LocalRT(self)` below K peers, the peer remains in the table but is still avoided by automatic lookup policy. Any new routing-table peer MUST have `TrustScore(self, P) >= QUARANTINE_READMIT_THRESHOLD`; existing routing-table peers between the two thresholds may remain in the table, including after moving into the K-closest set. |
|
Putting the July testnet results on record here, since so far they only live in Slack: as parameterized, this branch partitioned a healthy network in all three runs against it (07-10, 07-15, and 07-16 with 5d5f69f applied). Upload success fell 99.9% to 30.2% on 07-10; on 07-16 the fleet went from zero failures to every client failing within ~30 min of the first hourly audit burst, with routing-table admissions collapsing (59,274 to 633 on the 07-15 run). Why: one EMA step (0.124) moves a neutral peer 0.5 to 0.438, below the 0.45 admission bar, so a single failed interaction of any kind locks a peer out. Failures report at weights up to 5 while almost nothing reports success (421k negative vs 35k positive deltas on 07-16), recovery is decay-only (4h to 31h per single event), and gated lookups prevent a sub-0.45 peer from earning its way back. So under load: timeouts become penalties, penalties block admissions, routing tables stop learning, uploads fail. The dangers as designed:
What I'd want before merge:
Not repeating the earlier bot/Hermes findings (marker-set growth, re_evaluate_admission ordering, docs drift). Happy to pair on the penalty classification; the audit-side rate bounding is on me and I'll make sure the two land compatibly. |
Summary
[0.20, 0.45): they may remain in the table and can move into the close group, while peers below 0.20 are avoided by automatic lookup/dial paths.Breaking Changes
AdaptiveDhtConfignow includesquarantine_thresholdandquarantine_readmit_threshold.quarantine_readmit_thresholdbefore admission when trust quarantine is enabled.quarantine_thresholdare no longer handed out through DHT lookup results or used by automatic lookup/dial maintenance paths.Tests
Greptile Summary
This PR adds a three-tiered trust quarantine policy to the DHT routing layer: a 0.20 automatic-avoidance threshold, a 0.45 new-peer admission threshold, and the existing 0.35 lazy-swap threshold. Immediate close-group eviction is disabled for now while trust scoring stabilizes. The implementation gates new routing-table admissions, close-group evictions, local lookup results, automatic lookup/dial paths, bootstrap, and bucket refresh.
quarantine_thresholdandquarantine_readmit_thresholdfields toAdaptiveDhtConfig,DhtNetworkConfig, andDhtCoreEngine, wired end-to-end fromAdaptiveDHTthroughDhtNetworkManagerto the core routing engine.enforce_close_group_trust_gate,check_new_peer_admission,should_avoid_for_lookup, andshould_avoid_automatic_candidateas new trust-gating primitives guarded byquarantine_enabled().broadcast_routing_events_with_quarantineto run deferred close-group evictions after any routing-table admission and emit a single mergedKClosestPeersChangedevent.Confidence Score: 4/5
The new quarantine logic is well-tested, correctly gated behind quarantine_enabled(), and does not block explicit sends. All automatic lookup paths consistently apply the new filters.
The three-threshold eviction and lookup-filtering paths are covered by purpose-built unit tests exercising boundary conditions, deferred eviction, and readmission. The _previous_close_group dead parameter and quarantined_peers growth under adversarial churn are notable open questions but neither breaks current behaviour.
src/dht/core_engine.rs — the enforce_close_group_trust_gate dead parameter and quarantined_peers unbounded growth warrant a second look before long-running node deployments.
Important Files Changed
_previous_close_groupparameter inenforce_close_group_trust_gateis accepted at every call site but unused.quarantined_peersHashSet has no upper-bound or expiry mechanism.broadcast_routing_events_with_quarantinecorrectly merges admission and quarantine-eviction events into a singleKClosestPeersChanged.AdaptiveDhtConfigwith validation, defaults, and propagation. Tests cover defaults, validation, and threshold ordering.trust_enforcement(false)zeroes all three thresholds;trust_enforcement(true)uses fullAdaptiveDhtConfig::default()...Default::default()for the two new fields; test intent unchanged.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Trust event reported] --> B[Update TrustEngine score] B --> C[enforce_trust_quarantine] C --> D{trust_engine present?} D -- No --> Z[return false] D -- Yes --> E[enforce_close_group_trust_gate] E --> F{quarantine enabled?} F -- No --> G[return empty events] F -- Yes --> H{node_count > K AND K-closest peer below quarantine_threshold?} H -- No --> I[return empty events] H -- Yes --> J[evict peer, add to quarantined_peers] J --> H J --> K[emit PeerRemoved + KClosestPeersChanged] K --> L[broadcast_routing_events] M[New peer admission] --> N{peer already known?} N -- No --> O[check_new_peer_admission] O --> P{quarantine enabled?} P -- No --> Q[admit peer] P -- Yes --> R{trust >= readmit_threshold?} R -- No --> S[Reject] R -- Yes --> T[remove from quarantined_peers, admit] T --> U[broadcast_routing_events_with_quarantine] U --> V{PeerAdded or KClosestPeersChanged?} V -- No --> W[broadcast directly] V -- Yes --> X[enforce_close_group_trust_gate deferred pass] X --> Y[merge KClosestPeersChanged, broadcast] AA[Automatic lookup / bootstrap / bucket refresh] --> AB[should_avoid_automatic_peer] AB --> AC{quarantine enabled?} AC -- No --> AD[include peer] AC -- Yes --> AE{trust below quarantine_threshold OR quarantined and below readmit?} AE -- Yes --> AF[skip peer] AE -- No --> AG{unknown peer below readmit_threshold?} AG -- Yes --> AF AG -- No --> ADPrompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(docs): document adaptive trust enfor..." | Re-trigger Greptile