Skip to content

Latest commit

 

History

History
549 lines (454 loc) · 24.4 KB

File metadata and controls

549 lines (454 loc) · 24.4 KB

Relay Trust Scoring System Architecture

A system for computing and publishing NIP-XX relay trust assertions (kind 30385), combining direct probing, NIP-66 monitor data, user reports, and operator verification.

System Overview

┌──────────────────────────────────────────────────────────────────────────────┐
│                              Data Sources                                     │
├─────────────────────┬─────────────────────┬──────────────────────────────────┤
│   NIP-66 Monitors   │   User Reports      │        Direct Probing            │
│   (kind 30166)      │   (kind 1985)       │        (WebSocket + NIP-11)      │
│                     │                     │                                  │
│   - uptime/RTT      │   - spam flags      │   - connectivity test            │
│   - supported NIPs  │   - censorship      │   - latency measurement          │
│   - geolocation     │   - unreliable      │   - NIP-11 metadata              │
│   - requirements    │   - malicious       │   - operator pubkey              │
└─────────┬───────────┴─────────┬───────────┴────────────────┬─────────────────┘
          │                     │                            │
          ▼                     ▼                            ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                           Ingestion Layer                                     │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────────────────┐   │
│  │    ingestor.ts  │  │report-ingestor  │  │       prober.ts             │   │
│  │                 │  │      .ts        │  │                             │   │
│  │ Subscribe to    │  │ Subscribe to    │  │ WebSocket connectivity      │   │
│  │ kind 30166 from │  │ kind 1985 with  │  │ REQ/EOSE timing test        │   │
│  │ trusted monitors│  │ L=relay-report  │  │ NIP-11 HTTPS fetch          │   │
│  └────────┬────────┘  └────────┬────────┘  └─────────────┬───────────────┘   │
│           │                    │                         │                   │
│           └────────────────────┼─────────────────────────┘                   │
│                                ▼                                             │
│                    ┌───────────────────────┐                                 │
│                    │     database.ts       │                                 │
│                    │      (DuckDB)         │                                 │
│                    └───────────┬───────────┘                                 │
└────────────────────────────────┼─────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                          Computation Layer                                    │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │                         Scoring Pipeline                            │     │
│  │                                                                     │     │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌───────────┐  │     │
│  │  │  scorer.ts  │  │  quality-   │  │accessibility│  │ operator- │  │     │
│  │  │             │  │  scorer.ts  │  │  -scorer.ts │  │resolver.ts│  │     │
│  │  │ Reliability │  │   Quality   │  │Accessibility│  │ Operator  │  │     │
│  │  │   (40%)     │  │   (35%)     │  │   (25%)     │  │  Trust    │  │     │
│  │  │             │  │             │  │             │  │           │  │     │
│  │  │ - uptime    │  │ - policy    │  │ - barriers  │  │ - NIP-11  │  │     │
│  │  │ - resilienc │  │ - security  │  │ - limits    │  │ - DNS     │  │     │
│  │  │ - latency   │  │ - operator  │  │ - jurisdict │  │ - WoT     │  │     │
│  │  │ - consistcy │  │             │  │ - surveill  │  │           │  │     │
│  │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └─────┬─────┘  │     │
│  │         │                │                │               │        │     │
│  │         └────────────────┴────────┬───────┴───────────────┘        │     │
│  │                                   ▼                                │     │
│  │                        ┌───────────────────┐                       │     │
│  │                        │   assertion.ts    │                       │     │
│  │                        │  Build kind 30385 │                       │     │
│  │                        └───────────────────┘                       │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐     │
│  │                    Supporting Components                            │     │
│  │                                                                     │     │
│  │  policy-classifier.ts   jurisdiction.ts      wot-client.ts         │     │
│  │  (open/moderated/       (IP geolocation,     (NIP-85 trust         │     │
│  │   curated/specialized)   freedom scores)      assertions)          │     │
│  └─────────────────────────────────────────────────────────────────────┘     │
└──────────────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────────────────┐
│                          Publishing & API Layer                              │
│                                                                              │
│  ┌─────────────────────────────┐  ┌────────────────────────────────────┐    │
│  │  assertion-publisher.ts     │  │           api.ts                   │    │
│  │                             │  │                                    │    │
│  │  - Sign with provider key   │  │  - REST API endpoints              │    │
│  │  - Publish to relays        │  │  - Web dashboard UI                │    │
│  │  - Material change throttle │  │  - Rate limiting                   │    │
│  │  - Track published events   │  │  - Security headers                │    │
│  └─────────────────────────────┘  └────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────────────────┘

Project Structure

