Skip to content

WoT Relay  #14

@0ceanSlim

Description

@0ceanSlim

Status

New feature — nothing WoT-related exists in grain today. There is currently no WoT config, no graph builder, no permission groups, and no "whitelist-by-WoT" toggle in the codebase. This issue is the proposal for the shape WoT should take when it lands.

Wanted before 1.0, but not top priority right now — the tracker issues it depends on (#55, #56, #57) and the existing whitelist/blacklist cleanup come first.


Promote Web of Trust from a single-toggle whitelist into its own configuration set. The real primitive isn't "WoT yes/no" — it's a set of permission groups that the admin composes from any combination of (explicit whitelist, WoT membership, WoT score thresholds, AUTH state, admin pubkey). Each group then has its own access, retention, and rate-limit policy (see #57).

Identity root

The relay acts as a first-class Nostr actor with its own pubkey (depends on #55). That pubkey is:

  • advertised in the NIP-11 document as the relay operator identity
  • the seed of the WoT graph — the admin's own follow list is the root set

WoT construction

Built by the client library (#56 outbox-model pool) which fetches follow lists via indexer relays.

  • Strict — only the admin's direct follows
  • Normal — follows-of-follows
  • Exclude large lists — drop follow lists over N entries from graph building (prevents a single mega-follower from inflating the set)
  • Score — edges/occurrences counted toward a per-pubkey score; exposed as a numeric value groups can threshold on

WoT is a signal, not a permission by itself. Groups decide how to use it.

Permission groups

A group is a rule: "does this pubkey belong?" plus the policy attached to belonging. Admins define any number of groups. At connection time, the relay walks the group list in order; the first match assigns that connection's policy.

Group membership can be built from any combination of:

  • admin — relay's own pubkey (from Implement NIP-29 (Relay-based Groups) #55)
  • explicit_whitelist: [pubkeys] — hand-picked list (existing whitelist file)
  • in_wot: true — any non-zero WoT score
  • wot_score_min: N / wot_score_max: M — score bands
  • authenticated: true — passed NIP-42
  • negate: true — inverts any of the above (useful for "everyone not in WoT")

Policy attached to a group:

Unmatched connections fall through to the implicit public group at the bottom, which the admin can configure or leave as a deny-all default.

Access + retention are decoupled

A group can be allowed to write but have a short TTL, or denied write but be allowed to read, independently. "Who's allowed" and "whose events survive" are separate axes.

Example configurations

The point of the group model: all of these — and combinations the admin invents — are expressible.

Private archive. Only WoT + explicit list can do anything, kept forever.

groups:
  - name: insiders
    match: { any: [explicit_whitelist, in_wot] }
    access: { read: allow, write: allow }
    retention: { ttl: 0 }
public:
  access: { read: deny, write: deny }

Public relay, trust-weighted retention. Everyone can read/write, but only WoT and explicit keys survive past 24h.

groups:
  - name: trusted
    match: { any: [explicit_whitelist, in_wot] }
    retention: { ttl: 0 }
  - name: high_score_only
    match: { all: [{ wot_score_min: 10 }] }
    retention: { ttl: 0 }
public:
  access: { read: allow, write: allow }
  retention: { ttl: 24h }

Purge everyone ephemerally, except explicit whitelist.

groups:
  - name: keepers
    match: { any: [explicit_whitelist, admin] }
    retention: { ttl: 0 }
public:
  access: { read: allow, write: allow }
  retention: { ephemeral: true }  # events never persisted past broadcast

Purge everyone but WoT and explicit whitelist combined.

groups:
  - name: keepers
    match: { any: [explicit_whitelist, in_wot, admin] }
    retention: { ttl: 0 }
public:
  access: { read: allow, write: allow }
  retention: { ttl: 1h }

Pure score-driven, no explicit list. Keep events from anyone the graph reached with score ≥ 5; purge everyone else after an hour.

groups:
  - name: score_kept
    match: { all: [{ wot_score_min: 5 }] }
    retention: { ttl: 0 }
public:
  retention: { ttl: 1h }

Explicit-only, WoT ignored. Disable WoT entirely; only the explicit whitelist counts.

wot: { enabled: false }
groups:
  - name: members
    match: { any: [explicit_whitelist] }
    access: { read: allow, write: allow }
    retention: { ttl: 0 }
public:
  access: { read: deny, write: deny }

Config shape (top-level)

wot:
  enabled: true
  build:
    mode: normal           # strict | normal
    max_follow_list: 1000  # exclude larger follow lists from graph
    refresh_interval: 6h
  # scoring is exposed to groups via wot_score_min/max; no global threshold here

explicit_whitelist:
  # existing whitelist.yml continues to be the source of truth
  file: whitelist.yml

groups:
  # Evaluated in order, first match wins.
  # Each group: name, match (any/all/negate over predicates), access, retention, rate_limit
  - name: admin
    match: { any: [admin] }
    access: { read: allow, write: allow }
    retention: { ttl: 0 }
    rate_limit: { tier: admin }
  # ... user-defined groups ...

public:
  # Implicit fallback for unmatched connections
  access: { read: allow, write: allow }
  retention: { ttl: 24h }
  rate_limit: { tier: public }

Migration note

Because no WoT config exists yet, this is a greenfield design — no backward-compat shim needed. The existing whitelist.pubkey_whitelist continues to work as-is; groups layer on top rather than replacing it. Admins who don't opt into groups get today's behavior.

Out of scope for this issue

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions