Browse Source
- Add README.md table of contents for easier navigation - Add Curation ACL documentation section to README.md - Create detailed Curation Mode Guide (docs/CURATION_MODE_GUIDE.md) - Fix OOM during BBolt index building by closing temp file before build - Add GC calls before index building to reclaim batch buffer memory - Improve import-export.go with processJSONLEventsReturningCount - Add policy-aware import path for sync operations Files modified: - README.md: Added TOC and curation ACL documentation - docs/CURATION_MODE_GUIDE.md: New comprehensive curation mode guide - pkg/bbolt/import-export.go: Memory-safe import with deferred cleanup - pkg/bbolt/import-minimal.go: Added GC before index build - pkg/version/version: Bump to v0.48.8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main
5 changed files with 598 additions and 19 deletions
@ -0,0 +1,411 @@ |
|||||||
|
# Curation Mode Guide |
||||||
|
|
||||||
|
Curation mode is a sophisticated access control system for Nostr relays that provides three-tier publisher classification, rate limiting, IP-based flood protection, and event kind whitelisting. |
||||||
|
|
||||||
|
## Overview |
||||||
|
|
||||||
|
Unlike simple allow/deny lists, curation mode classifies publishers into three tiers: |
||||||
|
|
||||||
|
| Tier | Rate Limited | Daily Limit | Visibility | |
||||||
|
|------|--------------|-------------|------------| |
||||||
|
| **Trusted** | No | Unlimited | Full | |
||||||
|
| **Blacklisted** | N/A (blocked) | 0 | Hidden from regular users | |
||||||
|
| **Unclassified** | Yes | 50 events/day (default) | Full | |
||||||
|
|
||||||
|
This allows relay operators to: |
||||||
|
- Reward quality contributors with unlimited publishing |
||||||
|
- Block bad actors while preserving their events for admin review |
||||||
|
- Allow new users to participate with reasonable rate limits |
||||||
|
- Prevent spam floods through automatic IP-based protections |
||||||
|
|
||||||
|
## Quick Start |
||||||
|
|
||||||
|
### 1. Start the Relay |
||||||
|
|
||||||
|
```bash |
||||||
|
export ORLY_ACL_MODE=curating |
||||||
|
export ORLY_OWNERS=npub1your_owner_pubkey |
||||||
|
./orly |
||||||
|
``` |
||||||
|
|
||||||
|
### 2. Publish Configuration |
||||||
|
|
||||||
|
The relay will not accept events until you publish a configuration event. Use the web UI at `http://your-relay/#curation` or publish a kind 30078 event: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"kind": 30078, |
||||||
|
"tags": [["d", "curating-config"]], |
||||||
|
"content": "{\"dailyLimit\":50,\"ipDailyLimit\":500,\"firstBanHours\":1,\"secondBanHours\":168,\"kindCategories\":[\"social\"]}" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### 3. Manage Publishers |
||||||
|
|
||||||
|
Use the web UI or NIP-86 API to: |
||||||
|
- Trust quality publishers |
||||||
|
- Blacklist spammers |
||||||
|
- Review unclassified users by activity |
||||||
|
- Unblock IPs if needed |
||||||
|
|
||||||
|
## Configuration |
||||||
|
|
||||||
|
### Environment Variables |
||||||
|
|
||||||
|
| Variable | Default | Description | |
||||||
|
|----------|---------|-------------| |
||||||
|
| `ORLY_ACL_MODE` | `none` | Set to `curating` to enable | |
||||||
|
| `ORLY_OWNERS` | | Owner pubkeys (can configure relay) | |
||||||
|
| `ORLY_ADMINS` | | Admin pubkeys (can manage publishers) | |
||||||
|
|
||||||
|
### Configuration Event (Kind 30078) |
||||||
|
|
||||||
|
Configuration is stored as a replaceable Nostr event (kind 30078) with d-tag `curating-config`. Only owners and admins can publish configuration. |
||||||
|
|
||||||
|
```typescript |
||||||
|
interface CuratingConfig { |
||||||
|
// Rate Limiting |
||||||
|
dailyLimit: number; // Max events/day for unclassified users (default: 50) |
||||||
|
ipDailyLimit: number; // Max events/day from single IP (default: 500) |
||||||
|
|
||||||
|
// IP Ban Durations |
||||||
|
firstBanHours: number; // First offense ban duration (default: 1 hour) |
||||||
|
secondBanHours: number; // Subsequent offense ban duration (default: 168 hours / 1 week) |
||||||
|
|
||||||
|
// Kind Filtering (choose one or combine) |
||||||
|
allowedKinds: number[]; // Explicit kind numbers: [0, 1, 3, 7] |
||||||
|
allowedRanges: string[]; // Kind ranges: ["1000-1999", "30000-39999"] |
||||||
|
kindCategories: string[]; // Pre-defined categories: ["social", "dm"] |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### Event Kind Categories |
||||||
|
|
||||||
|
Pre-defined categories for convenient kind whitelisting: |
||||||
|
|
||||||
|
| Category | Kinds | Description | |
||||||
|
|----------|-------|-------------| |
||||||
|
| `social` | 0, 1, 3, 6, 7, 10002 | Profiles, notes, contacts, reposts, reactions | |
||||||
|
| `dm` | 4, 14, 1059 | Direct messages (NIP-04, NIP-17, gift wraps) | |
||||||
|
| `longform` | 30023, 30024 | Long-form articles and drafts | |
||||||
|
| `media` | 1063, 20, 21, 22 | File metadata, picture/video/audio events | |
||||||
|
| `marketplace` | 30017-30020, 1021, 1022 | Products, stalls, auctions, bids | |
||||||
|
| `groups_nip29` | 9-12, 9000-9002, 39000-39002 | NIP-29 relay-based groups | |
||||||
|
| `groups_nip72` | 34550, 1111, 4550 | NIP-72 moderated communities | |
||||||
|
| `lists` | 10000, 10001, 10003, 30000, 30001, 30003 | Mute, pin, bookmark lists | |
||||||
|
|
||||||
|
Example configuration allowing social interactions and DMs: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"kindCategories": ["social", "dm"], |
||||||
|
"dailyLimit": 100, |
||||||
|
"ipDailyLimit": 1000 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Three-Tier Classification |
||||||
|
|
||||||
|
### Trusted Publishers |
||||||
|
|
||||||
|
Trusted publishers have unlimited publishing rights: |
||||||
|
- Bypass all rate limiting |
||||||
|
- Can publish any allowed kind |
||||||
|
- Events always visible to all users |
||||||
|
|
||||||
|
**Use case**: Known quality contributors, verified community members, partner relays. |
||||||
|
|
||||||
|
### Blacklisted Publishers |
||||||
|
|
||||||
|
Blacklisted publishers are blocked from publishing: |
||||||
|
- All events rejected with `"pubkey is blacklisted"` error |
||||||
|
- Existing events become invisible to regular users |
||||||
|
- Admins and owners can still see blacklisted events (for review) |
||||||
|
|
||||||
|
**Use case**: Spammers, abusive users, bad actors. |
||||||
|
|
||||||
|
### Unclassified Publishers |
||||||
|
|
||||||
|
Everyone else falls into the unclassified tier: |
||||||
|
- Subject to daily event limit (default: 50 events/day) |
||||||
|
- Subject to IP-based flood protection |
||||||
|
- Events visible to all users |
||||||
|
- Can be promoted to trusted or demoted to blacklisted |
||||||
|
|
||||||
|
**Use case**: New users, general public. |
||||||
|
|
||||||
|
## Rate Limiting & Flood Protection |
||||||
|
|
||||||
|
### Per-Pubkey Limits |
||||||
|
|
||||||
|
Unclassified publishers are limited to a configurable number of events per day (default: 50). The count resets at midnight UTC. |
||||||
|
|
||||||
|
When a user exceeds their limit: |
||||||
|
1. Event is rejected with `"daily event limit exceeded"` error |
||||||
|
2. Their IP is flagged for potential abuse |
||||||
|
|
||||||
|
### Per-IP Limits |
||||||
|
|
||||||
|
To prevent Sybil attacks (creating many pubkeys from one IP), there's also an IP-based daily limit (default: 500 events). |
||||||
|
|
||||||
|
When an IP exceeds its limit: |
||||||
|
1. All events from that IP are rejected |
||||||
|
2. The IP is temporarily banned |
||||||
|
|
||||||
|
### Automatic IP Banning |
||||||
|
|
||||||
|
When rate limits are exceeded: |
||||||
|
|
||||||
|
| Offense | Ban Duration | Description | |
||||||
|
|---------|--------------|-------------| |
||||||
|
| First | 1 hour | Quick timeout for accidental over-posting | |
||||||
|
| Second+ | 1 week | Extended ban for repeated abuse | |
||||||
|
|
||||||
|
Ban durations are configurable via `firstBanHours` and `secondBanHours`. |
||||||
|
|
||||||
|
### Offense Tracking |
||||||
|
|
||||||
|
The system tracks which pubkeys triggered rate limits from each IP: |
||||||
|
|
||||||
|
``` |
||||||
|
IP 192.168.1.100: |
||||||
|
- npub1abc... exceeded limit at 2024-01-15 10:30:00 |
||||||
|
- npub1xyz... exceeded limit at 2024-01-15 10:45:00 |
||||||
|
Offense count: 2 |
||||||
|
Status: Banned until 2024-01-22 10:45:00 |
||||||
|
``` |
||||||
|
|
||||||
|
This helps identify coordinated spam attacks. |
||||||
|
|
||||||
|
## Spam Flagging |
||||||
|
|
||||||
|
Events can be flagged as spam without deletion: |
||||||
|
|
||||||
|
- Flagged events are hidden from regular users |
||||||
|
- Admins can review flagged events |
||||||
|
- Events can be unflagged if incorrectly marked |
||||||
|
- Original event data is preserved |
||||||
|
|
||||||
|
This is useful for: |
||||||
|
- Moderation review queues |
||||||
|
- Training spam detection systems |
||||||
|
- Preserving evidence of abuse |
||||||
|
|
||||||
|
## NIP-86 Management API |
||||||
|
|
||||||
|
All management operations use NIP-98 HTTP authentication. |
||||||
|
|
||||||
|
### Trust Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# Trust a pubkey |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"trustpubkey","params":["<pubkey_hex>"]}' |
||||||
|
|
||||||
|
# Untrust a pubkey |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"untrustpubkey","params":["<pubkey_hex>"]}' |
||||||
|
|
||||||
|
# List trusted pubkeys |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"listtrustedpubkeys","params":[]}' |
||||||
|
``` |
||||||
|
|
||||||
|
### Blacklist Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# Blacklist a pubkey |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"blacklistpubkey","params":["<pubkey_hex>"]}' |
||||||
|
|
||||||
|
# Remove from blacklist |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"unblacklistpubkey","params":["<pubkey_hex>"]}' |
||||||
|
|
||||||
|
# List blacklisted pubkeys |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"listblacklistedpubkeys","params":[]}' |
||||||
|
``` |
||||||
|
|
||||||
|
### Unclassified User Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# List unclassified users sorted by event count |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"listunclassifiedusers","params":[]}' |
||||||
|
``` |
||||||
|
|
||||||
|
Response includes pubkey, event count, and last activity for each user. |
||||||
|
|
||||||
|
### Spam Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# Mark event as spam |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"markspam","params":["<event_id_hex>"]}' |
||||||
|
|
||||||
|
# Unmark spam |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"unmarkspam","params":["<event_id_hex>"]}' |
||||||
|
|
||||||
|
# List spam events |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"listspamevents","params":[]}' |
||||||
|
``` |
||||||
|
|
||||||
|
### IP Block Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# List blocked IPs |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"listblockedips","params":[]}' |
||||||
|
|
||||||
|
# Unblock an IP |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"unblockip","params":["<ip_address>"]}' |
||||||
|
``` |
||||||
|
|
||||||
|
### Configuration Management |
||||||
|
|
||||||
|
```bash |
||||||
|
# Get current configuration |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"getcuratingconfig","params":[]}' |
||||||
|
|
||||||
|
# Set allowed kind categories |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"setallowedkindcategories","params":[["social","dm","longform"]]}' |
||||||
|
|
||||||
|
# Get allowed kind categories |
||||||
|
curl -X POST https://relay.example.com \ |
||||||
|
-H "Authorization: Nostr <nip98_token>" \ |
||||||
|
-d '{"method":"getallowedkindcategories","params":[]}' |
||||||
|
``` |
||||||
|
|
||||||
|
## Web UI |
||||||
|
|
||||||
|
The curation web UI is available at `/#curation` and provides: |
||||||
|
|
||||||
|
- **Configuration Panel**: Set rate limits, ban durations, and allowed kinds |
||||||
|
- **Publisher Management**: Trust/blacklist pubkeys with one click |
||||||
|
- **Unclassified Users**: View users sorted by activity, promote or blacklist |
||||||
|
- **IP Blocks**: View and unblock banned IP addresses |
||||||
|
- **Spam Queue**: Review flagged events, confirm or unflag |
||||||
|
|
||||||
|
## Database Storage |
||||||
|
|
||||||
|
Curation data is stored in the relay database with the following key prefixes: |
||||||
|
|
||||||
|
| Prefix | Purpose | |
||||||
|
|--------|---------| |
||||||
|
| `CURATING_ACL_CONFIG` | Current configuration | |
||||||
|
| `CURATING_ACL_TRUSTED_PUBKEY_{pubkey}` | Trusted publisher list | |
||||||
|
| `CURATING_ACL_BLACKLISTED_PUBKEY_{pubkey}` | Blacklisted publisher list | |
||||||
|
| `CURATING_ACL_EVENT_COUNT_{pubkey}_{date}` | Daily event counts per pubkey | |
||||||
|
| `CURATING_ACL_IP_EVENT_COUNT_{ip}_{date}` | Daily event counts per IP | |
||||||
|
| `CURATING_ACL_IP_OFFENSE_{ip}` | Offense tracking per IP | |
||||||
|
| `CURATING_ACL_BLOCKED_IP_{ip}` | Active IP blocks | |
||||||
|
| `CURATING_ACL_SPAM_EVENT_{eventID}` | Spam-flagged events | |
||||||
|
|
||||||
|
## Caching |
||||||
|
|
||||||
|
For performance, the following data is cached in memory: |
||||||
|
|
||||||
|
- Trusted pubkey set |
||||||
|
- Blacklisted pubkey set |
||||||
|
- Allowed kinds set |
||||||
|
- Current configuration |
||||||
|
|
||||||
|
Caches are refreshed every hour by the background cleanup goroutine. |
||||||
|
|
||||||
|
## Background Maintenance |
||||||
|
|
||||||
|
A background goroutine runs hourly to: |
||||||
|
|
||||||
|
1. Remove expired IP blocks |
||||||
|
2. Clean up old event count entries (older than 2 days) |
||||||
|
3. Refresh in-memory caches |
||||||
|
4. Log maintenance statistics |
||||||
|
|
||||||
|
## Best Practices |
||||||
|
|
||||||
|
### Starting a New Curated Relay |
||||||
|
|
||||||
|
1. Start with permissive settings: |
||||||
|
```json |
||||||
|
{"dailyLimit": 100, "ipDailyLimit": 1000, "kindCategories": ["social"]} |
||||||
|
``` |
||||||
|
|
||||||
|
2. Monitor unclassified users for a few days |
||||||
|
|
||||||
|
3. Trust active, quality contributors |
||||||
|
|
||||||
|
4. Blacklist obvious spammers |
||||||
|
|
||||||
|
5. Adjust rate limits based on observed patterns |
||||||
|
|
||||||
|
### Handling Spam Waves |
||||||
|
|
||||||
|
During spam attacks: |
||||||
|
|
||||||
|
1. The IP-based flood protection will auto-ban attack sources |
||||||
|
2. Review blocked IPs via web UI or API |
||||||
|
3. Blacklist any pubkeys that got through |
||||||
|
4. Consider temporarily lowering `ipDailyLimit` |
||||||
|
|
||||||
|
### Recovering from Mistakes |
||||||
|
|
||||||
|
- **Accidentally blacklisted someone**: Use `unblacklistpubkey` - their events become visible again |
||||||
|
- **Wrongly flagged spam**: Use `unmarkspam` - event becomes visible again |
||||||
|
- **Blocked legitimate IP**: Use `unblockip` - IP can publish again immediately |
||||||
|
|
||||||
|
## Comparison with Other ACL Modes |
||||||
|
|
||||||
|
| Feature | None | Follows | Managed | Curating | |
||||||
|
|---------|------|---------|---------|----------| |
||||||
|
| Default Access | Write | Write if followed | Explicit allow | Rate-limited | |
||||||
|
| Rate Limiting | No | No | No | Yes | |
||||||
|
| Kind Filtering | No | No | Optional | Yes | |
||||||
|
| IP Protection | No | No | No | Yes | |
||||||
|
| Spam Flagging | No | No | No | Yes | |
||||||
|
| Configuration | Env vars | Follow lists | NIP-86 | Kind 30078 events | |
||||||
|
| Web UI | Basic | Basic | Basic | Full curation panel | |
||||||
|
|
||||||
|
## Troubleshooting |
||||||
|
|
||||||
|
### "Relay not accepting events" |
||||||
|
|
||||||
|
The relay requires a configuration event before accepting any events. Publish a kind 30078 event with d-tag `curating-config`. |
||||||
|
|
||||||
|
### "daily event limit exceeded" |
||||||
|
|
||||||
|
The user has exceeded their daily limit. Options: |
||||||
|
1. Wait until midnight UTC for reset |
||||||
|
2. Trust the pubkey if they're a quality contributor |
||||||
|
3. Increase `dailyLimit` in configuration |
||||||
|
|
||||||
|
### "pubkey is blacklisted" |
||||||
|
|
||||||
|
The pubkey is on the blacklist. Use `unblacklistpubkey` if this was a mistake. |
||||||
|
|
||||||
|
### "IP is blocked" |
||||||
|
|
||||||
|
The IP has been auto-banned due to rate limit violations. Use `unblockip` if legitimate, or wait for the ban to expire. |
||||||
|
|
||||||
|
### Events disappearing for users |
||||||
|
|
||||||
|
Check if the event author has been blacklisted. Blacklisted authors' events are hidden from regular users but visible to admins. |
||||||
Loading…
Reference in new issue