trustedrelays/
├── src/
│   ├── index.ts              # CLI entry point
│   ├── config.ts             # Configuration management
│   ├── types.ts              # TypeScript interfaces
│   ├── database.ts           # DuckDB data store
│   │
│   ├── prober.ts             # Direct relay probing
│   ├── ingestor.ts           # NIP-66 monitor data ingestion
│   ├── report-ingestor.ts    # User report ingestion (kind 1985)
│   │
│   ├── scorer.ts             # Reliability scoring
│   ├── quality-scorer.ts     # Quality scoring
│   ├── accessibility-scorer.ts # Accessibility scoring
│   │
│   ├── operator-resolver.ts  # Operator pubkey resolution
│   ├── policy-classifier.ts  # Relay policy classification
│   ├── jurisdiction.ts       # IP geolocation
│   ├── freedom-scores.ts     # Freedom House index data
│   │
│   ├── wot-client.ts         # NIP-85 WoT integration
│   ├── assertion.ts          # Kind 30385 event builder
│   ├── assertion-publisher.ts # Event signing & publishing
│   │
│   ├── appeal-processor.ts   # Handle relay appeals
│   ├── key-utils.ts          # Nostr key management
│   ├── service.ts            # Daemon orchestration
│   └── api.ts                # HTTP API & dashboard
│
├── data/                     # DuckDB database files
├── mockups/                  # UI design mockups
├── package.json
└── tsconfig.json

Database Schema (DuckDB)

-- Direct probe results
CREATE TABLE probes (
  url TEXT NOT NULL,
  timestamp INTEGER NOT NULL,
  reachable INTEGER NOT NULL,
  connect_time INTEGER,
  read_time INTEGER,
  nip11_json TEXT,
  error TEXT,
  PRIMARY KEY (url, timestamp)
);

-- NIP-66 monitor metrics
CREATE TABLE nip66_metrics (
  relay_url TEXT NOT NULL,
  monitor_pubkey TEXT NOT NULL,
  timestamp INTEGER NOT NULL,
  rtt_open INTEGER,
  rtt_read INTEGER,
  rtt_write INTEGER,
  is_online INTEGER,
  PRIMARY KEY (relay_url, monitor_pubkey, timestamp)
);

-- Trusted NIP-66 monitors
CREATE TABLE trusted_monitors (
  pubkey TEXT PRIMARY KEY,
  name TEXT,
  relay_url TEXT,
  last_seen INTEGER,
  event_count INTEGER DEFAULT 0
);

-- Operator pubkey mappings (with WoT scores from NIP-85)
CREATE TABLE operator_mappings (
  relay_url TEXT PRIMARY KEY,
  operator_pubkey TEXT,
  verification_method TEXT,
  verified_at INTEGER,
  confidence INTEGER,
  wot_score INTEGER,           -- NIP-85 trust score (0-100)
  wot_confidence TEXT,         -- 'low', 'medium', 'high'
  wot_provider_count INTEGER,  -- Number of assertion providers
  wot_updated_at INTEGER       -- Last WoT refresh timestamp
);

-- User reports (kind 1985)
CREATE TABLE relay_reports (
  event_id TEXT PRIMARY KEY,
  relay_url TEXT NOT NULL,
  reporter_pubkey TEXT NOT NULL,
  report_type TEXT NOT NULL,
  content TEXT,
  timestamp INTEGER NOT NULL,
  reporter_trust_weight REAL
);

-- Published assertions
CREATE TABLE published_assertions (
  relay_url TEXT PRIMARY KEY,
  event_id TEXT,
  event_json TEXT,
  score INTEGER,
  reliability INTEGER,
  quality INTEGER,
  openness INTEGER,
  confidence TEXT,
  published_at INTEGER
);

-- Score history for trends
CREATE TABLE score_history (
  relay_url TEXT NOT NULL,
  timestamp INTEGER NOT NULL,
  score INTEGER,
  reliability INTEGER,
  quality INTEGER,
  openness INTEGER,
  operator_trust INTEGER,
  PRIMARY KEY (relay_url, timestamp)
);

-- Relay jurisdictions
CREATE TABLE relay_jurisdictions (
  relay_url TEXT PRIMARY KEY,
  ip_address TEXT,
  country_code TEXT,
  country_name TEXT,
  region TEXT,
  city TEXT,
  isp TEXT,
  asn TEXT,
  is_hosting INTEGER,
  resolved_at INTEGER
);

-- On-demand relay tracking requests
CREATE TABLE requested_relays (
  url TEXT PRIMARY KEY,
  requested_at INTEGER NOT NULL,
  requested_by TEXT
);

Scoring Algorithm

Overall Score Composition

Overall Score = (Reliability × 0.40) + (Quality × 0.35) + (Accessibility × 0.25)

