fix(pruning): make prune candidacy independent of repair-hint history#175
Merged
Conversation
Production nodes accumulate out-of-range chunks indefinitely: storage admission reaches width 20, retained-storage responsibility uses width 9, commitments cover the strict closest 7, and only ~35% (~7/20) of stored chunks appear in current commitments — yet almost no records reach prune audits or deletion. Cause: prune candidacy required six mature neighbor-sync repair-hint proofs among the key's close group before a record could even be selected for a prune audit. Repair hints are sent to the ~20 peers closest to SELF, while prune confirmation concerns the 7 peers closest to the KEY; for a record outside width 9 nothing guarantees those sets overlap, so candidates were silently deferred forever. The commitment-retention veto additionally blocked the START of the 3-day hysteresis rather than only deletion, and none of the intermediate states were visible at INFO level. Fix: - Candidacy now strictly means "continuously outside the storage- retention width (9) for PRUNE_HYSTERESIS_DURATION". It never depends on repair proofs, bootstrap state, audit budget, or prior neighbor-sync contact. - The out-of-range timestamp is recorded the moment a record leaves range, even while a retained commitment still holds the key; retention vetoes deletion only. - Prune-confirmation audits challenge the CURRENT strict closest 7 straight from the local routing table, never filtered through RepairProofs. The repair-proof maturity gate is unchanged for the responsible-chunk anti-outsourcing audit. - Deletion still requires 6-of-7 valid nonce-bound possession proofs, plus a pre-deletion revalidation against the current routing table (width-9 re-check, retention re-check, current-close-group proof re-check). Bootstrap gating, the per-pass challenge budget, and commitment retention are audit/deletion deferrals that preserve candidacy and the first-seen time. - Explicit lifecycle states (InRange, HysteresisPending, Candidate, HeldByCommitment, BootstrapDeferred, BudgetDeferred, AuditFailed, Pruned) and one aggregate INFO census per pass. Unit and E2E regression coverage added; no prune test manufactures repair proofs anymore, and the threshold E2E test now runs the production-width scenario (close group 7, retention width 9, empty RepairProofs, 5/7 retains, 6/7 deletes). SemVer: patch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes stored-record pruning liveness by making prune candidacy and audit targeting independent of neighbor-sync repair-hint history, and by ensuring the out-of-range hysteresis clock starts immediately (even when deletion is temporarily vetoed by retained commitments). It also adds an explicit prune lifecycle/state model and richer per-pass telemetry to make stalls observable in production.
Changes:
- Refactors stored-record pruning to make candidacy unconditional after hysteresis and to audit the current strict close group directly from the routing table (removing prune-path repair-proof gating).
- Ensures commitment retention only vetoes deletion (and skips audit) rather than blocking the hysteresis timer from starting.
- Updates E2E/unit tests and replication design docs to reflect the new lifecycle, targeting, and telemetry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| tests/e2e/replication.rs | Removes test-only repair-proof manufacturing and asserts new prune lifecycle/telemetry behaviors against live nodes. |
| src/replication/pruning.rs | Implements the new prune lifecycle state machine, unconditional candidacy, direct close-group audit targeting, and expanded per-pass metrics/logging. |
| src/replication/mod.rs | Removes pruning’s dependency on neighbor-sync epoch/proof-time fields and simplifies constant usage/imports. |
| docs/REPLICATION_DESIGN.md | Updates rules and scenarios to scope repair-proof prerequisites to responsible-chunk audits only, and documents the new prune lifecycle and widths taxonomy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jacderida
approved these changes
Jul 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production symptom
Nodes accumulate out-of-range chunks indefinitely. The widths tell the story:
SELF_CLOSENESS_GATE_WIDTH = K_BUCKET_SIZE);close_group_size + STORAGE_ADMISSION_MARGIN);Root cause
Two defects in
src/replication/pruning.rs:Repair-proof-gated candidacy (introduced in 4cce554, retuned in d04c5aa). After the 3-day hysteresis,
evaluate_record_prune_keyfiltered the strict close group throughpeers_with_mature_repair_proofsand, when fewer than 6 of 7 peers had a mature repair-hint proof, silently dropped the key from consideration (adebug!line only). Repair proofs are created exclusively when this node sends replica hints during neighbor sync — and neighbor sync contacts theNEIGHBOR_SYNC_SCOPE = 20peers closest to self, while prune confirmation concerns the 7 peers closest to the key. For a record outside width 9 there is no overlap guarantee between those sets, so six mature proofs could never accumulate: the record never became a candidate, was never audited, and was never deleted. Prune liveness depended on unrelated, self-centric neighbor-sync history.Retention veto blocked the hysteresis clock. The
commitment_state.is_heldcheck returned beforeset_record_out_of_range, so commitment retention postponed the start of the 3-day hysteresis instead of only vetoing deletion — serializing every prune behind the gossip retention window.A third, contributing problem: every intermediate state (hysteresis pending, proof-deferred, budget/bootstrap-deferred, held) was debug-level or absent, so the stall was invisible in production logs.
What changed
Candidacy is now unconditional. A record continuously outside the storage-retention width (9) for
PRUNE_HYSTERESIS_DURATIONis a prune candidate — full stop. It never depends on repair-hint proofs, bootstrap state, audit budget, or prior neighbor-sync contact.The timer starts immediately. The out-of-range first-seen timestamp is recorded the moment self is observed outside width 9, even while a retained commitment still contains the key. The commitment veto now applies to deletion (and skips the pointless audit), never to the clock. The timestamp remains process-local (restart resets it, per the conservative spec).
Audit targets come straight from the routing table. An auditable candidate challenges the CURRENT strict closest 7 to the key with the nonce-bound possession audit —
peers_with_mature_repair_proofsis deleted, andPrunePassContextloses itscurrent_sync_epoch/repair_proof_nowfields. The repair-proof maturity gate is unchanged for the responsible-chunk anti-outsourcing audit (audit.rs), where it belongs.Deletion rules are unchanged where they matter. Deletion still requires 6 of 7 valid positive possession proofs (
prune_proofs_neededuntouched); missing, timed-out, bootstrapping, malformed, absent, and digest-mismatching responses never count positively. Immediately before deletion, a revalidation (revalidate_record_prune_candidate, now a pure function) re-checks width-9 responsibility, the hysteresis, commitment retention (TOCTOU), and that the positive reports still satisfy the current close group — membership churn invalidates stale reports. Below-threshold candidates keep their record, timestamp, and candidacy for later passes.Scheduling gates are deferrals, not resets. Bootstrap state defers the audit; the per-pass challenge budget (64, unchanged — bounded per-pass network work) defers the audit; a retained answerable commitment vetoes deletion. None of them removes candidate status or restarts hysteresis.
Explicit state machine + telemetry. The lifecycle is now explicit in code —
RecordPruneKeyState::{InRange, HysteresisPending, Candidate},PruneCandidateDisposition::{HeldByCommitment, BootstrapDeferred, BudgetDeferred, Unauditable, Auditable},PruneRevalidationOutcome::{…, AuditFailed, Delete}— and each pass emits one aggregate INFO line: total, in_range, newly_marked, cleared, hysteresis_pending, candidates, held_by_commitment, bootstrap_deferred, budget_deferred, audits_attempted, audit_below_threshold, pruned (plus paid-list counts). All counters are also new publicPruneResultfields. No per-chunk INFO lines.Paid-list entry pruning, payment verification, commitment pricing, client PUT admission, and the 6/7 threshold are untouched.
Why this restores prune liveness
Every out-of-range record now deterministically reaches an audit of the peers actually responsible for it right now, instead of waiting on neighbor-sync hint history that may never exist for key-close peers outside the node's own neighborhood. The conservative failure mode is preserved throughout: uncertainty (timeouts, absent peers, churned groups, held commitments, budget exhaustion) always retains data.
Tests
No prune test manufactures repair proofs anymore (the
record_repair_proofs_for_peersE2E helper is deleted).Unit (15 new in
pruning.rs, 34 total): immediate marking even when held by a commitment; not-a-candidate inside the hysteresis; candidacy at hysteresis with an emptyRepairProofstable targeting the full strict close group; held/bootstrap/budget deferrals preserving first-seen and budget; back-in-range clearing; revalidation 6/7-delete vs 5/7-retain; stale-close-group proof rejection; re-commit TOCTOU veto; and response grading (absent digest, digest mismatch, wrong digest count, challenge-id mismatch, Rejected, unexpected body, Bootstrapping never counts).E2E (5, all against live testnets):
test_prune_pass_requires_remote_confirmation_before_delete— bootstrap gate defers but preserves candidacy and the original first-seen across passes; below-threshold audits surface in stats; retained until every target proves possession.test_prune_veto_for_committed_out_of_range_key— the veto blocks deletion while the timer still starts; prunes once no longer committed.prune_deletes_at_proof_threshold_and_retains_below_it— the production-width scenario: close group 7, retention width 9, pruner outside both, emptyRepairProofs; 5/7 retains across repeated passes (audits attempted every pass), 6/7 deletes.prune_marks_immediately_and_candidacy_waits_for_hysteresis— real non-zero hysteresis: marked on first pass, pending with stable first-seen, then matures, audits, and prunes.paid_prune_requires_paid_close_group_confirmations— unchanged semantics, updated to the new context.Docs
docs/REPLICATION_DESIGN.md: rule 20 now scopes the repair-proof prerequisite to the responsible-chunk audit ONLY (with the self-nearest vs key-nearest rationale); rule 22, Section 11, and scenarios 36/50 rewritten around the new lifecycle; Section 11 gains an explicit taxonomy separating storage admission, retained storage responsibility, commitment inclusion, prune candidacy, prune-audit scheduling, prune deletion confirmation (a direct current-close-group possession check requiring 6/7 positive proofs), and general anti-outsourcing audits. Also fixes the stale claim that paid-list pruning is local-state-only (it has required 15-of-20 paid confirmations since d04c5aa).Validation
Rollout notes / remaining risks
budget_deferredcounter to see drain progress.HeldByCommitmentskips the audit as well as the deletion (no budget spent on undeletable candidates); if pre-warming audit evidence during the hold window is preferred, it is a one-line change inschedule_prune_candidate.🤖 Generated with Claude Code