develop registration ratelimit mechanism
This commit is contained in:
981
docs/FIND_RATE_LIMITING_MECHANISMS.md
Normal file
981
docs/FIND_RATE_LIMITING_MECHANISMS.md
Normal 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.
|
||||
Reference in New Issue
Block a user