Repository navigation
Gate every node reputation penalty behind REPUTATION_PENALTY_SCALE (default 0) - #507
Conversation
…efault 0 A real mirrored node on the test droplet (node_ref ndebvzgeoij5t2l, node_id retce36dbb4) was permanently blocked off ONE trust sample. The chain: NodeAnalyticsManager.evaluate_reputations() acted on any node with at least one sample, a single out-of-threshold sample scores 0.0, NodeReputation .evaluate_trust charges 0.15 for that, and the evaluator runs every REPUTATION_INTERVAL_S = 60 s — so 1.0 crosses the 0.2 block threshold in six minutes. Once blocked, record_detection_frame returns False and every mirrored frame is dropped, apply_reward is a no-op so the node cannot climb back, and services/state_snapshot.py persists the block, so a restart brings it straight back. There is no admin unblock route, and NodeReputation .unblock() only resets to 0.3 — one penalty above re-blocking. Operator decision: for now, trust must never lower a node's reputation. One switch, default off, and a temporary stance rather than a change of intent — the only trust input today is a single claim residual from the identity-first lane, which is not enough evidence to act on. Trust is still computed and still reported; rewards are untouched. REPUTATION_PENALTY_SCALE (config/constants.py, default 0) multiplies every reputation penalty in the estate: trust 0.15/0.05, stale heartbeat 0.1, high detection rate 0.05, neighbour inconsistency 0.08, and the ADS-B cross-validation 0.1 charged from services/tasks/periodic.py. 0 records nothing at all, so no node can be blocked by any of those paths; 1 restores the historical behaviour. A negative or non-finite value logs a warning and reads as 0 — an unparseable gate must not read as "penalties on". core/state .py pushes it into the library at import, before restore_snapshot() rebuilds the reputations and before the evaluator's first pass, and logs it at INFO so a deploy log says which way it went. The scaling itself and the evaluator's new min-sample bar live in retina-analytics (separate submodule commit; the pin bump follows). services/node_bias.py now imports TRUST_MIN_SAMPLES from there instead of keeping its own literal 3, so the solver's reading of a young node's trust and the evaluator's willingness to act on it cannot drift apart. The switch deliberately does not unblock anything a snapshot already carries — that would hide from the operator which nodes had been blocked and why. backend/scripts/unblock_nodes.py does that instead: stdlib-only, operating on the schema-2 snapshot envelope with the server stopped. A script rather than a route because there is no unblock endpoint today and adding one is a separate decision (it would need an authz story and an audit trail, and this is a one-off cleanup after a bug, not an operation the product needs). Tests: penalties_on fixture in tests/conftest.py, applied to exactly the tests that assert a penalty lands, so the suite's ambient configuration is the deployed one. New coverage for the default recording nothing on a 100 km mismatch, the env parsing, that a restored block survives an evaluator pass untouched, and the unblock script's round trip through a verifying snapshot. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Picks up retina-analytics PR #34: NodeReputation.penalty_scale + set_penalty_scale(), which core/state.py now sets from REPUTATION_PENALTY_SCALE, and TRUST_MIN_SAMPLES, which node_bias imports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
id_for_ref answers from the registry, and a mirrored node has no row there: on the test droplet it printed None for ndebvzgeoij5t2l. The key was found by hand (the one ret* id among the synth-* blocked entries, whose single trust sample reproduces the published rms_delay_error_us). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
core/state.py runs at import, before main.py's logging.basicConfig, so its INFO line never reached the container log on the test droplet. The scale is still set at import (it must precede the snapshot restore); the line now prints from the lifespan, just before restore_snapshot(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
test_no_real_identities caught retce36dbb4 in the docstring, the argparse epilog and the test fixture. This repo is public; a placeholder says the same thing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Unrelated to the penalty scale; here so this PR's CI can go green. - test_health_returns_ok: #506 added synthetic_fleet to /api/health (routes/health.py) and the test still expected {"status": "ok"} alone. Assert the contract instead: status ok, synthetic_fleet a bool, no other keys. - test_no_real_identities: #506's mapShell.test.tsx rendered /nodes/<real node id>; any path segment does for that assertion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Claude finished @jehanazad's task in 1m 25s —— View job Code Review: Gate node reputation penalties behind
|
Why
On the test droplet the mirrored real node
ndebvzgeoij5t2lwas received but discarded:reputation 0.0,blocked: true, 100 stored penalties of 0.15 readingTrust score critically low: 0.000, one per minute, from a trust entry with one claim residual.evaluate_reputations()acted on any node with at least one sample, one out-of-threshold sample scores 0.0,evaluate_trustcharges 0.15 per 60 s pass, so 1.0 crossed the 0.2 block threshold in six minutes. Once blocked,record_detection_framedrops every frame,apply_rewardis a no-op, andstate_snapshot.pypersists the block across restarts. The backend's own trust reader (node_bias.get_node_trust) already used a 0.5 prior below 3 samples; the evaluator bypassed it.What
Pairs with retina-analytics #34 (merge that first; the pin here is its head).
REPUTATION_PENALTY_SCALE, default 0: one switch every reputation penalty routes through (trust 0.15/0.05, heartbeat 0.1, detection rate 0.05, neighbour inconsistency 0.08, ADS-B cross-validation 0.1). At 0 nothing is recorded and no node can be blocked; rewards untouched; trust still computed and published. Set from the environment incore/state.pybefore the snapshot restore and the first evaluator pass; logged at startup. Documented in.env.example, the runbook and the architecture gate table as a temporary stance while the trust input is a single residual.node_biasnow importsTRUST_MIN_SAMPLESfrom retina-analytics, where the evaluator applies the same bar, so a single residual cannot start a block even when penalties are turned back on.backend/scripts/unblock_nodes.py: stdlib-only, edits the schema-2 snapshot (checksum verified and rewritten atomically) to reset selected or all blocked entries to reputation 1.0 / unblocked / no penalties. A script rather than a route: there is no unblock endpoint today and adding one is a separate decision. Procedure in the runbook (server stopped while editing).penalties_onfixture for the tests that assert a penalty lands; new tests for the switch, the parser, restored blocks staying as restored, and the script.Full backend suite: 4195 passed, 1 skipped, 2 failed — both pre-existing on main in untouched files (
test_no_real_identities,test_health_returns_ok). Pre-commit gate green.Deployed to the test droplet and verified there (details in the session report).
🤖 Generated with Claude Code