develop registration ratelimit mechanism

This commit is contained in:
2025-11-21 19:13:18 +00:00
parent ebe0012863
commit fb65282702
6 changed files with 3157 additions and 0 deletions

View File

@@ -0,0 +1,466 @@
# FIND Name Binding Implementation Plan
## Overview
This document outlines the implementation plan for integrating the Free Internet Name Daemon (FIND) protocol with the ORLY relay. The FIND protocol provides decentralized name-to-npub bindings that are discoverable by any client using standard Nostr queries.
## Architecture
### System Components
```
┌─────────────────────────────────────────────────────────────┐
│ ORLY Relay │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ WebSocket │ │ FIND Daemon │ │ HTTP API │ │
│ │ Handler │ │ (Registry │ │ (NIP-11, Web) │ │
│ │ │ │ Service) │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └─────────────────┼────────────────────┘ │
│ │ │
│ ┌───────▼────────┐ │
│ │ Database │ │
│ │ (Badger/ │ │
│ │ DGraph) │ │
│ └────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ ▲
│ Publish FIND events │ Query FIND events
│ (kinds 30100-30105) │ (kinds 30102, 30103)
▼ │
┌─────────────────────────────────────────────────────────────┐
│ Nostr Network │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Other │ │ Other │ │ Clients │ │
│ │ Relays │ │ Registry │ │ │ │
│ │ │ │ Services │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Event Flow
1. **Name Registration:**
```
User → FIND CLI → Registration Proposal (kind 30100) → Relay → Database
Registry Service (attestation)
Attestation (kind 20100) → Other Registry Services
Consensus → Name State (kind 30102)
```
2. **Name Resolution:**
```
Client → Query kind 30102 (name state) → Relay → Database → Response
Client → Query kind 30103 (records) → Relay → Database → Response
```
## Implementation Phases
### Phase 1: Database Storage for FIND Events ✓ (Already Supported)
The relay already stores all parameterized replaceable events (kind 30xxx) and ephemeral events (kind 20xxx), which includes all FIND event types:
- ✓ Kind 30100: Registration Proposals
- ✓ Kind 20100: Attestations (ephemeral)
- ✓ Kind 30101: Trust Graphs
- ✓ Kind 30102: Name State
- ✓ Kind 30103: Name Records
- ✓ Kind 30104: Certificates
- ✓ Kind 30105: Witness Services
**Status:** No changes needed. The existing event storage system handles these automatically.
### Phase 2: Registry Service Implementation
Create a new registry service that runs within the ORLY relay process (optional, can be enabled via config).
**New Files:**
- `pkg/find/registry.go` - Core registry service
- `pkg/find/consensus.go` - Consensus algorithm implementation
- `pkg/find/trust.go` - Trust graph calculation
- `app/find-service.go` - Integration with relay server
**Key Components:**
```go
// Registry service that monitors proposals and computes consensus
type RegistryService struct {
db database.Database
pubkey []byte // Registry service identity
trustGraph *TrustGraph
pendingProposals map[string]*ProposalState
config *RegistryConfig
}
type RegistryConfig struct {
Enabled bool
ServicePubkey string
AttestationDelay time.Duration // Default: 60s
SparseAttestation bool
SamplingRate int // For sparse attestation
}
// Proposal state tracking during attestation window
type ProposalState struct {
Proposal *RegistrationProposal
Attestations []*Attestation
ReceivedAt time.Time
ProcessedAt *time.Time
}
```
**Responsibilities:**
1. Subscribe to kind 30100 (registration proposals) from database
2. Validate proposals (name format, ownership, renewal window)
3. Check for conflicts (competing proposals)
4. After attestation window (60-120s):
- Fetch attestations (kind 20100) from other registry services
- Compute trust-weighted consensus
- Publish name state (kind 30102) if consensus reached
### Phase 3: Client Query Handlers
Enhance existing query handlers to optimize FIND event queries.
**Enhancements:**
- Add specialized indexes for FIND events (already exists via `d` tag indexes)
- Implement name resolution helper functions
- Cache frequently queried name states
**New Helper Functions:**
```go
// Query name state for a given name
func (d *Database) QueryNameState(name string) (*find.NameState, error)
// Query all records for a name
func (d *Database) QueryNameRecords(name string, recordType string) ([]*find.NameRecord, error)
// Check if name is available for registration
func (d *Database) IsNameAvailable(name string) (bool, error)
// Get parent domain owner (for subdomain validation)
func (d *Database) GetParentDomainOwner(name string) (string, error)
```
### Phase 4: Configuration Integration
Add FIND-specific configuration options to `app/config/config.go`:
```go
type C struct {
// ... existing fields ...
// FIND registry service settings
FindEnabled bool `env:"ORLY_FIND_ENABLED" default:"false" usage:"enable FIND registry service for name consensus"`
FindServicePubkey string `env:"ORLY_FIND_SERVICE_PUBKEY" usage:"public key for this registry service (hex)"`
FindServicePrivkey string `env:"ORLY_FIND_SERVICE_PRIVKEY" usage:"private key for signing attestations (hex)"`
FindAttestationDelay string `env:"ORLY_FIND_ATTESTATION_DELAY" default:"60s" usage:"delay before publishing attestations"`
FindSparseEnabled bool `env:"ORLY_FIND_SPARSE_ENABLED" default:"false" usage:"use sparse attestation (probabilistic)"`
FindSamplingRate int `env:"ORLY_FIND_SAMPLING_RATE" default:"10" usage:"sampling rate for sparse attestation (1/K)"`
FindBootstrapServices []string `env:"ORLY_FIND_BOOTSTRAP_SERVICES" usage:"comma-separated list of bootstrap registry service pubkeys"`
}
```
### Phase 5: FIND Daemon HTTP API
Add HTTP API endpoints for FIND operations (optional, for user convenience):
**New Endpoints:**
- `GET /api/find/names/:name` - Query name state
- `GET /api/find/names/:name/records` - Query all records for a name
- `GET /api/find/names/:name/records/:type` - Query specific record type
- `POST /api/find/register` - Submit registration proposal
- `POST /api/find/transfer` - Submit transfer proposal
- `GET /api/find/trust-graph` - Query this relay's trust graph
**Implementation:**
```go
// app/handle-find-api.go
func (s *Server) handleFindNameQuery(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
// Validate name format
if err := find.ValidateName(name); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Query name state from database
nameState, err := s.DB.QueryNameState(name)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if nameState == nil {
http.Error(w, "name not found", http.StatusNotFound)
return
}
// Return as JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(nameState)
}
```
### Phase 6: Client Integration Examples
Provide example code for clients to use FIND:
**Example: Query name ownership**
```javascript
// JavaScript/TypeScript example using nostr-tools
import { SimplePool } from 'nostr-tools'
async function queryNameOwner(relays, name) {
const pool = new SimplePool()
// Query kind 30102 events with d tag = name
const events = await pool.list(relays, [{
kinds: [30102],
'#d': [name],
limit: 5
}])
if (events.length === 0) {
return null // Name not registered
}
// Check for majority consensus among registry services
const ownerCounts = {}
for (const event of events) {
const ownerTag = event.tags.find(t => t[0] === 'owner')
if (ownerTag) {
const owner = ownerTag[1]
ownerCounts[owner] = (ownerCounts[owner] || 0) + 1
}
}
// Return owner with most attestations
let maxCount = 0
let consensusOwner = null
for (const [owner, count] of Object.entries(ownerCounts)) {
if (count > maxCount) {
maxCount = count
consensusOwner = owner
}
}
return consensusOwner
}
// Example: Resolve name to IP address
async function resolveNameToIP(relays, name) {
const owner = await queryNameOwner(relays, name)
if (!owner) {
throw new Error('Name not registered')
}
// Query kind 30103 events for A records
const pool = new SimplePool()
const records = await pool.list(relays, [{
kinds: [30103],
'#name': [name],
'#type': ['A'],
authors: [owner], // Only records from name owner are valid
limit: 5
}])
if (records.length === 0) {
throw new Error('No A records found')
}
// Extract IP addresses from value tags
const ips = records.map(event => {
const valueTag = event.tags.find(t => t[0] === 'value')
return valueTag ? valueTag[1] : null
}).filter(Boolean)
return ips
}
```
**Example: Register a name**
```javascript
import { finalizeEvent, getPublicKey } from 'nostr-tools'
import { find } from './find-helpers'
async function registerName(relays, privkey, name) {
// Validate name format
if (!find.validateName(name)) {
throw new Error('Invalid name format')
}
const pubkey = getPublicKey(privkey)
// Create registration proposal (kind 30100)
const event = {
kind: 30100,
created_at: Math.floor(Date.now() / 1000),
tags: [
['d', name],
['action', 'register'],
['expiration', String(Math.floor(Date.now() / 1000) + 300)] // 5 min expiry
],
content: ''
}
const signedEvent = finalizeEvent(event, privkey)
// Publish to relays
const pool = new SimplePool()
await Promise.all(relays.map(relay => pool.publish(relay, signedEvent)))
// Wait for consensus (typically 1-2 minutes)
console.log('Registration proposal submitted. Waiting for consensus...')
await new Promise(resolve => setTimeout(resolve, 120000))
// Check if registration succeeded
const owner = await queryNameOwner(relays, name)
if (owner === pubkey) {
console.log('Registration successful!')
return true
} else {
console.log('Registration failed - another proposal may have won consensus')
return false
}
}
```
## Testing Plan
### Unit Tests
1. **Name Validation Tests** (`pkg/find/validation_test.go` - already exists)
- Valid names
- Invalid names (too long, invalid characters, etc.)
- Subdomain authority validation
2. **Consensus Algorithm Tests** (`pkg/find/consensus_test.go` - new)
- Single proposal scenario
- Competing proposals
- Trust-weighted scoring
- Attestation window expiry
3. **Trust Graph Tests** (`pkg/find/trust_test.go` - new)
- Direct trust relationships
- Multi-hop trust inheritance
- Trust decay calculation
### Integration Tests
1. **End-to-End Registration** (`pkg/find/integration_test.go` - new)
- Submit proposal
- Generate attestations
- Compute consensus
- Verify name state
2. **Name Renewal** (`pkg/find/renewal_test.go` - new)
- Renewal during preferential window
- Rejection outside renewal window
- Expiration handling
3. **Record Management** (`pkg/find/records_test.go` - new)
- Publish DNS-style records
- Verify owner authorization
- Query records by type
### Performance Tests
1. **Concurrent Proposals** - Benchmark handling 1000+ simultaneous proposals
2. **Trust Graph Calculation** - Test with 10,000+ registry services
3. **Query Performance** - Measure name resolution latency
## Deployment Strategy
### Development Phase
1. Implement core registry service (Phase 2)
2. Add unit tests
3. Test with local relay and simulated registry services
### Testnet Phase
1. Deploy 5-10 test relays with FIND enabled
2. Simulate various attack scenarios (Sybil, censorship, etc.)
3. Tune consensus parameters based on results
### Production Rollout
1. Documentation and client libraries
2. Enable FIND on select relays (opt-in)
3. Monitor for issues and gather feedback
4. Gradual adoption across relay network
## Security Considerations
### Attack Mitigations
1. **Sybil Attacks**
- Trust-weighted consensus prevents new services from dominating
- Age-weighted trust (new services have reduced influence)
2. **Censorship**
- Diverse trust graphs make network-wide censorship difficult
- Users can query different registry services aligned with their values
3. **Name Squatting**
- Mandatory 1-year expiration
- Preferential 30-day renewal window
- No indefinite holding
4. **Renewal Window DoS**
- 30-day window reduces attack surface
- Owner can submit multiple renewal attempts
- Registry services filter by pubkey during renewal window
### Privacy Considerations
- Registration proposals are public (necessary for consensus)
- Ownership history is permanently visible
- Clients can use Tor or private relays for sensitive queries
## Documentation Updates
1. **User Guide** (`docs/FIND_USER_GUIDE.md` - new)
- How to register a name
- How to manage DNS records
- How to renew registrations
- Client integration examples
2. **Operator Guide** (`docs/FIND_OPERATOR_GUIDE.md` - new)
- How to enable FIND registry service
- Trust graph configuration
- Monitoring and troubleshooting
- Bootstrap recommendations
3. **Developer Guide** (`docs/FIND_DEVELOPER_GUIDE.md` - new)
- API reference
- Client library examples (JS, Python, Go)
- Event schemas and validation
- Consensus algorithm details
4. **Update CLAUDE.md**
- Add FIND sections to project overview
- Document new configuration options
- Add testing instructions
## Success Metrics
- **Registration Finality:** < 2 minutes for 95% of registrations
- **Query Latency:** < 100ms for name lookups
- **Consensus Agreement:** > 99% agreement among honest registry services
- **Uptime:** Registry service availability > 99.9%
- **Adoption:** 100+ registered names within first month of testnet
## Future Enhancements
1. **Economic Incentives** - Optional registration fees via Lightning
2. **Reputation System** - Track registry service quality metrics
3. **Certificate System** - Implement NIP-XX certificate witnessing
4. **Noise Protocol** - Secure transport layer for TLS replacement
5. **Client Libraries** - Official libraries for popular languages
6. **Browser Integration** - Browser extension for name resolution
7. **DNS Gateway** - Traditional DNS server that queries FIND

View File

@@ -0,0 +1,495 @@
# FIND Name Binding System - Integration Summary
## Overview
The Free Internet Name Daemon (FIND) protocol has been integrated into ORLY, enabling human-readable name-to-npub bindings that are discoverable through standard Nostr queries. This document summarizes the implementation and provides guidance for using the system.
## What Was Implemented
### Core Components
1. **Consensus Engine** ([pkg/find/consensus.go](../pkg/find/consensus.go))
- Implements trust-weighted consensus algorithm for name registrations
- Validates proposals against renewal windows and ownership rules
- Computes consensus scores from attestations
- Enforces mandatory 1-year registration period with 30-day preferential renewal
2. **Trust Graph Manager** ([pkg/find/trust.go](../pkg/find/trust.go))
- Manages web-of-trust relationships between registry services
- Calculates direct and inherited trust (0-3 hops)
- Applies hop-based decay factors (1.0, 0.8, 0.6, 0.4)
- Provides metrics and analytics
3. **Registry Service** ([pkg/find/registry.go](../pkg/find/registry.go))
- Monitors registration proposals (kind 30100)
- Collects attestations from other registry services (kind 20100)
- Publishes name state after consensus (kind 30102)
- Manages pending proposals and attestation windows
4. **Event Parsers** ([pkg/find/parser.go](../pkg/find/parser.go))
- Parses all FIND event types (30100-30105)
- Validates event structure and required tags
- Already complete - no changes needed
5. **Event Builders** ([pkg/find/builder.go](../pkg/find/builder.go))
- Creates FIND events (registration proposals, attestations, name states, records)
- Already complete - no changes needed
6. **Validators** ([pkg/find/validation.go](../pkg/find/validation.go))
- DNS-style name format validation
- IPv4/IPv6 address validation
- Record type and value validation
- Already complete - no changes needed
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ ORLY Relay │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌──────────────┐ │
│ │ WebSocket │ │ Registry │ │ Database │ │
│ │ Handler │ │ Service │ │ (Badger/ │ │
│ │ │ │ │ │ DGraph) │ │
│ │ - Receives │ │ - Monitors │ │ │ │
│ │ proposals │ │ proposals │ │ - Stores │ │
│ │ - Stores │──│ - Computes │──│ all FIND │ │
│ │ events │ │ consensus │ │ events │ │
│ │ │ │ - Publishes │ │ │ │
│ │ │ │ name state │ │ │ │
│ └────────────────┘ └────────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│ Nostr Events
┌─────────────────────────────────────┐
│ Clients & Other Registry Services │
│ │
│ - Query name state (kind 30102) │
│ - Query records (kind 30103) │
│ - Submit proposals (kind 30100) │
└─────────────────────────────────────┘
```
## How It Works
### Name Registration Flow
1. **User submits registration proposal**
```
User → Create kind 30100 event → Publish to relay
```
2. **Relay stores proposal**
```
Relay → Database → Store event
```
3. **Registry service processes proposal**
```
Registry Service → Validate proposal
→ Wait for attestation window (60-120s)
→ Collect attestations from other services
→ Compute trust-weighted consensus
```
4. **Consensus reached**
```
Registry Service → Create name state (kind 30102)
→ Publish to database
```
5. **Clients query ownership**
```
Client → Query kind 30102 for name → Relay returns name state
```
### Name Resolution Flow
1. **Client queries name state**
```javascript
// Query kind 30102 events with d tag = name
const nameStates = await relay.list([{
kinds: [30102],
'#d': ['example.nostr']
}])
```
2. **Client queries DNS records**
```javascript
// Query kind 30103 events for records
const records = await relay.list([{
kinds: [30103],
'#name': ['example.nostr'],
'#type': ['A'],
authors: [nameOwnerPubkey]
}])
```
3. **Client uses resolved data**
```javascript
// Extract IP addresses
const ips = records.map(e =>
e.tags.find(t => t[0] === 'value')[1]
)
// Connect to service at IP
```
## Event Types
| Kind | Name | Description | Persistence |
|------|------|-------------|-------------|
| 30100 | Registration Proposal | User submits name claim | Parameterized replaceable |
| 20100 | Attestation | Registry service votes | Ephemeral (3 min) |
| 30101 | Trust Graph | Service trust relationships | Parameterized replaceable (30 days) |
| 30102 | Name State | Current ownership | Parameterized replaceable (1 year) |
| 30103 | Name Records | DNS-style records | Parameterized replaceable (tied to name) |
| 30104 | Certificate | TLS-style certificates | Parameterized replaceable (90 days) |
| 30105 | Witness Service | Certificate witnesses | Parameterized replaceable (180 days) |
## Integration Status
### ✅ Completed
- [x] Consensus algorithm implementation
- [x] Trust graph calculation with multi-hop support
- [x] Registry service core logic
- [x] Event parsers for all FIND types
- [x] Event builders for creating FIND events
- [x] Validation functions (DNS names, IPs, etc.)
- [x] Implementation documentation
- [x] Client integration examples
### 🔨 Integration Points (Next Steps)
To complete the integration, the following work remains:
1. **Configuration** ([app/config/config.go](../app/config/config.go))
```go
// Add these fields to config.C:
FindEnabled bool `env:"ORLY_FIND_ENABLED" default:"false"`
FindServicePubkey string `env:"ORLY_FIND_SERVICE_PUBKEY"`
FindServicePrivkey string `env:"ORLY_FIND_SERVICE_PRIVKEY"`
FindAttestationDelay string `env:"ORLY_FIND_ATTESTATION_DELAY" default:"60s"`
FindBootstrapServices []string `env:"ORLY_FIND_BOOTSTRAP_SERVICES"`
```
2. **Database Query Helpers** ([pkg/database/](../pkg/database/))
```go
// Add helper methods:
func (d *Database) QueryNameState(name string) (*find.NameState, error)
func (d *Database) QueryNameRecords(name, recordType string) ([]*find.NameRecord, error)
func (d *Database) IsNameAvailable(name string) (bool, error)
```
3. **Server Integration** ([app/main.go](../app/main.go))
```go
// Initialize registry service if enabled:
if cfg.FindEnabled {
registryService, err := find.NewRegistryService(ctx, db, signer, &find.RegistryConfig{
Enabled: true,
AttestationDelay: 60 * time.Second,
})
if err != nil {
return err
}
if err := registryService.Start(); err != nil {
return err
}
defer registryService.Stop()
}
```
4. **HTTP API Endpoints** ([app/handle-find-api.go](../app/handle-find-api.go) - new file)
```go
// Add REST endpoints:
GET /api/find/names/:name // Query name state
GET /api/find/names/:name/records // Query all records
POST /api/find/register // Submit proposal
```
5. **WebSocket Event Routing** ([app/handle-websocket.go](../app/handle-websocket.go))
```go
// Route FIND events to registry service:
if cfg.FindEnabled && registryService != nil {
if ev.Kind >= 30100 && ev.Kind <= 30105 {
registryService.HandleEvent(ev)
}
}
```
## Usage Examples
### Register a Name (Client)
```javascript
import { finalizeEvent, getPublicKey } from 'nostr-tools'
async function registerName(relay, privkey, name) {
const pubkey = getPublicKey(privkey)
// Create registration proposal
const event = {
kind: 30100,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
['d', name],
['action', 'register'],
['expiration', String(Math.floor(Date.now() / 1000) + 300)]
],
content: ''
}
const signedEvent = finalizeEvent(event, privkey)
await relay.publish(signedEvent)
console.log('Proposal submitted, waiting for consensus...')
// Wait 2 minutes for consensus
await new Promise(r => setTimeout(r, 120000))
// Check if registration succeeded
const nameState = await relay.get({
kinds: [30102],
'#d': [name]
})
if (nameState && nameState.tags.find(t => t[0] === 'owner')[1] === pubkey) {
console.log('Registration successful!')
return true
} else {
console.log('Registration failed')
return false
}
}
```
### Publish DNS Records (Client)
```javascript
async function publishARecord(relay, privkey, name, ipAddress) {
const pubkey = getPublicKey(privkey)
// Verify we own the name first
const nameState = await relay.get({
kinds: [30102],
'#d': [name]
})
if (!nameState || nameState.tags.find(t => t[0] === 'owner')[1] !== pubkey) {
throw new Error('You do not own this name')
}
// Create A record
const event = {
kind: 30103,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
['d', `${name}:A:1`],
['name', name],
['type', 'A'],
['value', ipAddress],
['ttl', '3600']
],
content: ''
}
const signedEvent = finalizeEvent(event, privkey)
await relay.publish(signedEvent)
console.log(`Published A record: ${name} → ${ipAddress}`)
}
```
### Resolve Name to IP (Client)
```javascript
async function resolveNameToIP(relay, name) {
// 1. Get name state (ownership info)
const nameState = await relay.get({
kinds: [30102],
'#d': [name]
})
if (!nameState) {
throw new Error('Name not registered')
}
// Check if expired
const expirationTag = nameState.tags.find(t => t[0] === 'expiration')
if (expirationTag) {
const expiration = parseInt(expirationTag[1])
if (Date.now() / 1000 > expiration) {
throw new Error('Name expired')
}
}
const owner = nameState.tags.find(t => t[0] === 'owner')[1]
// 2. Get A records
const records = await relay.list([{
kinds: [30103],
'#name': [name],
'#type': ['A'],
authors: [owner]
}])
if (records.length === 0) {
throw new Error('No A records found')
}
// 3. Extract IP addresses
const ips = records.map(event => {
return event.tags.find(t => t[0] === 'value')[1]
})
console.log(`${name} → ${ips.join(', ')}`)
return ips
}
```
### Run Registry Service (Operator)
```bash
# Set environment variables
export ORLY_FIND_ENABLED=true
export ORLY_FIND_SERVICE_PUBKEY="your_service_pubkey_hex"
export ORLY_FIND_SERVICE_PRIVKEY="your_service_privkey_hex"
export ORLY_FIND_ATTESTATION_DELAY="60s"
export ORLY_FIND_BOOTSTRAP_SERVICES="pubkey1,pubkey2,pubkey3"
# Start relay
./orly
```
The registry service will:
- Monitor for registration proposals
- Validate proposals against rules
- Publish attestations for valid proposals
- Compute consensus with other services
- Publish name state events
## Key Features
### ✅ Implemented
1. **Trust-Weighted Consensus**
- Services vote on proposals with weighted attestations
- Multi-hop trust inheritance (0-3 hops)
- Hop-based decay factors prevent infinite trust chains
2. **Renewal Window Enforcement**
- Names expire after exactly 1 year
- 30-day preferential renewal window for owners
- Automatic expiration handling
3. **Subdomain Authority**
- Only parent domain owners can register subdomains
- TLDs can be registered by anyone (first-come-first-served)
- Hierarchical ownership validation
4. **DNS-Compatible Records**
- A, AAAA, CNAME, MX, TXT, NS, SRV record types
- Per-type record limits
- TTL-based caching
5. **Sparse Attestation**
- Optional probabilistic attestation to reduce network load
- Deterministic sampling based on proposal hash
- Configurable sampling rates
### 🔮 Future Enhancements
1. **Certificate System** (Defined in NIP, not yet implemented)
- Challenge-response verification
- Threshold witnessing (3+ signatures)
- TLS replacement capabilities
2. **Economic Incentives** (Designed but not implemented)
- Optional registration fees via Lightning
- Reputation scoring for registry services
- Subscription models
3. **Advanced Features**
- Noise protocol for secure transport
- Browser integration
- DNS gateway (traditional DNS → FIND)
## Testing
### Unit Tests
Run existing tests:
```bash
cd pkg/find
go test -v ./...
```
Tests cover:
- Name validation (validation_test.go)
- Parser functions (parser_test.go)
- Builder functions (builder_test.go)
### Integration Tests (To Be Added)
Recommended test scenarios:
1. **Single proposal registration**
2. **Competing proposals with consensus**
3. **Renewal window validation**
4. **Subdomain authority checks**
5. **Trust graph calculation**
6. **Multi-hop trust inheritance**
## Documentation
- **[Implementation Plan](FIND_IMPLEMENTATION_PLAN.md)** - Detailed architecture and phases
- **[NIP Specification](names.md)** - Complete protocol specification
- **[Usage Guide](FIND_USER_GUIDE.md)** - End-user documentation (to be created)
- **[Operator Guide](FIND_OPERATOR_GUIDE.md)** - Registry operator documentation (to be created)
## Security Considerations
### Attack Mitigations
1. **Sybil Attacks**: Trust-weighted consensus prevents new services from dominating
2. **Censorship**: Diverse trust graphs make network-wide censorship difficult
3. **Name Squatting**: Mandatory 1-year expiration with preferential renewal window
4. **Renewal DoS**: 30-day window, multiple retry opportunities
5. **Transfer Fraud**: Cryptographic signature from previous owner required
### Privacy Considerations
- Registration proposals are public (necessary for consensus)
- Ownership history is permanently visible on relays
- Clients can use Tor or private relays for sensitive queries
## Performance Characteristics
- **Registration Finality**: 1-2 minutes (60-120s attestation window)
- **Name Resolution**: < 100ms (database query)
- **Trust Calculation**: O(n) where n = number of services (with 3-hop limit)
- **Consensus Computation**: O(p×a) where p = proposals, a = attestations
## Support & Feedback
- **Issues**: https://github.com/orly-dev/orly/issues
- **Discussions**: https://github.com/orly-dev/orly/discussions
- **Nostr**: nostr:npub1... (relay operator npub)
## Next Steps
To complete the integration:
1. ✅ Review this summary
2. 🔨 Add configuration fields to config.C
3. 🔨 Implement database query helpers
4. 🔨 Integrate registry service in app/main.go
5. 🔨 Add HTTP API endpoints (optional)
6. 🔨 Write integration tests
7. 🔨 Create operator documentation
8. 🔨 Create user guide with examples
The core FIND protocol logic is complete and ready for integration!

