Skip to content

fix(replication)!: require close-group proofs for every prune deletion - #266

Closed
mickvandijke wants to merge 3 commits into
mainfrom
mickvandijke/fast-prune-security-assessment
Closed

mickvandijke wants to merge 3 commits into
mainfrom
mickvandijke/fast-prune-security-assessment

Conversation

@mickvandijke

@mickvandijke mickvandijke commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Linear issue

Closes V2-1825

Risk tier

  • T0 — docs / tooling / CI / pure UX-output. Repo CI only.
  • T1 — client-only, no network-facing behavior change. CI + prod compat smoke.
  • T2 — node/client logic with behavioral surface, no protocol/format/economics change. Dev testnet + ADR.
  • T3 — protocol / storage format / payments / routing. T2 evidence + adversarial testing.

Compatibility

  • Wire: none. No message, field or protocol id changes; prune audits use the existing AuditChallenge path.
  • Storage: none. No format change. Nodes far outside many keys' groups keep those records longer, until the close group proves possession.
  • API: removes the public constant replication::config::MAX_FAST_PRUNE_DELETIONS_PER_PASS. The prune-pass log line drops its fast_delete_candidates, fast_delete_budget_deferred and fast_pruned fields.

Semver impact

  • breaking
  • feature
  • fix

Test evidence

  • cargo test --lib --features test-utils replication::: 646 passed, 0 failed.
  • cargo test --test e2e --features test-utils -- --test-threads=1 prun pointer: 20 passed, 0 failed (all prune and pointer e2e tests).
  • Against the previous pruning code, the two updated e2e tests fail as intended: the pointer pruner deleted the only copy, and the chunk candidate skipped its possession audit.
  • New regression coverage: a pruner outside a complete group now keeps a record until the close group proves possession: unit test revalidation_retains_record_outside_complete_group_without_proofs; e2e prune_deletes_at_proof_threshold_and_retains_below_it (now run with a paid width the ten-node harness completes) and pruning_deletes_only_once_the_close_group_proves_it_holds_the_record (pointers).
  • Pointer pass: unit tests for the rotating cursor (a_prune_pass_starts_past_the_last_one_and_wraps_round, candidates_the_group_cannot_prove_never_keep_the_rest_from_a_turn) and for proofs that stop early (enough_proofs_are_taken_without_waiting_for_a_peer_that_stalls, a_group_that_can_no_longer_reach_the_threshold_is_given_up_at_once, a_group_too_small_to_reach_the_threshold_is_never_asked). With proofs run one at a time and no cursor, four of the five fail.
  • Dev testnet: not yet run. Required before this leaves draft. Watch disk use, the chunk prune-pass counters, audit timeouts and pointer prune counts.
  • cargo fmt --all -- --check: clean. cargo clippy --all-targets --all-features -- -D warnings: no findings in changed code. The remaining failures (chunk_store.rs significant-drop lints, Duration unit lints in pointer.rs and pointer_replication.rs test code) already fail on main with the current toolchain.

New dependency

none

ADR

  • ADR-0005 decision 11: width-20 fast path withdrawn; consequences, test plan and re-open triggers updated.
  • ADR-0016: pointer pruning text updated to match.

Mitigation / rollback

Revert this PR to restore the fast path. Watch disk usage and the prune-pass candidates, budget_deferred and audit_below_threshold counters for backlogs draining too slowly.

Summary

Every prune deletion now requires possession proofs from the current strict close group (6 of 7 at production parameters), however far outside the group the node has moved.

  • Chunks: remove the width-20 FastDelete path, its revalidation, and the no-proof shortcut in audited revalidation. Drop the now-unused width-20 lookup per stored key per pass.
  • Pointers: remove the matching "far outside the group" shortcut. others_hold is now always required. Because every pointer candidate now needs proof, the pointer pass is rebuilt so it stays fair and bounded:
    • a rotating cursor, so candidates nobody can prove yet never block the rest (before, the first 256 by address were retried forever);
    • each candidate asks its capable group members at once and stops as soon as the threshold is met or can no longer be met (before, one 30 s fetch at a time);
    • 4 candidates at once, half of this node's 8 request slots per peer. In-flight candidates mostly share one group, so pruning never queues behind itself, and repair and possession checks keep the other half;
    • each candidate gets 5 s, the storage audit's floor for one small read over a cold connection, so a pass takes at most about 5 minutes however many peers stay silent (before, up to 256 × 7 × 30 s inline in the neighbour-sync loop). A group slower than that keeps the record until a later pass.
  • Docs: ADR-0005, ADR-0016 and REPLICATION_DESIGN.md updated.

