mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
lists: cap login list subscriptions to the §17 read set (≤8)
The cold-login REQ storm came from list subsystems subscribing/fetching across the user's full NIP-65 relay set (often ~20) instead of the documented §17 read cap. The fetchListIncrementally paths already route through capReadRelays, but the live subscriptions and direct pool.list calls bypassed it: - userBlocks.ensureBlockListSubscription: cap the live block-list subscription - subscriptions.ensureSubscriptionListSubscription: cap the live follows-list subscription - hashtagPreferencesService: cap the relay set used by pool.list + fetch capReadRelays keeps the user's own relays first and guarantees the reserved defaults, bounded to MAX_SUBSCRIBE_RELAYS (8). Trade-off accepted: a list event living only on a user's 9th+ relay may sync on a later pass rather than at login. Regression: tests/user-blocks-relay-cap.test.mjs. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
7f6a0f3adf
commit
2c62b959fd
4 files changed
+85
-8
No files matched your search
@@ -22,7 +22,7 @@ import {
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { normalizeHashtag } from "../utils/hashtagNormalization.js";
|
||||
import { profileCache } from "../state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "../nostr/toolkit.js";
|
||||
import { DEFAULT_RELAY_URLS, capReadRelays } from "../nostr/toolkit.js";
|
||||
import { relayManager } from "../relayManager.js";
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
@@ -686,7 +686,8 @@ class HashtagPreferencesService {
|
||||
}
|
||||
|
||||
const readRelays = relayManager.getReadRelayUrls();
|
||||
const relays = readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS);
|
||||
// capReadRelays bounds to the §17 read cap (≤8) so this doesn't fan out across the full NIP-65 set at login.
|
||||
const relays = capReadRelays(readRelays.length > 0 ? readRelays : Array.from(DEFAULT_RELAY_URLS));
|
||||
if (!relays.length) {
|
||||
if (wasLoadedForUser) {
|
||||
userLogger.warn(
|
||||
|
||||
+8
-3
@@ -10,7 +10,7 @@ import {
|
||||
listVideoViewEvents,
|
||||
subscribeVideoViewEvents,
|
||||
} from "./nostrViewEventsFacade.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { DEFAULT_RELAY_URLS, capReadRelays } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import {
|
||||
buildSubscriptionListEvent,
|
||||
@@ -730,6 +730,11 @@ class SubscriptionsManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bound the live follows-list subscription to the §17 read cap (≤8, user
|
||||
// relays first + reserved defaults) so it doesn't fan out across the user's
|
||||
// full NIP-65 set during the cold-login REQ storm.
|
||||
const cappedRelays = capReadRelays(relays);
|
||||
|
||||
const key = `subscriptions:${normalized}`;
|
||||
this.subscriptionKey = key;
|
||||
|
||||
@@ -751,7 +756,7 @@ class SubscriptionsManager {
|
||||
if (manager) {
|
||||
this.subscriptionHandle = manager.subscribe({
|
||||
key,
|
||||
relays,
|
||||
relays: cappedRelays,
|
||||
filters,
|
||||
label: "subscription-list",
|
||||
onEvent: (event) => this.handleSubscriptionListEvent(event),
|
||||
@@ -762,7 +767,7 @@ class SubscriptionsManager {
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostrClient.pool,
|
||||
relays,
|
||||
relays: cappedRelays,
|
||||
filters,
|
||||
label: "subscription-list",
|
||||
onEvent: (event) => this.handleSubscriptionListEvent(event),
|
||||
|
||||
+9
-3
@@ -27,7 +27,7 @@ import {
|
||||
assertAnyRelayAccepted,
|
||||
} from "./nostrPublish.js";
|
||||
import { profileCache } from "./state/profileCache.js";
|
||||
import { DEFAULT_RELAY_URLS } from "./nostr/toolkit.js";
|
||||
import { DEFAULT_RELAY_URLS, capReadRelays } from "./nostr/toolkit.js";
|
||||
import { relayManager } from "./relayManager.js";
|
||||
import {
|
||||
DEFAULT_NIP07_ENCRYPTION_METHODS,
|
||||
@@ -741,6 +741,12 @@ class UserBlockListManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bound the live block-list subscription to the §17 read cap (user relays
|
||||
// first + reserved defaults, ≤8). Without this the subscription fanned out
|
||||
// across the user's full NIP-65 set, a primary contributor to the cold-login
|
||||
// REQ storm.
|
||||
const cappedRelays = capReadRelays(relays);
|
||||
|
||||
const key = `block-list:${normalized}`;
|
||||
this.blockListSubscriptionKey = key;
|
||||
|
||||
@@ -758,7 +764,7 @@ class UserBlockListManager {
|
||||
if (manager) {
|
||||
this.blockListSubscriptionHandle = manager.subscribe({
|
||||
key,
|
||||
relays,
|
||||
relays: cappedRelays,
|
||||
filters,
|
||||
label: "block-list",
|
||||
onEvent: (event) => this.handleBlockListEvent(event),
|
||||
@@ -769,7 +775,7 @@ class UserBlockListManager {
|
||||
return relaySubscriptionService.ensureSubscription({
|
||||
key,
|
||||
pool: nostrClient.pool,
|
||||
relays,
|
||||
relays: cappedRelays,
|
||||
filters,
|
||||
label: "block-list",
|
||||
onEvent: (event) => this.handleBlockListEvent(event),
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Regression: the live block-list subscription (ensureBlockListSubscription) used
|
||||
// the user's full NIP-65 relay set, a primary contributor to the cold-login REQ
|
||||
// storm. It must be bounded to the §17 read cap (capReadRelays, ≤8).
|
||||
//
|
||||
// Scenario (SCN-blocklist-subscription-relay-cap):
|
||||
// Given a user with 20 read relays,
|
||||
// When the live block-list subscription is (re)established,
|
||||
// Then it subscribes over at most the §17-capped relay set, not all 20.
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { userBlocks } = await import("../js/userBlocks.js");
|
||||
const { nostrClient } = await import("../js/nostrClientFacade.js");
|
||||
const { capReadRelays, MAX_SUBSCRIBE_RELAYS } = await import(
|
||||
"../js/nostr/toolkit.js"
|
||||
);
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
test("ensureBlockListSubscription caps the live subscription relay set", () => {
|
||||
const actor = "f".repeat(64);
|
||||
const manyRelays = Array.from(
|
||||
{ length: 20 },
|
||||
(_, i) => `wss://relay-${i}.example`,
|
||||
);
|
||||
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalGetManager = nostrClient.getSubscriptionManager;
|
||||
|
||||
let captured = null;
|
||||
try {
|
||||
nostrClient.pool = { close() {} };
|
||||
nostrClient.getSubscriptionManager = () => ({
|
||||
subscribe: (args) => {
|
||||
captured = args;
|
||||
return { close() {} };
|
||||
},
|
||||
});
|
||||
|
||||
userBlocks.ensureBlockListSubscription(actor, manyRelays);
|
||||
|
||||
assert.ok(captured, "subscription manager should have been invoked");
|
||||
const expected = capReadRelays(manyRelays);
|
||||
assert.deepEqual(
|
||||
captured.relays,
|
||||
expected,
|
||||
"live subscription must use the §17-capped relay set",
|
||||
);
|
||||
assert.ok(
|
||||
captured.relays.length <= MAX_SUBSCRIBE_RELAYS,
|
||||
`relay set must be capped to ${MAX_SUBSCRIBE_RELAYS}, got ${captured.relays.length}`,
|
||||
);
|
||||
assert.ok(
|
||||
captured.relays.length < manyRelays.length,
|
||||
"must not fan out across the full 20-relay NIP-65 set",
|
||||
);
|
||||
} finally {
|
||||
nostrClient.pool = originalPool;
|
||||
nostrClient.getSubscriptionManager = originalGetManager;
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user