982 lines
27 KiB
Markdown
982 lines
27 KiB
Markdown
# 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.
|