Reliability Score (40% weight)

Measures operational stability from probe data and NIP-66 metrics.

Reliability = (Uptime × 0.40) + (Resilience × 0.20) + (Consistency × 0.20) + (Latency × 0.20)
Component Calculation
Uptime % of successful probes over observation period (temporally weighted)
Resilience 100 - OutageSeverity - FrequencyPenalty - FlappingPenalty (see below)
Consistency Inverse of connection time variance (stable = better)
Latency Tiered scoring based on absolute latency (≤50ms=100, ≤200ms=85, ≤500ms=60, etc.)

Resilience calculation (designed for hourly probe granularity):

  • OutageSeverity: Sum of severity points per outage based on consecutive failed probes: 1 fail→2pts, 2-3→6pts, 4-6→15pts, 7-12→25pts, 13-24→40pts, 24+→60pts. Cap: 60.
  • FrequencyPenalty: 2 pts × distinct outage events, max 20. Catches "constantly flaky" relays.
  • FlappingPenalty: 3 pts × state changes in 6-hour window, max 15. Penalizes instability.

Temporal weighting: Recent observations weighted more heavily using exponential decay with 3-day half-life (minimum weight 0.1). This ensures recent behavior has more impact than old data.

Offline relay decay: When a relay is currently unreachable, its reliability score decays linearly over 30 days to a 20% floor. A relay offline for 15 days retains ~60% of its historical score; after 30+ days it's capped at 20%.

Data fusion: Probe data (30%) + NIP-66 data (70%) when both available. Stale monitors (inactive 30+ days) are excluded.

Quality Score (35% weight)

Evaluates relay professionalism and operator accountability.

Quality = (Policy × 0.60) + (Security × 0.25) + (Operator × 0.15)
Component Calculation
Policy NIP-11 documentation completeness (name, description, contact, limits, fees)
Security TLS encryption: wss:// = 100, ws:// = 0
Operator Verification confidence + WoT trust (50/50 blend)

Policy score caps:

  • No operator identity → max 50
  • No contact info → max 70
  • No limitation docs → max 85

Accessibility Score (25% weight)

Assesses openness and freedom characteristics.

Accessibility = (Barriers × 0.40) + (Limits × 0.20) + (Jurisdiction × 0.20) + (Surveillance × 0.20)
Component Calculation
Barriers Penalties for auth (-30), payment (-40), restricted writes (-10), PoW (-5 to -15) with diminishing returns
Limits Penalty for restrictive subscription/content/message limits
Jurisdiction Freedom House internet freedom index (0-100)
Surveillance Eyes Alliance: Five Eyes (-30), Nine Eyes (-25), Fourteen Eyes (-20), Privacy-friendly (+0)

Barrier diminishing returns: When multiple barriers stack, penalties apply with decreasing weight: first at 100%, second at 50%, third at 30%, fourth+ at 20%. This prevents unrealistic scores when barriers overlap (e.g., auth + payment = 55 penalty, not 70).

Confidence Levels

Based on weighted observation count:

Level Threshold Description
high ≥500 weighted observations Reliable long-term data
medium ≥100 weighted observations Sufficient data
low <100 weighted observations Limited data, scores may change

Observation weighting:

weighted_obs = metric_count × monitor_diversity_bonus × time_factor

monitor_diversity_bonus = 1 + (unique_monitors / 10)  // range: 1.1-2.8
time_factor = 1 + (min(days, 30) / 30)                // range: 1.0-2.0

API Endpoints

Endpoint Description
GET / Web dashboard with relay table, filters, and details modal
GET /api API documentation
GET /api/health Health check
GET /api/relays List all relays with scores (rate limited: 10/min)
GET /api/relay?url=<url> Single relay details
GET /api/score?url=<url> Lightweight score only
GET /api/assertion?url=<url> Kind 30385 event JSON
GET /api/history?url=<url>&days=N Score history and trend
GET /api/countries Country distribution stats
GET /api/stats Overall statistics
GET /api/track?url=<url> Add relay to tracking
GET /api/untrack?url=<url> Remove relay from tracking

Rate limiting: 60 requests/minute per IP (10/min for /api/relays).

Security: All endpoints include CORS headers, CSP, X-Frame-Options, etc.

CLI Commands

# Probing
bun run src/index.ts probe [relay...]       # Probe specific relays
bun run src/index.ts watch [--interval N]   # Continuous probing

# Data ingestion
bun run src/index.ts discover               # Find NIP-66 monitors
bun run src/index.ts ingest                 # Ingest NIP-66 data
bun run src/index.ts reports ingest         # Ingest user reports