A local routing-table view says which peers are closer, not that any of them holds the bytes. Deleting on distance alone could remove the last surviving copy.

Known limit, by design. Pointer requests go only to peers known to speak pointers, learned from their own pointer messages, so older peers are never asked something they cannot decode and never penalised for the silence. A node far outside a record's group may never hear from that group, so it keeps the record: storage held, never a copy lost. This ends when the capability gate is retired, once no pre-pointer peer remains (recorded in ADR-0016's consequences).

🤖 Generated with Claude Code

mickvandijke and others added 3 commits November 3, 2026 09:46
Distance alone no longer authorises deleting a stored record. A local
routing-table view says which peers are closer, not that any of them
holds the bytes, so deleting on topology could drop the last copy.

- Chunks: remove the width-20 FastDelete path, its revalidation, and the
  no-proof shortcut in audited revalidation. Every deletion now needs
  6-of-7 possession proofs from the current strict close group. Drop
  the width-20 lookup per stored key per pass.
- Pointers: remove the matching "far outside the group" shortcut;
  others_hold is always required.
- Remove the public MAX_FAST_PRUNE_DELETIONS_PER_PASS constant and the
  fast-path prune-pass stats.
- Tests: replace the fast-path tests with regressions proving a pruner
  outside a complete paid-width group keeps records until proofs arrive.
- Docs: withdraw the fast path in ADR-0005 decision 11, update ADR-0016
  and REPLICATION_DESIGN.md.

BREAKING CHANGE: removes replication::config::MAX_FAST_PRUNE_DELETIONS_PER_PASS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every pointer prune candidate now needs close-group proof, so the pass
must not stall on candidates nobody can prove yet.

- Rotate a cursor through candidates in address order, so unprovable
  ones no longer keep the rest from a turn.
- Ask each candidate's capable group members at once; stop as soon as
  the threshold is met or can no longer be met, and skip a group with
  too few capable members entirely.
- Prove up to 32 candidates at once, each within one fetch timeout, so
  a pass is bounded however many peers stay silent.
- ADR-0016: record that far copies wait for capability until the gate
  is retired.
- Rename the chunk regression test to the retention-width input it
  actually passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Candidates come in address order, so those in flight share one group.
At 32 at once they queued behind their own requests at each peer's 8
outbound slots, which repair and possession checks share, and the
per-candidate budget counted that queueing.

- Prove 4 candidates at once, half of each peer's slots, so pruning
  never queues behind itself and repair keeps the other half.
- Give each candidate 5 s, the storage audit's floor for one small read
  over a cold connection, so a pass still ends within about 5 minutes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@MaidSafe-Hermes MaidSafe-Hermes left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 95a0afc. Coverage: 2/2 seats reviewed (sol-1 gpt-6.1-sol xhigh; glm-1 z-ai/glm-5.3); not unanimous (GLM recommends approve, sol is uncertain).

I verified the fairness concern: PointerStore::held_states sorts by address before candidate collection (src/pointer/store.rs:677-693), as next_prune_batch requires. The five-second proof bound and four-candidate cap are clear in src/replication/pointer.rs:164-181, 1906-1945; the relevant pointer unit tests cover rotation and early proof termination.

One residual safety concern is not established as a new blocker: proof results become a boolean in others_hold, then prune_candidate rechecks responsibility/commitment but not whether the proof-giving peers are still in the current close group (src/replication/pointer.rs:1941-1975). This can leave a group-change race against the docs’ current-group requirement; it merits a focused race test or peer-set revalidation.

The PR is still draft and its stated dev-testnet gate is outstanding. Exact-head CI has build/test, format, clippy and storage checks passing; Security Audit fails on crypto-bigint 0.5.5 (RUSTSEC-2026-0387), but Cargo.lock is unchanged from base, so this is inherited rather than introduced here. I am leaving a comment, not an approval.

@mickvandijke

Copy link
Copy Markdown
Member Author

Superseded by #271. It carries this PR's three commits rebased onto main (every prune deletion needs close-group proofs; the fast path and pointer far path are gone) and adds pressure-gated retention (ADR-0028): out-of-range records are erased only under disk pressure, farthest first. The pointer address cursor from this PR is replaced there by the shared erase order, which keeps the same fairness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants