DoS-resistance pass before shipping Layers 1-5. Found three concerns:
1. Receive-path lacked validation on FoF gating shape.
2. vouch_unreadable_posts queue had no upper bound.
3. Receive-path FoFClosed visibility could pair with no fof_gating.
Fix 1 (fof::validate_fof_gating_on_receive):
- Called from control::receive_post BEFORE any storage write.
- Rejects wrap_slots/pub_post_set length mismatch (preserves the
pub_x_index lookup invariant).
- Caps wrap_slots at MAX_FOF_WRAP_SLOTS=8192. Above that we assume
attacker-shaped; legitimate bucket rule maxes at ~real+128 above 256.
- Validates each WrapSlot.read_ciphertext / sign_ciphertext is
exactly 48 bytes (matches seal_wrap_slot's output).
- Caps revocation_list at MAX_FOF_REVOCATION_LIST=4096.
- Bad posts never enter storage, never get re-propagated via
neighbor-manifest diffs.
Fix 2 (fof::validate_fof_closed_has_gating):
- FoFClosed visibility + None gating is an invariant violation.
Rejected at the same receive boundary.
Fix 3 (storage::record_unreadable_post):
- Per-persona cap of MAX_UNREADABLE_PER_PERSONA=4096. Above the cap,
new posts get last_attempt_ms touched if already present but no
new INSERT. Bounds sweep-on-V_x-arrival cost.
7 new tests bring the suite to 157:
- validate_rejects_length_mismatch / oversized_slots / wrong_ciphertext
- validate_accepts_well_formed_gating / post_without_gating
- validate_fof_closed_requires_gating
- unreadable_queue_is_capped
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>