# Analysis
bun run src/index.ts list                   # List known relays
bun run src/index.ts stats [relay...]       # Show statistics
bun run src/index.ts history [--days N]     # Score trends
bun run src/index.ts jurisdiction           # Geolocate relays

# Publishing
bun run src/index.ts publish [--force]      # Publish assertions
bun run src/index.ts published              # List published

# Service
bun run src/index.ts daemon                 # Run as service
bun run src/index.ts api [--port N]         # Start API server

Operator Verification

Multi-method verification with confidence scores. When multiple independent sources agree on the same pubkey, confidence is increased through corroboration:

Single Source Confidence

Method Confidence Description
dns 80% TXT record at _nostr.<domain>
wellknown 75% /.well-known/nostr.json on relay domain
nip11 70% Pubkey in NIP-11 (self-attested)
claimed 20% Found but unverified

Corroborated Confidence (Multiple Sources Agree)

Sources Agreeing Confidence Description
NIP-11 + well-known 85% Two independent sources confirm
NIP-11 + DNS 90% Strong corroboration
DNS + well-known 90% Strong corroboration
All three 95% Maximum confidence without cryptographic proof

When sources disagree (different pubkeys found), the system uses the highest-confidence source but flags the disagreement for review.

WoT Score Refresh

Operator WoT scores are fetched from NIP-85 assertion providers and cached in the database. Scores are refreshed daily during the probe cycle:

  • Queries relays: wss://nip85.nostr.band, wss://relay.damus.io, wss://nos.lol, wss://relay.snort.social
  • Parallel lookups with concurrency limit (20) to avoid rate limiting
  • Scores older than 24 hours are refreshed
  • Results cached in operator_mappings table

Report Processing

User reports (kind 1985) are weighted by reporter trust:

effective_weight = (reporter_trust / 100) ^ 2  // quadratic weighting
Reporter Trust Effective Weight
100 1.00
50 0.25
20 0.04

Spam prevention:

  • Minimum reporter trust: 20
  • Rate limit: 10 reports/pubkey/day
  • Time decay: 30-day half-life
  • Minimum 3.0 weighted sum to affect score

Policy Classification

Relays are classified based on NIP-11 limitations:

Policy Criteria
open No auth, no payment, accepts most content
moderated Open but with content filtering
curated Auth or payment required
specialized Purpose-built (e.g., NIP-46 only)

Material Change Publishing

Assertions are republished when:

  • Score changes by ≥5 points
  • Confidence level changes
  • First assertion for a relay

Can be overridden with --force flag.

Configuration

Generate a config file with bun run src/index.ts config init. Key options:

{
  "provider": {
    "privateKey": "<hex_or_nsec>",
    "algorithmVersion": "v0.1.1"
  },
  "targets": {
    "relays": ["wss://relay.damus.io"],
    "discoverFromMonitors": true,
    "maxRelays": 500
  },
  "probing": {
    "concurrency": 30
  },
  "intervals": {
    "cycle": 3600
  },
  "api": {
    "enabled": true,
    "port": 3000
  }
}
Option Default Description
probing.concurrency 30 Relays to probe in parallel
intervals.cycle 3600 Seconds between probe→publish cycles
targets.discoverFromMonitors true Auto-discover relays from NIP-66
publishing.materialChangeThreshold 5 Min score change to republish
database.retentionDays 90 Days to retain historical data

Deployment

Docker

# Create config file
bun run src/index.ts config init

# Set environment variables
export NOSTR_PRIVATE_KEY=<your_hex_or_nsec_key>

# Start container
docker-compose up -d

# View logs
docker-compose logs -f

The docker-compose.yaml mounts:

  • ./data — Database persistence
  • ./config.json — Configuration (read-only)

Systemd

# Create system user
sudo useradd -r -s /bin/false trustedrelays

# Install application
sudo mkdir -p /opt/trustedrelays
sudo cp -r src public package.json bun.lock /opt/trustedrelays/
cd /opt/trustedrelays && sudo bun install --production
sudo chown -R trustedrelays:trustedrelays /opt/trustedrelays

# Configure
sudo -u trustedrelays bun run src/index.ts config init
echo "NOSTR_PRIVATE_KEY=<your_key>" | sudo tee /opt/trustedrelays/.env
sudo chmod 600 /opt/trustedrelays/.env

# Install and start service
sudo cp trustedrelays.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now trustedrelays

# View logs
sudo journalctl -u trustedrelays -f

Reverse Proxy (nginx)

For production, run the API behind nginx:

server {
    listen 443 ssl http2;
    server_name trustedrelays.example.com;

    ssl_certificate /etc/letsencrypt/live/trustedrelays.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/trustedrelays.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}