Skip to content

feat(wot): add web of trust graph service for NIP-02 follow distance - #745

Merged
Priyanshubhartistm merged 5 commits into
cameri:mainfrom
Priyanshubhartistm:feat/wot-graph-service
Sep 8, 2026
Merged

feat(wot): add web of trust graph service for NIP-02 follow distance#745
Priyanshubhartistm merged 5 commits into
cameri:mainfrom
Priyanshubhartistm:feat/wot-graph-service

Conversation

@Priyanshubhartistm

Copy link
Copy Markdown
Collaborator

Description

Adds a WotService that builds a trust graph rooted at an operator-configured seed pubkey, ingesting NIP-02 contact lists (kind 3). The graph is updated in real time as kind-3 events come in, exposes getDistance(pubkey) isTrusted(pubkey) for other parts of the relay to query, and supports a configurable trust depth plus a minimum-followers threshold for anything past direct (1-hop) follows.

Storage is Redis-backed (per-pubkey follow sets), with the in-memory graph rebuilt from Redis on demand and lazily primed from already-stored contact-list events in Postgres when a pubkey's follow set isn't cached
yet. Pure logic only, matching the issue's scope no background refresh worker is wired up in this PR.

Related Issue

Closes #626

Motivation and Context

Nostream has no way to filter or weight events by social trust today any pubkey can publish regardless of whether the relay owner actually knows them. This is the foundation piece: a queryable trust graph that later anti-spam work (adaptive PoW reductions, WoT-gated content reporting) can build on top of.

Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Signed-off-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
@changeset-bot

changeset-bot Bot commented Aug 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9806a5e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
nostream Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coveralls

coveralls commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Coverage Status

coverage: 72.227% (+0.3%) from 71.928% — Priyanshubhartistm:feat/wot-graph-service into cameri:main

@phoenix-server

Copy link
Copy Markdown
Collaborator

I reviewed the pull request for the WoT graph service. The design is sound when the service is off or at the default depth. Two blocking issues prevent a merge. Both live in wot-graph-service.ts.

1. A kind-3 event can start a full graph rebuild.

When the seed, or any pubkey already in the graph, publishes a kind-3 event, updateFollowList runs a full graph rebuild (wot-graph-service.ts:66-71). The event handler awaits this rebuild (contact-list-event-strategy.ts:36). Each rebuild does sequential Redis reads and Postgres lookups for each frontier pubkey at each depth level. Concurrent events run concurrent rebuilds. On a public relay with wot.enabled, one small event can cost many sequential cache and database operations.

Fix this in updateFollowList:

  • Share one in-flight rebuild across all callers.
  • Debounce rapid updates.
  • Do not await the rebuild in the event path.

2. One failed build breaks every later call.

ensureBuilt stores one build promise in this.building (wot-graph-service.ts:74-82). When a rebuild fails, the promise stays rejected. Every later call awaits that rejected promise and fails, even after Redis and the database recover. The service recovers only after a successful rebuild from a trusted kind-3 event, or a restart.

Fix this in ensureBuilt:

  • If a rebuild fails, clear this.building.
  • Run the next rebuild on the next call.

Non-blocking items:

  • Make sure that seedPubkey is 64 hex characters. An invalid seed gives an empty graph, so everyone is untrusted (wot-graph-service.ts:38-44).
  • minimumFollowers counts only the previous frontier, not all trusted accounts. This matters only when maxDepth is more than 2. Make the count match the documentation, or change the documentation.
  • The documentation and the settings describe refreshIntervalHours as a periodic rebuild. No code reads it. Implement that rebuild, or remove the description.

I ran the unit tests for this change. All 178 tests pass.

@phoenix-server

Copy link
Copy Markdown
Collaborator

Re-review: fix-up verified (ca2da03)

Thanks for addressing the review feedback. I re-read the diff at head 9806a5e against main, verified each prior finding, ran the touched unit specs (149 passing), and scanned the fix-up for new issues. No blocking issues remain — approving.

Prior findings

1. Full graph rebuild per kind-3 event, awaited in the event path — FIXED ✅

  • updateFollowList (src/services/wot-graph-service.ts:60-77) now awaits only the cheap Redis writes and fires the rebuild as void this.scheduleRebuild() (line 76) — fire-and-forget, never awaited by the event path.
  • Rebuilds are single-flight with coalescing: scheduleRebuild (lines 94-113) returns the in-flight promise and sets rebuildQueued instead of starting a second rebuild; on completion, .finally drains the queued flag into exactly one follow-up rebuild, so a burst of kind-3 events collapses to at most one extra rebuild.
  • Rebuilds are gated to changes that can matter: only the seed's own list or a currently-tracked pubkey's list triggers one (line 75).
  • The strategy (contact-list-event-strategy.ts:34-40) wraps the update in its own try/catch so a graph-update failure can't reject a valid, already-stored event.
  • Covered by specs ("does not block on the triggered rebuild"). I traced the race by hand — no lost-trigger window, no concurrent rebuilds, and concurrent cold-start getDistance callers all share the one in-flight build promise (no thundering herd).

2. Rejected build promise poisoned this.building until restart — FIXED ✅

  • this.building is now the chained this.rebuild().catch(...).finally(...) (lines 100-111): rejection is logged and swallowed, and .finally always clears this.building, so the next call starts a fresh attempt. Covered by the "recovers after a rebuild failure" spec.

3. seedPubkey not validated as 64-hex — FIXED ✅SEED_PUBKEY_PATTERN (line 12) checked before the BFS; invalid seed logs an error and yields an empty graph, fail-closed and loud.

4. minimumFollowers counted only the previous frontier — FIXED ✅followerCounts persists across depth levels (lines 137-152), so candidates are promoted by cumulative trusted followers; direct follows always trusted (depth === 1 ? 1 : minimumFollowers, line 157). Docs updated to match.

5. refreshIntervalHours documented but never read — PARTIAL ⚠️ — still dead config, but now explicitly labeled "Reserved for a future periodic rebuild; not yet read by any code" in all three places. Acceptable as disclosed; a follow-up should implement or remove it.

New-issue scan

No blocking issues introduced. The kind-3 interception in event-strategy-factory.ts:60-66 is correct, ContactListEventStrategy preserves all ReplaceableEventStrategy behavior (upsert/broadcast/duplicates), duplicates correctly skip the WoT update. Three P3 notes for follow-ups, none blocking:

  1. The seed pattern's /i flag accepts uppercase hex that can never match stored lowercase pubkeys — passes validation but silently builds an empty graph. Drop the i flag or normalize before use.
  2. The strategy writes unfiltered p-tag values into the follow cache (contact-list-event-strategy.ts:35), unlike the service's own extractFollowedPubkeys on the DB path — the two extraction paths disagree.
  3. After a failed rebuild, every subsequent read starts a new rebuild attempt with no backoff (lines 80-84). Bounded by single-flight, and nothing consumes getDistance yet — revisit when the first consumer lands.

CI

UNSTABLE is caused solely by the Coveralls integration-coverage gate (−0.1%); all other checks pass (Unit Tests +0.3%, Integration, Build, Lint, CodeQL, Aikido, Socket).

Verdict: APPROVE — both blockers fixed with correct implementations, non-blocking items addressed or honestly documented, failure/coalescing behavior now directly covered by specs.

@phoenix-server phoenix-server left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approve - both blocking issues fixed and verified, re-review clean.

@Priyanshubhartistm
Priyanshubhartistm merged commit 276fe16 into cameri:main Sep 8, 2026
19 of 20 checks passed
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.

feat: Web of Trust graph service

3 participants