docs: correct padding rule — bucketed throughout, not random above 256

Prior round misread Opus's recommendation: I wrote "rand(0..=256) above
256" for slots and "round up to nearest 256KB above 256KB" for body.
Body was right; slots were wrong. Correct rule: bucketed throughout.

Slot buckets: 8, 16, 32, 64, 128, 256 (power-of-2 sub-256), then
384, 512, 640, 768, ... (+128 steps above).

Body buckets: 1KB, 2KB, ..., 256KB (power-of-2 sub-256KB), then 512KB,
768KB, 1024KB, ... (+256KB steps above; aligns with future chunk size).

Stronger privacy than random: observer learns bucket, never position
within it. Stable across posts; no min-over-many-posts floor attack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-12 21:47:44 -04:00
parent 3ee20736aa
commit 9040d70bf6
3 changed files with 20 additions and 17 deletions

View file

@ -35,7 +35,7 @@ Cost: `pub_x_index` is a per-post pseudonym for the voucher-chain — leaks "the
- **Comments are encrypted.** `CEK_comments = HKDF(CEK, "comments")`. Non-FoF observers see only ciphertext + signatures on comments. Read-gating is a side effect of the same slot unwrap.
- **CDN propagation verifies before forwarding.** Four-check accept rule: valid `pub_x_index`, not in `revocation_list`, `group_sig` validates, `identity_sig` validates. Any failure → drop, do not propagate.
- **Revocation is retroactive.** Revocation diff is author-signed, appends a revoked `pub_x` entry. Every file-holder that receives the diff **deletes all comments on this post signed by that `pub_x`** from local storage, then forwards the diff. Comments in flight before the diff arrived get deleted when it catches up. Stronger than "stop forwarding" — prior garbage also goes away.
- **Hybrid slot-count padding.** Up to 256 real slots: pad to next power of 2 (smallest authors get strong bucket-grouping; e.g., 5 real → 8 published, 100 real → 128 published, 200 real → 256 published). Above 256 real slots: pad to `real_count + rand(0..=256)` (large authors get probabilistic noise; power-of-2 jumps would waste up to 50%). Dummy `pub_x` entries are included in `pub_post_set` 1:1 so `pub_post_set.len() == wrap_slots.len()`; dummy pubkeys are 32B random bytes (no one holds the matching `priv_x`, so `group_sig` verify against a dummy always fails — benign).
- **Bucketed slot-count padding.** Deterministic buckets throughout. Up to 256 real slots: next power of 2 (8, 16, 32, 64, 128, 256). Above 256: next +128-step bucket (384, 512, 640, 768, …). Author publishes `next_bucket(real_count)` slots; dummy slots fill the gap. Dummy `pub_x` entries are included in `pub_post_set` 1:1 so `pub_post_set.len() == wrap_slots.len()`; dummy pubkeys are 32B random bytes (no one holds the matching `priv_x`, so `group_sig` verify against a dummy always fails — benign).
- **Post-hoc read-access grant via author comment.** Author can widen the read-set of an already-published post by publishing an author-signed special comment that appends a new `WrapSlot` (and its `pub_post_set` entry) for a newly-vouched persona. No rotation, no republish; retroactive inclusion. See "Access-grant author comment" below.
- **`vouch_mac` retained.** Inside ciphertext. Enables author render-time and intra-circle attribution complementary to CDN-level `pub_x_index` attribution.
- **Author has their own `pub_me/priv_me`.** Treated as one of the entries in `pub_post_set`. Author signs their own comments through the same path.
@ -246,7 +246,7 @@ struct AccessGrantComment {
| `prefilter_tag` | 2 |
| **Subtotal per slot** | **~154** |
At 500 real vouchees (above the 256 inflection point): pad to `500 + rand(0..=256)` = 500756 slots × ~154B = ~77KB to ~117KB. At 200 real vouchees: pad to 256 = ~39KB. At 9 real vouchees: pad to 16 = ~2.5KB. Acceptable for a header carried once per post.
At 500 real vouchees → next bucket above 256 is 512 → 512 slots × ~154B ≈ 79KB. At 200 real vouchees → bucket 256 ≈ 39KB. At 9 real vouchees → bucket 16 ≈ 2.5KB. Worst-case in-bucket overhead: just-under-boundary case (e.g., 257 real → 384 bucket → 33% padded; 129 real → 256 bucket → 49% padded; well below pure power-of-2 doubling). Acceptable for a header carried once per post.
Per-comment CDN overhead (vs shared-keypair baseline): +64B (`group_sig`) + 4B (`pub_x_index`) + 64B (`identity_sig` — already in baseline) ≈ 68B additional. Negligible.
@ -266,11 +266,7 @@ Decision: **defer Merkle variant to PQ migration**. v1 ships inline.
The reason this is acceptable: the alternative is no propagation-level verification, which means any one admitted FoF member can DoS the mesh. Per-post pseudonym is the minimum viable disclosure for CDN-level filtering.
**Vouch-set size:** hybrid padding behaves differently in two regimes.
- **Small authors (≤256 real slots)** publish a power-of-2 count. Observer learns the bucket (e.g., `published == 128``real_count ∈ (64, 128]`). Bucket boundaries are 8, 16, 32, 64, 128, 256. Within-bucket count is fully hidden.
- **Large authors (>256 real slots)** publish `real_count + rand(0..=256)`. Each post leaks an upper bound on real_count. Across many posts, an observer's best floor estimate is `min(published_i) - 256`, which doesn't converge tighter without independent side information.
Small-author bucket boundaries are a known cost; large authors trade off a coarser noise floor against bandwidth. Both regimes hide the true count within meaningful brackets.
**Vouch-set size:** bucketed padding throughout. Observer learns the bucket; position within the bucket is fully hidden. Buckets are 8, 16, 32, 64, 128, 256 (power-of-2 sub-256) then 384, 512, 640, 768, … (+128 steps above 256). The same author's bucket is stable across posts unless they cross a boundary — so observers see "this author has between X and Y real vouchees" with no way to converge tighter from multiple posts.
**Non-FoF reader UX:** non-FoF readers see the public body + "Comments are private" affordance. Comment count is not shown (no engagement leak). Optional: a "Request access via DM" button that sends the author a note; author can respond by publishing an access-grant author comment (above) that retroactively admits the requester.
@ -306,6 +302,6 @@ Small-author bucket boundaries are a known cost; large authors trade off a coars
- Revocation diff format + CDN honor path (retroactive delete + forward).
- Access-grant author comment mechanism for post-hoc read widening.
- Per-post ephemeral-keypair generation.
- Hybrid dummy padding on both `wrap_slots` and `pub_post_set` (power-of-2 up to 256, then `count + rand(0..=256)`), shuffled together.
- Bucketed dummy padding on both `wrap_slots` and `pub_post_set` (power-of-2 up to 256, then +128 steps above), shuffled together.
- Back-compat: old clients can still render existing non-FoF posts; old comments on new FoF posts are rejected at CDN (missing required fields).
- Integration test: 3-node FoF chain (A→B→C). A posts Mode 2 FoF. B comments (accepted, propagates). C comments (accepted via B's chain). D (unrelated) signs junk with fake `pub_x_index` pointing at a real entry (rejected at first-hop CDN: `group_sig` fails). D signs junk with `pub_x_index` pointing at a dummy (rejected: `group_sig` fails against a dummy random pubkey). A revokes B's `pub_x`: B's already-propagated comments are deleted on each file-holder as the diff sweeps through; B's subsequent comments rejected at first hop. A vouches for E after the post is live and publishes access-grant author comment; E can now read + comment retroactively.