fix(lurker): PSS bin selection — base is min(PO(peer, target), L), not PO(peer, target) - #54
fix(lurker): PSS bin selection — base is min(PO(peer, target), L), not PO(peer, target)#54flotob wants to merge 2 commits into
Conversation
… PO(peer,target) Correction due to Viktor Trón (inventor of the Swarm protocol), who reviewed the receiver design and asked exactly the right question: which PO bins is the receiver subscribing to? The trie geometry gives two regimes for where a trojan mined to an L-bit prefix of target t sits on a covering peer p with b_p = PO(p, t): - b_p < L: p diverges from t at bit b_p while the trojan still agrees there, so PO(c, p) = b_p EXACTLY. One deterministic bin; the old +3-deeper window pulled three bins that cannot contain it. - b_p >= L: the trojan agrees with p through bit L and is mined noise beyond, so PO(c, p) = L + Geom(1/2) — independent of b_p, i.e. around bin L, SHALLOWER than b_p. The old b_p..=b_p+3 selection missed it entirely once b_p > L + 3. Why the mainnet runs passed anyway: the demos mined 2-byte (16-bit) targets while covering storers sat at b_p ≈ 11–14 < L (the exact-bin regime, where the old base happened to be right), and the 5-peer covering redundancy masked the per-peer misses of deeper nodes (the symmetric-rendezvous co-resident at 18 shared bits was in exactly the broken regime). The fix pins L as a receiver-side convention (PSS_MINED_PREFIX_BITS = 16 — bee's de-facto 2-byte targets) and selects per covering peer: - GSOC: exactly bin b_p (the watch target IS the chunk address; never affected by this bug). - PSS, b_p < L: exactly bin b_p. - PSS, b_p >= L: bins L..=L+PSS_BIN_WINDOW (geometric tail: 15/16 of the mass per peer, N-way covering redundancy on top). - Mixed watch: the deduplicated union. The convention also bounds candidate cost (bins >= L carry ~2^-(L-d) of a depth-d storer's ingest) and is a storability floor (L must exceed the storage depth or no storer keeps the trojan) — and at L = 24 the ENTIRE trojan-bearing backlog of a reserve is ~1-2K chunks, which makes offline mailbox sweeps feasible (future work). Bin selection is now a pure covering_bins(b_p, want_gsoc, want_pss) with regression tests for both regimes, GSOC exactness, mixed-watch union/dedup, and the empty watch.
Deep-regime mainnet verification — the fix observed liveRan the scenario this PR exists for, on mainnet at Setup: rendezvous target = a connected mainnet storer's own overlay ( Puller placement (the fix, visible): the deep peer got pullers at bins 16, 17, 18, 19 — the The pre-fix selection ( Delivery attribution (the regime, observed): all 4 messages delivered, every one through the deep peer, at bins matching Observed bins {16,16,16,18} against predicted mass (½ at 16, ¼ at 17, ⅛ at 18). Attribution is single-source: the other covering peers sit at One additional live observation matching the geometry: an initial single-send run produced no delivery — consistent with the 1/16 tail ( No-regression: With the unit tests covering both regimes and the deep regime now demonstrated live against mainnet storers, this is proven end to end. 🤖 Generated with Claude Code |
…ffline delivery Builds on the bin-selection fix (#54, Viktor Trón's correction): now that PSS pullers pull the correct trojan bins (L..L+3 in the geometric regime), those bins can be swept from the past instead of only tailed live — turning PSS reception from a live socket into a mailbox. WebSocket `GET /pss/subscribe/{topic}?history=true` (and the `LurkerSubscribe { history }` control command + `WatchState::history` it threads to) makes a fresh puller start a bounded window behind the peer's cursor rather than just PULL_BACKLOG behind it, so messages sent while the receiver was offline are recovered before live traffic. The lookback (HISTORY_BACKLOG, in binIDs) is bounded ON PURPOSE: per-bin occupancy on a depth-d storer is ~2^(22-(k-d+1)), so at the current 2-byte mining prefix (L=16) the trojan bins hold hundreds of thousands of chunks — an unbounded start=1 sweep would be a self-inflicted DoS. The window caps it at a few thousand recent chunks per bin. Crucially, it becomes a COMPLETE backlog sweep for free under the deeper (L=24) mining convention Viktor advocates: there the whole trojan-bin backlog is ~1-2K chunks, so cursor-HISTORY_BACKLOG underflows to the floor and the mailbox recovers everything. Raising the mining convention upgrades the mailbox from 'recent' to 'all' with no code change. Semantics unchanged where they should be: resume (epoch-matched handover) always continues exactly where the predecessor stopped — mailbox never rewinds a live puller; GSOC has no mailbox (a SOC has a latest value, not a message backlog) — always live; history is union-OR'd across subscribers on the shared lurker. A sweep exceeding SEEN_CAP may re-deliver its oldest chunks, within the documented at-least-once/may-duplicate contract. Tests: start_bin_id both regimes + resume override + sparse-bin underflow-to-floor; WatchState::merge_from history union; WS ?history=true wiring smoke.
|
Viktor Trón ruled on the mining-prefix convention: Re-verified end to end on mainnet at the L=24 build (release, 3-byte mined targets, directed PSS to the node's own overlay so residency is guaranteed):
One real finding worth flagging (and now in the SWIP as an informative note): 24-bit mining is ~256× a 2-byte mine. On the debug lab build it exceeds the 60s Net: the deeper convention is the right call — it's what makes the mailbox a complete backlog sweep rather than a recent window — and it's now proven on mainnet. Sender-side mining ergonomics are the one thing it costs, and that's a UX problem, not a protocol one. 🤖 Generated with Claude Code |
After discussion with Viktor Trón, keep the convention at L=16 (2-byte targets). His L=24 motivation was proof-of-work spam punishment on senders, not receiver efficiency. Two reasons 16 is the better fit for a light-node messaging client: - No receiver benefit at our residency. The deeper-prefix wins (cheaper candidate traffic, complete mailbox) require covering peers at b_p >= L. A light node's covering peers sit at b_p ~9-14, so covering_bins pulls bin b_p regardless of L — measured identical on mainnet at L=16 and L=24. L=24 buys the light-node receiver nothing. - L=24 costs the sender 256x (~2^24 hashes): seconds on a phone, and it trips the /pss/send timeout. PSS already carries an economic spam gate via the postage stamp, so the PoW punishment is largely redundant with postage and can't distinguish spammer from legitimate mobile user. Doc-only: flips PSS_MINED_PREFIX_BITS back to 16 (from the reverted L=24 experiment) with the reasoning. A deeper prefix as a deliberate network anti-spam PoW remains an open protocol-incentive question, tracked in the SWIP.
341e696 to
573a54c
Compare
…ffline delivery Builds on the bin-selection fix (#54, Viktor Trón's correction): now that PSS pullers pull the correct trojan bins (L..L+3 in the geometric regime), those bins can be swept from the past instead of only tailed live — turning PSS reception from a live socket into a mailbox. WebSocket `GET /pss/subscribe/{topic}?history=true` (and the `LurkerSubscribe { history }` control command + `WatchState::history` it threads to) makes a fresh puller start a bounded window behind the peer's cursor rather than just PULL_BACKLOG behind it, so messages sent while the receiver was offline are recovered before live traffic. The lookback (HISTORY_BACKLOG, in binIDs) is bounded ON PURPOSE: per-bin occupancy on a depth-d storer is ~2^(22-(k-d+1)), so at the current 2-byte mining prefix (L=16) the trojan bins hold hundreds of thousands of chunks — an unbounded start=1 sweep would be a self-inflicted DoS. The window caps it at a few thousand recent chunks per bin. Crucially, it becomes a COMPLETE backlog sweep for free under the deeper (L=24) mining convention Viktor advocates: there the whole trojan-bin backlog is ~1-2K chunks, so cursor-HISTORY_BACKLOG underflows to the floor and the mailbox recovers everything. Raising the mining convention upgrades the mailbox from 'recent' to 'all' with no code change. Semantics unchanged where they should be: resume (epoch-matched handover) always continues exactly where the predecessor stopped — mailbox never rewinds a live puller; GSOC has no mailbox (a SOC has a latest value, not a message backlog) — always live; history is union-OR'd across subscribers on the shared lurker. A sweep exceeding SEEN_CAP may re-deliver its oldest chunks, within the documented at-least-once/may-duplicate contract. Tests: start_bin_id both regimes + resume override + sparse-bin underflow-to-floor; WatchState::merge_from history union; WS ?history=true wiring smoke.
|
Reverted to The deciding insight — and a correction to my earlier "L=24 is 256× cheaper reception" claim: that benefit only accrues to a receiver deeply resident in the target neighborhood (covering peers at Net: 16 is the right default for a mobile light node — cheap to mine, identical receive. The mailbox (#55) is a recent-history window at any L (its docs are updated to say so honestly, rather than claiming complete-backlog which also needs deep residency). Whether the network should mandate a deeper prefix as deliberate anti-spam PoW is a genuine protocol-incentive question the Swarm designer raised and is still weighing — parked as an open question in the SWIP, since its home is the core protocol, not our light-node default. Both branches force-pushed at L=16; gate green. 🤖 Generated with Claude Code |
Credit where due
This correction comes from Viktor Trón, the inventor of the Swarm protocol, who reviewed the light-node receiver design and asked the one question that mattered: which PO bins is the receiver subscribing to? — and then pointed out that our answer was wrong in exactly the deep-peer regime.
The bug
For a trojan mined to an L-bit prefix of target
t, on a covering peerpwithb_p = PO(p, t), the trie geometry gives two regimes:pb_p..=b_p+3)b_p < Lb_p(p diverges from t atb_p; the trojan still agrees there)b_p ≥ LL + Geom(½)— independent ofb_p, i.e. around binL, shallower thanb_pb_p > L+3Why every mainnet run passed anyway: the demos mined 2-byte (16-bit) targets while covering storers sat at
b_p ≈ 11–14 < L— the accidental-correct regime — and 5-peer covering redundancy masked individual deep-peer misses (the symmetric-rendezvous co-resident node at 18 shared bits was squarely in the broken regime; its neighbors covered for it). Working, but not for the stated reason.The fix
PSS_MINED_PREFIX_BITS = 16becomes a receiver-side convention (bee's de-facto 2-byte targets, what ant's own gateway demos send), and bin selection moves into a purecovering_bins(b_p, want_gsoc, want_pss):b_p— the watch target is the chunk address; never affected.b_p < L: exactly binb_p(was:b_p..=b_p+3— 3 dead bins dropped).b_p ≥ L: binsL..=L+3(geometric tail: 15/16 of the mass per peer; the covering-set redundancy handles the rest).The convention does double duty, per Viktor's own accounting: bins
≥ Lcarry only~2^−(L−d)of a depth-dstorer's ingest (candidate try-unwrap traffic stays small), andLmust exceed the storage depth or no storer keeps the trojan at all — the "1–3 bytes, your choice" freedom in the API was a trap at both ends.Follow-up worth its own discussion: at
L = 24, the entire trojan-bearing backlog of a 4M-chunk reserve is ~1–2K chunks (~8 MB) — small enough to sweep completely on subscribe. That turns PSS reception from a live tail into an offline mailbox. Not in this PR; noted in the constant's docs.Verification
b_p > L+3, the total-miss case), GSOC exactness, mixed-watch union/dedup, empty watch.cargo clippy --workspace --all-targets -- -D warnings= 0, fmt clean, full workspace green (34 suites; 110 in ant-p2p).🤖 Generated with Claude Code