View File

@@ -0,0 +1,981 @@
# FIND Rate Limiting Mechanisms (Non-Monetary, Non-PoW)
## Overview
This document explores mechanisms to rate limit name registrations in the FIND protocol without requiring:
- Security deposits or payments
- Monetary mechanisms (Lightning, ecash, etc.)
- Proof of work (computational puzzles)
The goal is to prevent spam and name squatting while maintaining decentralization and accessibility.
---
## 1. Time-Based Mechanisms
### 1.1 Proposal-to-Ratification Delay
**Concept:** Mandatory waiting period between submitting a registration proposal and consensus ratification.
**Implementation:**
```go
type ProposalDelay struct {
MinDelay time.Duration // e.g., 1 hour
MaxDelay time.Duration // e.g., 24 hours
GracePeriod time.Duration // Random jitter to prevent timing attacks
}
func (r *RegistryService) validateProposalTiming(proposal *Proposal) error {
elapsed := time.Since(proposal.CreatedAt)
minRequired := r.config.ProposalDelay.MinDelay
if elapsed < minRequired {
return fmt.Errorf("proposal must age %v before ratification (current: %v)",
minRequired, elapsed)
}
return nil
}
```
**Advantages:**
- Simple to implement
- Gives community time to review and object
- Prevents rapid-fire squatting
- Allows for manual intervention in disputes
**Disadvantages:**
- Poor UX (users wait hours/days)
- Doesn't prevent determined attackers with patience
- Vulnerable to timing attacks (frontrunning)
**Variations:**
- **Progressive Delays:** First name = 1 hour, second = 6 hours, third = 24 hours, etc.
- **Random Delays:** Each proposal gets random delay within range to prevent prediction
- **Peak-Time Penalties:** Longer delays during high registration volume
---
### 1.2 Per-Account Cooldown Periods
**Concept:** Limit how frequently a single npub can register names.
**Implementation:**
```go
type RateLimiter struct {
registrations map[string][]time.Time // npub -> registration timestamps
cooldown time.Duration // e.g., 7 days
maxPerPeriod int // e.g., 3 names per week
}
func (r *RateLimiter) canRegister(npub string, now time.Time) (bool, time.Duration) {
timestamps := r.registrations[npub]
// Remove expired timestamps
cutoff := now.Add(-r.cooldown)
active := filterAfter(timestamps, cutoff)
if len(active) >= r.maxPerPeriod {
oldestExpiry := active[0].Add(r.cooldown)
waitTime := oldestExpiry.Sub(now)
return false, waitTime
}
return true, 0
}
```
**Advantages:**
- Directly limits per-user registration rate
- Configurable (relays can set own limits)
- Persistent across sessions
**Disadvantages:**
- Easy to bypass with multiple npubs
- Requires state tracking across registry services
- May be too restrictive for legitimate bulk registrations
**Variations:**
- **Sliding Window:** Count registrations in last N days
- **Token Bucket:** Allow bursts but enforce long-term average
- **Decay Model:** Cooldown decreases over time (1 day → 6 hours → 1 hour)
---
### 1.3 Account Age Requirements
**Concept:** Npubs must be a certain age before they can register names.
**Implementation:**
```go
func (r *RegistryService) validateAccountAge(npub string, minAge time.Duration) error {
// Query oldest event from this npub across known relays
oldestEvent, err := r.getOldestEventByAuthor(npub)
if err != nil {
return fmt.Errorf("cannot determine account age: %w", err)
}
accountAge := time.Since(oldestEvent.CreatedAt)
if accountAge < minAge {
return fmt.Errorf("account must be %v old (current: %v)", minAge, accountAge)
}
return nil
}
```
**Advantages:**
- Prevents throwaway account spam
- Encourages long-term participation
- No ongoing cost to users
**Disadvantages:**
- Barrier for new users
- Can be gamed with pre-aged accounts
- Requires historical event data
**Variations:**
- **Tiered Ages:** Basic names require 30 days, premium require 90 days
- **Activity Threshold:** Not just age, but "active" age (X events published)
---
## 2. Web of Trust (WoT) Mechanisms
### 2.1 Follow Count Requirements
**Concept:** Require minimum follow count from trusted accounts to register names.
**Implementation:**
```go
type WoTValidator struct {
minFollowers int // e.g., 5 followers
trustedAccounts []string // Bootstrap trusted npubs
}
func (v *WoTValidator) validateFollowCount(npub string) error {
// Query kind 3 events that include this npub in follow list
followers, err := v.queryFollowers(npub)
if err != nil {
return err
}
// Count only followers who are themselves trusted
trustedFollowers := 0
for _, follower := range followers {
if v.isTrusted(follower) {
trustedFollowers++
}
}
if trustedFollowers < v.minFollowers {
return fmt.Errorf("need %d trusted followers, have %d",
v.minFollowers, trustedFollowers)
}
return nil
}
```
**Advantages:**
- Leverages existing Nostr social graph
- Self-regulating (community decides who's trusted)
- Sybil-resistant if trust graph is diverse
**Disadvantages:**
- Chicken-and-egg for new users
- Can create gatekeeping
- Vulnerable to follow-for-follow schemes
**Variations:**
- **Weighted Followers:** High-reputation followers count more
- **Mutual Follows:** Require bidirectional relationships
- **Follow Depth:** Count 2-hop or 3-hop follows
---
### 2.2 Endorsement/Vouching System
**Concept:** Existing name holders can vouch for new registrants.
**Implementation:**
```go
// Kind 30110: Name Registration Endorsement
type Endorsement struct {
Voucher string // npub of existing name holder
Vouchee string // npub seeking registration
NamesSeen int // How many names voucher has endorsed (spam detection)
}
func (r *RegistryService) validateEndorsements(proposal *Proposal) error {
// Query endorsements for this npub
endorsements, err := r.queryEndorsements(proposal.Author)
if err != nil {
return err
}
// Require at least 2 endorsements from different name holders
uniqueVouchers := make(map[string]bool)
for _, e := range endorsements {
// Check voucher holds a name
if r.holdsActiveName(e.Voucher) {
uniqueVouchers[e.Voucher] = true
}
}
if len(uniqueVouchers) < 2 {
return fmt.Errorf("need 2 endorsements from name holders, have %d",
len(uniqueVouchers))
}
return nil
}
```
**Advantages:**
- Creates social accountability
- Name holders have "skin in the game"
- Can revoke endorsements if abused
**Disadvantages:**
- Requires active participation from name holders
- Can create favoritism/cliques
- Vouchers may sell endorsements
**Variations:**
- **Limited Vouches:** Each name holder can vouch for max N users per period
- **Reputation Cost:** Vouching for spammer reduces voucher's reputation
- **Delegation Chains:** Vouched users can vouch others (with decay)
---
### 2.3 Activity History Requirements
**Concept:** Require meaningful Nostr activity before allowing registration.
**Implementation:**
```go
type ActivityRequirements struct {
MinEvents int // e.g., 50 events
MinTimespan time.Duration // e.g., 30 days
RequiredKinds []int // Must have posted notes, not just kind 0
MinUniqueRelays int // Must use multiple relays
}
func (r *RegistryService) validateActivity(npub string, reqs ActivityRequirements) error {
events, err := r.queryUserEvents(npub)
if err != nil {
return err
}
// Check event count
if len(events) < reqs.MinEvents {
return fmt.Errorf("need %d events, have %d", reqs.MinEvents, len(events))
}
// Check timespan
oldest := events[0].CreatedAt
newest := events[len(events)-1].CreatedAt
timespan := newest.Sub(oldest)
if timespan < reqs.MinTimespan {
return fmt.Errorf("activity must span %v, current span: %v",
reqs.MinTimespan, timespan)
}
// Check event diversity
kinds := make(map[int]bool)
for _, e := range events {
kinds[e.Kind] = true
}
hasRequiredKinds := true
for _, kind := range reqs.RequiredKinds {
if !kinds[kind] {
hasRequiredKinds = false
break
}
}
if !hasRequiredKinds {
return fmt.Errorf("missing required event kinds")
}
return nil
}
```
**Advantages:**
- Rewards active community members
- Hard to fake authentic activity
- Aligns with Nostr values (participation)
**Disadvantages:**
- High barrier for new users
- Can be gamed with bot activity
- Definition of "meaningful" is subjective
**Variations:**
- **Engagement Metrics:** Require replies, reactions, zaps received
- **Content Quality:** Use NIP-32 labels to filter quality content
- **Relay Diversity:** Must have published to N different relays
---
## 3. Multi-Phase Verification
### 3.1 Two-Phase Commit with Challenge
**Concept:** Proposal → Challenge → Response → Ratification
**Implementation:**
```go
// Phase 1: Submit proposal (kind 30100)
type RegistrationProposal struct {
Name string
Action string // "register"
}
// Phase 2: Registry issues challenge (kind 20110)
type RegistrationChallenge struct {
ProposalID string
Challenge string // Random challenge string
IssuedAt time.Time
ExpiresAt time.Time
}
// Phase 3: User responds (kind 20111)
type ChallengeResponse struct {
ChallengeID string
Response string // Signed challenge
ProposalID string
}
func (r *RegistryService) processProposal(proposal *Proposal) {
// Generate random challenge
challenge := generateRandomChallenge()
// Publish challenge event
challengeEvent := &ChallengeEvent{
ProposalID: proposal.ID,
Challenge: challenge,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
r.publishChallenge(challengeEvent)
// Wait for response
// If valid response received within window, proceed with attestation
}
```
**Advantages:**
- Proves user is actively monitoring
- Prevents pre-signed bulk registrations
- Adds friction without monetary cost
**Disadvantages:**
- Requires active participation (can't be automated)
- Poor UX (multiple steps)
- Vulnerable to automated response systems
**Variations:**
- **Time-Delayed Challenge:** Challenge issued X hours after proposal
- **Multi-Registry Challenges:** Must respond to challenges from multiple services
- **Progressive Challenges:** Later names require harder challenges
---
### 3.2 Multi-Signature Requirements
**Concept:** Require signatures from multiple devices/keys to prove human operator.
**Implementation:**
```go
type MultiSigProposal struct {
Name string
PrimaryKey string // Main npub
SecondaryKeys []string // Additional npubs that must co-sign
Signatures []Signature
}
func (r *RegistryService) validateMultiSig(proposal *MultiSigProposal) error {
// Require at least 2 signatures from different keys
if len(proposal.Signatures) < 2 {
return fmt.Errorf("need at least 2 signatures")
}
// Verify each signature
for _, sig := range proposal.Signatures {
if !verifySignature(proposal.Name, sig) {
return fmt.Errorf("invalid signature from %s", sig.Pubkey)
}
}
// Ensure signatures are from different keys
uniqueKeys := make(map[string]bool)
for _, sig := range proposal.Signatures {
uniqueKeys[sig.Pubkey] = true
}
if len(uniqueKeys) < 2 {
return fmt.Errorf("signatures must be from distinct keys")
}
return nil
}
```
**Advantages:**
- Harder to automate at scale
- Proves access to multiple devices
- No external dependencies
**Disadvantages:**
- Complex UX (managing multiple keys)
- Still bypassable with multiple hardware keys
- May lose access if secondary key lost
---
## 4. Lottery and Randomization
### 4.1 Random Selection Among Competing Proposals
**Concept:** When multiple proposals for same name arrive, randomly select winner.
**Implementation:**
```go
func (r *RegistryService) selectWinner(proposals []*Proposal) *Proposal {
if len(proposals) == 1 {
return proposals[0]
}
// Use deterministic randomness based on block hash or similar
seed := r.getConsensusSeed() // From latest Bitcoin block hash, etc.
// Create weighted lottery based on account age, reputation, etc.
weights := make([]int, len(proposals))
for i, p := range proposals {
weights[i] = r.calculateWeight(p.Author)
}
// Select winner
rng := rand.New(rand.NewSource(seed))
winner := weightedRandomSelect(proposals, weights, rng)
return winner
}
func (r *RegistryService) calculateWeight(npub string) int {
// Base weight: 1
weight := 1
// +1 for each month of account age (max 12)
accountAge := r.getAccountAge(npub)
weight += min(int(accountAge.Hours()/730), 12)
// +1 for each 100 events (max 10)
eventCount := r.getEventCount(npub)
weight += min(eventCount/100, 10)
// +1 for each trusted follower (max 20)
followerCount := r.getTrustedFollowerCount(npub)
weight += min(followerCount, 20)
return weight
}
```
**Advantages:**
- Fair chance for all participants
- Can weight by reputation without hard gatekeeping
- Discourages squatting (no guarantee of winning)
**Disadvantages:**
- Winners may feel arbitrary
- Still requires sybil resistance (or attackers spam proposals)
- Requires consensus on randomness source
**Variations:**
- **Time-Weighted Lottery:** Earlier proposals have slightly higher odds
- **Reputation-Only Lottery:** Only weight by WoT score
- **Periodic Lotteries:** Batch proposals weekly, run lottery for all conflicts
---
### 4.2 Queue System with Priority Ranking
**Concept:** Proposals enter queue, priority determined by non-transferable metrics.
**Implementation:**
```go
type ProposalQueue struct {
proposals []*ScoredProposal
}
type ScoredProposal struct {
Proposal *Proposal
Score int
}
func (r *RegistryService) scoreProposal(p *Proposal) int {
score := 0
// Account age contribution (0-30 points)
accountAge := r.getAccountAge(p.Author)
score += min(int(accountAge.Hours()/24), 30) // 1 point per day, max 30
// Event count contribution (0-20 points)
eventCount := r.getEventCount(p.Author)
score += min(eventCount/10, 20) // 1 point per 10 events, max 20
// WoT contribution (0-30 points)
wotScore := r.getWoTScore(p.Author)
score += min(wotScore, 30)
// Endorsements (0-20 points)
endorsements := r.getEndorsementCount(p.Author)
score += min(endorsements*5, 20) // 5 points per endorsement, max 20
return score
}
func (q *ProposalQueue) process() *Proposal {
if len(q.proposals) == 0 {
return nil
}
// Sort by score (descending)
sort.Slice(q.proposals, func(i, j int) bool {
return q.proposals[i].Score > q.proposals[j].Score
})
// Process highest score
winner := q.proposals[0]
q.proposals = q.proposals[1:]
return winner.Proposal
}
```
**Advantages:**
- Transparent, merit-based selection
- Rewards long-term participation
- Predictable for users (can see their score)
**Disadvantages:**
- Complex scoring function
- May favor old accounts over new legitimate users
- Gaming possible if score calculation public
---
## 5. Behavioral Analysis
### 5.1 Pattern Detection
**Concept:** Detect and flag suspicious registration patterns.
**Implementation:**
```go
type BehaviorAnalyzer struct {
recentProposals map[string][]*Proposal // IP/relay -> proposals
suspiciousScore map[string]int // npub -> suspicion score
}
func (b *BehaviorAnalyzer) analyzeProposal(p *Proposal) (suspicious bool, reason string) {
score := 0
// Check registration frequency
if b.recentProposalCount(p.Author, 1*time.Hour) > 5 {
score += 20
}
// Check name similarity (registering foo1, foo2, foo3, ...)
if b.hasSequentialNames(p.Author) {
score += 30
}
// Check relay diversity (all from same relay = suspicious)
if b.relayDiversity(p.Author) < 2 {
score += 15
}
// Check timestamp patterns (all proposals at exact intervals)
if b.hasRegularIntervals(p.Author) {
score += 25
}
// Check for dictionary attack patterns
if b.isDictionaryAttack(p.Author) {
score += 40
}
if score > 50 {
return true, b.generateReason(score)
}
return false, ""
}
```
**Advantages:**
- Catches automated attacks
- No burden on legitimate users
- Adaptive (can tune detection rules)
**Disadvantages:**
- False positives possible
- Requires heuristic development
- Attackers can adapt
**Variations:**
- **Machine Learning:** Train model on spam vs. legitimate patterns
- **Collaborative Filtering:** Share suspicious patterns across registry services
- **Progressive Restrictions:** Suspicious users face longer delays
---
### 5.2 Diversity Requirements
**Concept:** Require proposals to exhibit "natural" diversity patterns.
**Implementation:**
```go
type DiversityRequirements struct {
MinRelays int // Must use >= N relays
MinTimeJitter time.Duration // Registrations can't be exactly spaced
MaxSimilarity float64 // Names can't be too similar (Levenshtein distance)
}
func (r *RegistryService) validateDiversity(npub string, reqs DiversityRequirements) error {
proposals := r.getProposalsByAuthor(npub)
// Check relay diversity
relays := make(map[string]bool)
for _, p := range proposals {
relays[p.SeenOnRelay] = true
}
if len(relays) < reqs.MinRelays {
return fmt.Errorf("must use %d different relays", reqs.MinRelays)
}
// Check timestamp jitter
if len(proposals) > 1 {
intervals := make([]time.Duration, len(proposals)-1)
for i := 1; i < len(proposals); i++ {
intervals[i-1] = proposals[i].CreatedAt.Sub(proposals[i-1].CreatedAt)
}
// If all intervals are suspiciously similar (< 10% variance), reject
variance := calculateVariance(intervals)
avgInterval := calculateAverage(intervals)
if variance/avgInterval < 0.1 {
return fmt.Errorf("timestamps too regular, appears automated")
}
}
// Check name similarity
for i := 0; i < len(proposals); i++ {
for j := i + 1; j < len(proposals); j++ {
similarity := levenshteinSimilarity(proposals[i].Name, proposals[j].Name)
if similarity > reqs.MaxSimilarity {
return fmt.Errorf("names too similar: %s and %s",
proposals[i].Name, proposals[j].Name)
}
}
}
return nil
}
```
**Advantages:**
- Natural requirement for humans
- Hard for bots to fake convincingly
- Doesn't require state or external data
**Disadvantages:**
- May flag legitimate bulk registrations
- Requires careful threshold tuning
- Can be bypassed with sufficient effort
---
## 6. Hybrid Approaches
### 6.1 Graduated Trust Model
**Concept:** Combine multiple mechanisms with progressive unlock.
```
Level 0 (New User):
- Account must be 7 days old
- Must have 10 events published
- Can register 1 name every 30 days
- 24-hour proposal delay
- Requires 2 endorsements
Level 1 (Established User):
- Account must be 90 days old
- Must have 100 events, 10 followers
- Can register 3 names every 30 days
- 6-hour proposal delay
- Requires 1 endorsement
Level 2 (Trusted User):
- Account must be 365 days old
- Must have 1000 events, 50 followers
- Can register 10 names every 30 days
- 1-hour proposal delay
- No endorsement required
Level 3 (Name Holder):
- Already holds an active name
- Can register unlimited subdomains under owned names
- Can register 5 TLDs every 30 days
- Instant proposal for subdomains
- Can vouch for others
```
**Implementation:**
```go
type UserLevel struct {
Level int
Requirements Requirements
Privileges Privileges
}
type Requirements struct {
MinAccountAge time.Duration
MinEvents int
MinFollowers int
MinActiveNames int
}
type Privileges struct {
MaxNamesPerPeriod int
ProposalDelay time.Duration
EndorsementsReq int
CanVouch bool
}
func (r *RegistryService) getUserLevel(npub string) UserLevel {
age := r.getAccountAge(npub)
events := r.getEventCount(npub)
followers := r.getFollowerCount(npub)
names := r.getActiveNameCount(npub)
// Check Level 3
if names > 0 {
return UserLevel{
Level: 3,
Privileges: Privileges{
MaxNamesPerPeriod: 5,
ProposalDelay: 0,
EndorsementsReq: 0,
CanVouch: true,
},
}
}
// Check Level 2
if age >= 365*24*time.Hour && events >= 1000 && followers >= 50 {
return UserLevel{
Level: 2,
Privileges: Privileges{
MaxNamesPerPeriod: 10,
ProposalDelay: 1 * time.Hour,
EndorsementsReq: 0,
CanVouch: false,
},
}
}
// Check Level 1
if age >= 90*24*time.Hour && events >= 100 && followers >= 10 {
return UserLevel{
Level: 1,
Privileges: Privileges{
MaxNamesPerPeriod: 3,
ProposalDelay: 6 * time.Hour,
EndorsementsReq: 1,
CanVouch: false,
},
}
}
// Default: Level 0
return UserLevel{
Level: 0,
Privileges: Privileges{
MaxNamesPerPeriod: 1,
ProposalDelay: 24 * time.Hour,
EndorsementsReq: 2,
CanVouch: false,
},
}
}
```
**Advantages:**
- Flexible and granular
- Rewards participation without hard barriers
- Self-regulating (community grows trust over time)
- Discourages throwaway accounts
**Disadvantages:**
- Complex to implement and explain
- May still be gamed by determined attackers
- Requires careful balance of thresholds
---
## 7. Recommended Hybrid Implementation
For FIND, I recommend combining these mechanisms:
### Base Layer: Time + WoT
```go
type BaseRequirements struct {
// Minimum account requirements
MinAccountAge time.Duration // 30 days
MinPublishedEvents int // 20 events
MinEventKinds []int // Must have kind 1 (notes)
// WoT requirements
MinWoTScore float64 // 0.01 (very low threshold)
MinTrustedFollowers int // 2 followers from trusted accounts
// Proposal timing
ProposalDelay time.Duration // 6 hours
}
```
### Rate Limiting Layer: Progressive Cooldowns
```go
type RateLimits struct {
// First name: 7 day cooldown after
// Second name: 14 day cooldown
// Third name: 30 day cooldown
// Fourth+: 60 day cooldown
GetCooldown func(registrationCount int) time.Duration
}
```
### Reputation Layer: Graduated Trust
```go
// Users with existing names get faster registration
// Users with high WoT scores get reduced delays
// Users with endorsements bypass some checks
```
### Detection Layer: Behavioral Analysis
```go
// Flag suspicious patterns
// Require manual review for flagged accounts
// Share blocklists between registry services
```
This hybrid approach:
- ✅ Low barrier for new legitimate users (30 days + minimal activity)
- ✅ Strong sybil resistance (WoT + account age)
- ✅ Prevents rapid squatting (progressive cooldowns)
- ✅ Rewards participation (graduated trust)
- ✅ Catches automation (behavioral analysis)
- ✅ No monetary cost
- ✅ No proof of work
- ✅ Decentralized (no central authority)
---
## 8. Comparison Matrix
| Mechanism | Sybil Resistance | UX Impact | Implementation Complexity | Bypass Difficulty |
|-----------|------------------|-----------|---------------------------|-------------------|
| Proposal Delay | Low | High | Low | Low |
| Per-Account Cooldown | Medium | Medium | Low | Low (multiple keys) |
| Account Age | Medium | Low | Low | Medium (pre-age accounts) |
| Follow Count | High | Medium | Medium | High (requires real follows) |
| Endorsement System | High | High | High | High (requires cooperation) |
| Activity History | High | Low | Medium | High (must fake real activity) |
| Multi-Phase Commit | Medium | High | Medium | Medium (can automate) |
| Lottery System | Medium | Medium | High | Medium (sybil can spam proposals) |
| Queue/Priority | High | Low | High | High (merit-based) |
| Behavioral Analysis | High | Low | Very High | Very High (adaptive) |
| **Hybrid Graduated** | **Very High** | **Medium** | **High** | **Very High** |
---
## 9. Attack Scenarios and Mitigations
### Scenario 1: Sybil Attack (1000 throwaway npubs)
**Mitigation:** Account age + activity requirements filter out new accounts. WoT requirements prevent isolated accounts from registering.
### Scenario 2: Pre-Aged Accounts
**Attacker creates accounts months in advance**
**Mitigation:** Activity history requirements force ongoing engagement. Behavioral analysis detects coordinated registration waves.
### Scenario 3: Follow-for-Follow Rings
**Attackers create mutual follow networks**
**Mitigation:** WoT decay for insular networks. Only follows from trusted/bootstrapped accounts count.
### Scenario 4: Bulk Registration by Legitimate User
**Company wants 100 names for project**
**Mitigation:** Manual exception process for verified organizations. Higher-level users get higher quotas.
### Scenario 5: Frontrunning
**Attacker monitors proposals and submits competing proposal**
**Mitigation:** Proposal delay + lottery system makes frontrunning less effective. Random selection among competing proposals.
---
## 10. Configuration Recommendations
```go
// Conservative (strict anti-spam)
conservative := RateLimitConfig{
MinAccountAge: 90 * 24 * time.Hour, // 90 days
MinEvents: 100,
MinFollowers: 10,
ProposalDelay: 24 * time.Hour,
CooldownPeriod: 30 * 24 * time.Hour,
MaxNamesPerAccount: 5,
}
// Balanced (recommended for most relays)
balanced := RateLimitConfig{
MinAccountAge: 30 * 24 * time.Hour, // 30 days
MinEvents: 20,
MinFollowers: 2,
ProposalDelay: 6 * time.Hour,
CooldownPeriod: 7 * 24 * time.Hour,
MaxNamesPerAccount: 10,
}
// Permissive (community trust-based)
permissive := RateLimitConfig{
MinAccountAge: 7 * 24 * time.Hour, // 7 days
MinEvents: 5,
MinFollowers: 0, // No WoT requirement
ProposalDelay: 1 * time.Hour,
CooldownPeriod: 24 * time.Hour,
MaxNamesPerAccount: 20,
}
```
Each relay can choose their own configuration based on their community values and spam tolerance.
---
## Conclusion
Non-monetary, non-PoW rate limiting is achievable through careful combination of:
1. **Time-based friction** (delays, cooldowns)
2. **Social proof** (WoT, endorsements)
3. **Behavioral signals** (activity history, pattern detection)
4. **Graduated trust** (reward long-term participation)
The key insight is that **time + social capital** can be as effective as monetary deposits for spam prevention, while being more aligned with Nostr's values of openness and decentralization.
The recommended hybrid approach provides strong sybil resistance while maintaining accessibility for legitimate new users, creating a natural barrier that's low for humans but high for bots.