13 KiB
Web of Trust (WoT) Implementation Plan
Feature Description
When enabled, the relay restricts access to pubkeys that the admin follows. The admin's kind 3 (contact list) event is used as the source of truth — all p tags in that event become whitelisted pubkeys. The admin themselves is always whitelisted.
Single config variable wot_enabled with three levels:
| Value | Mode | Effect |
|---|---|---|
0 |
Off | Open relay — anyone can read and write |
1 |
Write-only | Only followed pubkeys can publish events. Anyone can read/subscribe. |
2 |
Full | Only followed pubkeys can publish AND subscribe. Requires NIP-42 auth for subscriptions. Eliminates subscription churn from anonymous connections. |
How It Works
WoT Sync Flow
flowchart TD
A["Admin publishes kind 3 event\n- contact list -"] --> B{wot_enabled > 0?}
B -->|No| C["Store event normally"]
B -->|Yes| D["Extract p tags from kind 3"]
D --> E["Clear existing WoT whitelist rules"]
E --> F["Insert new whitelist rules\nfor each followed pubkey"]
F --> G["Enable auth_enabled"]
G --> H{wot_enabled == 2?}
H -->|Yes| I["Enable nip42_auth_required_subscriptions"]
H -->|No| J["Done - write-only restriction"]
Event Publishing Flow — Write Restriction (wot_enabled >= 1)
flowchart TD
A["EVENT message arrives"] --> B{auth_enabled?}
B -->|No| C["Accept event"]
B -->|Yes| D["check_database_auth_rules - pubkey -"]
D --> E{Pubkey in whitelist\nor wot_whitelist?}
E -->|Yes| C
E -->|No| F{Any whitelist rules exist?}
F -->|Yes| G["REJECT: not whitelisted"]
F -->|No| C
Subscription Flow — Read Restriction (wot_enabled == 2)
flowchart TD
A["REQ message arrives"] --> B{wot_enabled == 2?}
B -->|No| C["Allow subscription"]
B -->|Yes| D{NIP-42 authenticated?}
D -->|No| E["Send AUTH challenge"]
D -->|Yes| F["check_database_auth_rules\nusing authenticated_pubkey"]
F --> G{Pubkey in whitelist\nor wot_whitelist?}
G -->|Yes| C
G -->|No| H["REJECT: not authorized\nfor subscriptions"]
Design Decisions
Leveraging Existing Infrastructure
The relay already has everything needed:
auth_rulestable — stores whitelist/blacklist rules withrule_type,pattern_type,pattern_valuecheck_database_auth_rules()inrequest_validator.c:529— already implements the logic: "if whitelist rules exist and pubkey is not whitelisted, deny"add_auth_rule_from_config()/remove_auth_rule_from_config()inconfig.c— already manage auth rulesevent_tagstable — can efficiently queryptags from kind 3 events- Config system —
auth_enabledflag already controls whether auth rules are checked - NIP-42 auth —
nip42_auth_required_subscriptionsalready gates REQ access,pss->authenticated_pubkeystores the authenticated pubkey
WoT-Specific Whitelist Rules
To distinguish WoT-generated whitelist rules from manually-added ones, we use a new rule_type value: wot_whitelist. This allows:
- Clearing all WoT rules without affecting manual whitelist/blacklist rules
- Querying WoT status separately
- The existing
check_database_auth_rules()function already checks forrule_type = 'whitelist'— we need to also matchwot_whitelistin the whitelist check
Trigger Mechanism
The WoT sync happens when:
- A kind 3 event from the admin is stored — detected in
store_event()after successful INSERT - Startup — if
wot_enabled > 0, sync from the most recent admin kind 3 event in the database - Admin DM command —
wot syncto force a manual resync
What Gets Whitelisted
- All pubkeys in
ptags of the admin's kind 3 event - The admin pubkey itself (always whitelisted)
- The relay pubkey (always whitelisted — for admin DM responses)
What Is NOT Blocked
Even with WoT enabled, these are always allowed:
- Admin events (kind 23456) — already bypassed in
request_validator.c:303 - NIP-42 auth events (kind 22242) — already bypassed in
request_validator.c:318 - Kind 3 events from the admin — needed to update the follow list itself
- Kind 1059 gift wraps addressed to the relay — needed for admin DMs
Files to Modify
| File | Changes |
|---|---|
src/default_config_event.h |
Add wot_enabled config key (default: 0) |
src/config.c |
Add wot_sync_from_admin_kind3() function |
src/main.c |
Add WoT trigger in store_event() when admin kind 3 is stored, add startup WoT sync |
src/request_validator.c |
Update whitelist SQL to also match wot_whitelist rule type |
src/sql_schema.h |
Update auth_rules CHECK constraint to include wot_whitelist |
src/websockets.c |
Add WoT pubkey check after NIP-42 auth check in REQ handler |
src/dm_admin.c |
Add wot 0, wot 1, wot 2, wot sync, wot status DM commands |
Detailed Changes
1. Schema: src/sql_schema.h
Update the auth_rules table CHECK constraint to allow wot_whitelist:
-- Before:
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required'))
-- After:
rule_type TEXT NOT NULL CHECK (rule_type IN ('whitelist', 'blacklist', 'rate_limit', 'auth_required', 'wot_whitelist'))
2. Config: src/default_config_event.h
Add WoT configuration:
// Web of Trust Settings
// 0 = off, 1 = write-only (followed pubkeys can publish), 2 = full (followed pubkeys can publish AND subscribe)
{"wot_enabled", "0"},
3. Core WoT Sync Function: src/config.c
New function wot_sync_from_admin_kind3():
int wot_sync_from_admin_kind3(void) {
int wot_level = get_config_int("wot_enabled", 0);
if (wot_level <= 0) return 0; // WoT disabled
// 1. Get admin pubkey from config
// 2. Query event_tags for p tags from admin's latest kind 3 event:
// SELECT DISTINCT et.tag_value FROM event_tags et
// JOIN events e ON et.event_id = e.id
// WHERE e.kind = 3 AND e.pubkey = ? AND et.tag_name = 'p'
// ORDER BY e.created_at DESC
// 3. BEGIN TRANSACTION
// 4. DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'
// 5. INSERT wot_whitelist for admin pubkey
// 6. INSERT wot_whitelist for relay pubkey
// 7. For each p tag: INSERT INTO auth_rules (rule_type, pattern_type, pattern_value)
// VALUES ('wot_whitelist', 'pubkey', ?)
// 8. COMMIT
// 9. Set auth_enabled = true
// 10. If wot_level == 2: set nip42_auth_required_subscriptions = true
// 11. Log count of whitelisted pubkeys
return 0;
}
4. Trigger on Kind 3 Store: src/main.c
In store_event(), after successful INSERT and after store_event_tags():
// Check if this is a kind 3 event from the admin — trigger WoT sync
if ((int)cJSON_GetNumberValue(kind) == 3) {
int wot_level = get_config_int("wot_enabled", 0);
if (wot_level > 0) {
const char* admin_pubkey = get_config_value("admin_pubkey");
if (admin_pubkey && strcmp(cJSON_GetStringValue(pubkey), admin_pubkey) == 0) {
DEBUG_INFO("Admin kind 3 event stored — triggering WoT sync");
wot_sync_from_admin_kind3();
}
if (admin_pubkey) free((char*)admin_pubkey);
}
}
5. Startup WoT Sync: src/main.c
In main(), after populate_event_tags_from_existing():
// Sync Web of Trust whitelist if enabled
int wot_level = get_config_int("wot_enabled", 0);
if (wot_level > 0) {
wot_sync_from_admin_kind3();
}
6. Update Whitelist Check: src/request_validator.c
Update the whitelist SQL queries to also match wot_whitelist:
// Before:
"SELECT rule_type FROM auth_rules WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"
// After:
"SELECT rule_type FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') AND pattern_type = 'pubkey' AND pattern_value = ? AND active = 1 LIMIT 1"
And the whitelist-exists check:
// Before:
"SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'whitelist' AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"
// After:
"SELECT COUNT(*) FROM auth_rules WHERE rule_type IN ('whitelist', 'wot_whitelist') AND pattern_type = 'pubkey' AND active = 1 LIMIT 1"
7. REQ Handler WoT Check: src/websockets.c
When wot_enabled == 2, add a pubkey whitelist check after the existing NIP-42 auth check in the REQ handler. The existing code at line 903 already requires NIP-42 auth when nip42_auth_required_subscriptions is set. We add a WoT check right after:
// Existing NIP-42 auth check (line 903):
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
// ... send AUTH challenge or NOTICE ...
return 0;
}
// NEW: WoT read restriction check (wot_enabled == 2)
if (pss && pss->authenticated && get_config_int("wot_enabled", 0) == 2) {
// Client is authenticated — check if their pubkey is in the WoT whitelist
int wot_result = check_database_auth_rules(pss->authenticated_pubkey, "subscription", NULL);
if (wot_result != NOSTR_SUCCESS) {
send_notice_message(wsi, pss, "restricted: your pubkey is not in this relay's web of trust");
DEBUG_INFO("REQ rejected: pubkey %s not in WoT whitelist", pss->authenticated_pubkey);
cJSON_Delete(json);
return 0;
}
}
8. Admin DM Commands: src/dm_admin.c
Add plain text DM commands:
| Command | Action |
|---|---|
wot 0 or wot off |
Disable WoT, delete all wot_whitelist rules, reset nip42_auth_required_subscriptions |
wot 1 or wot write |
Write-only WoT — sync follow list, set auth_enabled=true |
wot 2 or wot full |
Full WoT — sync follow list, set auth_enabled=true, set nip42_auth_required_subscriptions=true |
wot sync |
Force resync from admin's kind 3 event |
wot status |
Show WoT level (0/1/2), count of whitelisted pubkeys |
9. NIP-11 Update
When WoT is enabled (level 1 or 2), the relay should indicate this in NIP-11 relay info. Add to the limitation object:
"auth_required": true
Edge Cases
-
Admin has no kind 3 event in database: WoT sync does nothing, logs a warning. No whitelist rules created = open relay.
-
Admin updates follow list: New kind 3 event triggers full resync — old WoT rules are cleared, new ones inserted. This is atomic (transaction).
-
WoT disabled (set to 0): Clears all
wot_whitelistrules. Manualwhitelistrules are preserved.nip42_auth_required_subscriptionsis reset to false. -
Admin unfollows someone: Next kind 3 event triggers resync, the unfollowed pubkey's
wot_whitelistrule is removed (full clear + reinsert). -
Kind 1059 gift wraps: These need special handling — the relay needs to accept gift wraps addressed to it even from non-whitelisted pubkeys (for admin DMs). The
is_nip17_gift_wrap_for_relay()check should bypass WoT. -
Read restriction without NIP-42 support (level 2): If a client doesn't support NIP-42, they cannot authenticate and therefore cannot subscribe. This is by design — it's the most effective way to reduce subscription churn from anonymous connections.
-
NIP-11 discovery: Clients can check NIP-11 to see if
auth_requiredis true before connecting, avoiding wasted connections. -
Changing level from 2 to 1:
nip42_auth_required_subscriptionsis reset to false, allowing anonymous subscriptions again. WoT whitelist rules remain for write restriction.
Testing Strategy
- Enable write-only WoT via DM:
wot 1 - Verify admin can still publish events
- Verify followed pubkeys can publish events
- Verify non-followed pubkeys are rejected for EVENT
- Verify non-followed pubkeys can still subscribe (REQ)
- Enable full WoT via DM:
wot 2 - Verify unauthenticated clients get AUTH challenge on REQ
- Verify authenticated non-followed pubkeys are rejected for REQ
- Verify authenticated followed pubkeys can subscribe
- Verify
wot statusshows correct level and count - Publish new kind 3 event, verify auto-resync
wot 0— verify all pubkeys can publish and subscribe again- Verify admin DMs still work when WoT is enabled
Performance Considerations
- WoT sync is O(n) where n = number of follows (typically 100-2000)
- Uses a transaction for bulk insert — fast even for large follow lists
- Auth rule check is already indexed:
idx_auth_rules_pattern ON auth_rules(pattern_type, pattern_value) - No additional per-event overhead — the existing
check_database_auth_rules()already runs on every event when auth is enabled - Level 2 directly addresses CPU load: With
wot_enabled=2, unauthenticated connections cannot create subscriptions, eliminating the ~120 REQ/minute churn from anonymous connections that was causing 99% CPU