develop registration ratelimit mechanism
This commit is contained in:
466
docs/FIND_IMPLEMENTATION_PLAN.md
Normal file
466
docs/FIND_IMPLEMENTATION_PLAN.md
Normal 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
|
||||
495
docs/FIND_INTEGRATION_SUMMARY.md
Normal file
495
docs/FIND_INTEGRATION_SUMMARY.md
Normal 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!
|
||||
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.
|
||||
376
pkg/find/consensus.go
Normal file
376
pkg/find/consensus.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"next.orly.dev/pkg/database"
|
||||
)
|
||||
|
||||
// ConsensusEngine handles the consensus algorithm for name registrations
|
||||
type ConsensusEngine struct {
|
||||
db database.Database
|
||||
trustGraph *TrustGraph
|
||||
threshold float64 // Consensus threshold (e.g., 0.51 for 51%)
|
||||
minCoverage float64 // Minimum trust graph coverage required
|
||||
conflictMargin float64 // Margin for declaring conflicts (e.g., 0.05 for 5%)
|
||||
}
|
||||
|
||||
// NewConsensusEngine creates a new consensus engine
|
||||
func NewConsensusEngine(db database.Database, trustGraph *TrustGraph) *ConsensusEngine {
|
||||
return &ConsensusEngine{
|
||||
db: db,
|
||||
trustGraph: trustGraph,
|
||||
threshold: 0.51, // 51% threshold
|
||||
minCoverage: 0.30, // 30% minimum coverage
|
||||
conflictMargin: 0.05, // 5% conflict margin
|
||||
}
|
||||
}
|
||||
|
||||
// ProposalScore holds scoring information for a proposal
|
||||
type ProposalScore struct {
|
||||
Proposal *RegistrationProposal
|
||||
Score float64
|
||||
Attestations []*Attestation
|
||||
Weights map[string]float64 // Attester pubkey -> weighted score
|
||||
}
|
||||
|
||||
// ConsensusResult represents the result of consensus computation
|
||||
type ConsensusResult struct {
|
||||
Winner *RegistrationProposal
|
||||
Score float64
|
||||
Confidence float64 // 0.0 to 1.0
|
||||
Attestations int
|
||||
Conflicted bool
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ComputeConsensus computes consensus for a set of competing proposals
|
||||
func (ce *ConsensusEngine) ComputeConsensus(proposals []*RegistrationProposal, attestations []*Attestation) (*ConsensusResult, error) {
|
||||
if len(proposals) == 0 {
|
||||
return nil, errorf.E("no proposals to evaluate")
|
||||
}
|
||||
|
||||
// Group attestations by proposal ID
|
||||
attestationMap := make(map[string][]*Attestation)
|
||||
for _, att := range attestations {
|
||||
if att.Decision == DecisionApprove {
|
||||
attestationMap[att.ProposalID] = append(attestationMap[att.ProposalID], att)
|
||||
}
|
||||
}
|
||||
|
||||
// Score each proposal
|
||||
scores := make([]*ProposalScore, 0, len(proposals))
|
||||
totalWeight := 0.0
|
||||
|
||||
for _, proposal := range proposals {
|
||||
proposalAtts := attestationMap[proposal.Event.GetIDString()]
|
||||
score, weights := ce.ScoreProposal(proposal, proposalAtts)
|
||||
|
||||
scores = append(scores, &ProposalScore{
|
||||
Proposal: proposal,
|
||||
Score: score,
|
||||
Attestations: proposalAtts,
|
||||
Weights: weights,
|
||||
})
|
||||
|
||||
totalWeight += score
|
||||
}
|
||||
|
||||
// Check if we have sufficient coverage
|
||||
if totalWeight < ce.minCoverage {
|
||||
return &ConsensusResult{
|
||||
Conflicted: true,
|
||||
Reason: fmt.Sprintf("insufficient attestations: %.2f%% < %.2f%%", totalWeight*100, ce.minCoverage*100),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Find highest scoring proposal
|
||||
var winner *ProposalScore
|
||||
for _, ps := range scores {
|
||||
if winner == nil || ps.Score > winner.Score {
|
||||
winner = ps
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate relative score
|
||||
relativeScore := winner.Score / totalWeight
|
||||
|
||||
// Check for conflicts (multiple proposals within margin)
|
||||
conflicted := false
|
||||
for _, ps := range scores {
|
||||
if ps.Proposal.Event.GetIDString() != winner.Proposal.Event.GetIDString() {
|
||||
otherRelative := ps.Score / totalWeight
|
||||
if (relativeScore - otherRelative) < ce.conflictMargin {
|
||||
conflicted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if winner meets threshold
|
||||
if relativeScore < ce.threshold {
|
||||
return &ConsensusResult{
|
||||
Winner: winner.Proposal,
|
||||
Score: winner.Score,
|
||||
Confidence: relativeScore,
|
||||
Attestations: len(winner.Attestations),
|
||||
Conflicted: true,
|
||||
Reason: fmt.Sprintf("score %.2f%% below threshold %.2f%%", relativeScore*100, ce.threshold*100),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Check for conflicts
|
||||
if conflicted {
|
||||
return &ConsensusResult{
|
||||
Winner: winner.Proposal,
|
||||
Score: winner.Score,
|
||||
Confidence: relativeScore,
|
||||
Attestations: len(winner.Attestations),
|
||||
Conflicted: true,
|
||||
Reason: "competing proposals within conflict margin",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Success!
|
||||
return &ConsensusResult{
|
||||
Winner: winner.Proposal,
|
||||
Score: winner.Score,
|
||||
Confidence: relativeScore,
|
||||
Attestations: len(winner.Attestations),
|
||||
Conflicted: false,
|
||||
Reason: "consensus reached",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ScoreProposal computes the trust-weighted score for a proposal
|
||||
func (ce *ConsensusEngine) ScoreProposal(proposal *RegistrationProposal, attestations []*Attestation) (float64, map[string]float64) {
|
||||
totalScore := 0.0
|
||||
weights := make(map[string]float64)
|
||||
|
||||
for _, att := range attestations {
|
||||
if att.Decision != DecisionApprove {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get attestation weight (default 100)
|
||||
attWeight := float64(att.Weight)
|
||||
if attWeight <= 0 {
|
||||
attWeight = 100
|
||||
}
|
||||
|
||||
// Get trust level for this attester
|
||||
trustLevel := ce.trustGraph.GetTrustLevel(att.Event.Pubkey)
|
||||
|
||||
// Calculate weighted score
|
||||
// Score = attestation_weight * trust_level / 100
|
||||
score := (attWeight / 100.0) * trustLevel
|
||||
|
||||
weights[att.Event.GetPubkeyString()] = score
|
||||
totalScore += score
|
||||
}
|
||||
|
||||
return totalScore, weights
|
||||
}
|
||||
|
||||
// ValidateProposal validates a registration proposal against current state
|
||||
func (ce *ConsensusEngine) ValidateProposal(proposal *RegistrationProposal) error {
|
||||
// Validate name format
|
||||
if err := ValidateName(proposal.Name); err != nil {
|
||||
return errorf.E("invalid name format: %w", err)
|
||||
}
|
||||
|
||||
// Check if proposal is expired
|
||||
if !proposal.Expiration.IsZero() && time.Now().After(proposal.Expiration) {
|
||||
return errorf.E("proposal expired at %v", proposal.Expiration)
|
||||
}
|
||||
|
||||
// Validate subdomain authority (if applicable)
|
||||
if !IsTLD(proposal.Name) {
|
||||
parent := GetParentDomain(proposal.Name)
|
||||
if parent == "" {
|
||||
return errorf.E("invalid subdomain structure")
|
||||
}
|
||||
|
||||
// Query parent domain ownership
|
||||
parentState, err := ce.QueryNameState(parent)
|
||||
if err != nil {
|
||||
return errorf.E("failed to query parent domain: %w", err)
|
||||
}
|
||||
|
||||
if parentState == nil {
|
||||
return errorf.E("parent domain %s not registered", parent)
|
||||
}
|
||||
|
||||
// Verify proposer owns parent domain
|
||||
proposerPubkey := proposal.Event.GetPubkeyString()
|
||||
if parentState.Owner != proposerPubkey {
|
||||
return errorf.E("proposer does not own parent domain %s", parent)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate against current name state
|
||||
nameState, err := ce.QueryNameState(proposal.Name)
|
||||
if err != nil {
|
||||
return errorf.E("failed to query name state: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Name is not registered - anyone can register
|
||||
if nameState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name is expired - anyone can register
|
||||
if !nameState.Expiration.IsZero() && now.After(nameState.Expiration) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Calculate renewal window start (30 days before expiration)
|
||||
renewalStart := nameState.Expiration.Add(-PreferentialRenewalDays * 24 * time.Hour)
|
||||
|
||||
// Before renewal window - reject all proposals
|
||||
if now.Before(renewalStart) {
|
||||
return errorf.E("name is currently owned and not in renewal window")
|
||||
}
|
||||
|
||||
// During renewal window - only current owner can register
|
||||
if now.Before(nameState.Expiration) {
|
||||
proposerPubkey := proposal.Event.GetPubkeyString()
|
||||
if proposerPubkey != nameState.Owner {
|
||||
return errorf.E("only current owner can renew during preferential renewal window")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Should not reach here, but allow registration if we do
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateTransfer validates a transfer proposal
|
||||
func (ce *ConsensusEngine) ValidateTransfer(proposal *RegistrationProposal) error {
|
||||
if proposal.Action != ActionTransfer {
|
||||
return errorf.E("not a transfer proposal")
|
||||
}
|
||||
|
||||
// Must have previous owner and signature
|
||||
if proposal.PrevOwner == "" {
|
||||
return errorf.E("missing previous owner")
|
||||
}
|
||||
if proposal.PrevSig == "" {
|
||||
return errorf.E("missing previous owner signature")
|
||||
}
|
||||
|
||||
// Query current name state
|
||||
nameState, err := ce.QueryNameState(proposal.Name)
|
||||
if err != nil {
|
||||
return errorf.E("failed to query name state: %w", err)
|
||||
}
|
||||
|
||||
if nameState == nil {
|
||||
return errorf.E("name not registered")
|
||||
}
|
||||
|
||||
// Verify previous owner matches current owner
|
||||
if nameState.Owner != proposal.PrevOwner {
|
||||
return errorf.E("previous owner mismatch")
|
||||
}
|
||||
|
||||
// Verify name is not expired
|
||||
if !nameState.Expiration.IsZero() && time.Now().After(nameState.Expiration) {
|
||||
return errorf.E("name expired")
|
||||
}
|
||||
|
||||
// TODO: Verify signature over transfer message
|
||||
// Message format: "transfer:<name>:<new_owner_pubkey>:<timestamp>"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryNameState queries the current name state from the database
|
||||
func (ce *ConsensusEngine) QueryNameState(name string) (*NameState, error) {
|
||||
// Query kind 30102 events with d tag = name
|
||||
filter := &struct {
|
||||
Kinds []uint16
|
||||
DTags []string
|
||||
Limit int
|
||||
}{
|
||||
Kinds: []uint16{KindNameState},
|
||||
DTags: []string{name},
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
// Note: This would use the actual database query method
|
||||
// For now, return nil to indicate not found
|
||||
// TODO: Implement actual database query
|
||||
_ = filter
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// CreateNameState creates a name state event from consensus result
|
||||
func (ce *ConsensusEngine) CreateNameState(result *ConsensusResult, registryPubkey []byte) (*NameState, error) {
|
||||
if result.Winner == nil {
|
||||
return nil, errorf.E("no winner in consensus result")
|
||||
}
|
||||
|
||||
proposal := result.Winner
|
||||
|
||||
return &NameState{
|
||||
Name: proposal.Name,
|
||||
Owner: proposal.Event.GetPubkeyString(),
|
||||
RegisteredAt: time.Now(),
|
||||
ProposalID: proposal.Event.GetIDString(),
|
||||
Attestations: result.Attestations,
|
||||
Confidence: result.Confidence,
|
||||
Expiration: time.Now().Add(NameRegistrationPeriod),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ProcessProposalBatch processes a batch of proposals and returns consensus results
|
||||
func (ce *ConsensusEngine) ProcessProposalBatch(proposals []*RegistrationProposal, attestations []*Attestation) ([]*ConsensusResult, error) {
|
||||
// Group proposals by name
|
||||
proposalsByName := make(map[string][]*RegistrationProposal)
|
||||
for _, proposal := range proposals {
|
||||
proposalsByName[proposal.Name] = append(proposalsByName[proposal.Name], proposal)
|
||||
}
|
||||
|
||||
results := make([]*ConsensusResult, 0)
|
||||
|
||||
// Process each name's proposals independently
|
||||
for name, nameProposals := range proposalsByName {
|
||||
// Filter attestations for this name's proposals
|
||||
proposalIDs := make(map[string]bool)
|
||||
for _, p := range nameProposals {
|
||||
proposalIDs[p.Event.GetIDString()] = true
|
||||
}
|
||||
|
||||
nameAttestations := make([]*Attestation, 0)
|
||||
for _, att := range attestations {
|
||||
if proposalIDs[att.ProposalID] {
|
||||
nameAttestations = append(nameAttestations, att)
|
||||
}
|
||||
}
|
||||
|
||||
// Compute consensus for this name
|
||||
result, err := ce.ComputeConsensus(nameProposals, nameAttestations)
|
||||
if chk.E(err) {
|
||||
// Log error but continue processing other names
|
||||
result = &ConsensusResult{
|
||||
Conflicted: true,
|
||||
Reason: fmt.Sprintf("error: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
// Add name to result for tracking
|
||||
if result.Winner != nil {
|
||||
result.Winner.Name = name
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
456
pkg/find/registry.go
Normal file
456
pkg/find/registry.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lol "lol.mleku.dev"
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/interfaces/signer"
|
||||
)
|
||||
|
||||
// RegistryService implements the FIND name registry consensus protocol
|
||||
type RegistryService struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
db database.Database
|
||||
signer signer.I
|
||||
trustGraph *TrustGraph
|
||||
consensus *ConsensusEngine
|
||||
config *RegistryConfig
|
||||
pendingProposals map[string]*ProposalState
|
||||
mu sync.RWMutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// RegistryConfig holds configuration for the registry service
|
||||
type RegistryConfig struct {
|
||||
Enabled bool
|
||||
AttestationDelay time.Duration
|
||||
SparseEnabled bool
|
||||
SamplingRate int
|
||||
BootstrapServices []string
|
||||
MinimumAttesters int
|
||||
}
|
||||
|
||||
// ProposalState tracks a proposal during its attestation window
|
||||
type ProposalState struct {
|
||||
Proposal *RegistrationProposal
|
||||
Attestations []*Attestation
|
||||
ReceivedAt time.Time
|
||||
ProcessedAt *time.Time
|
||||
Timer *time.Timer
|
||||
}
|
||||
|
||||
// NewRegistryService creates a new registry service
|
||||
func NewRegistryService(ctx context.Context, db database.Database, signer signer.I, config *RegistryConfig) (*RegistryService, error) {
|
||||
if !config.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
trustGraph := NewTrustGraph(signer.Pub())
|
||||
consensus := NewConsensusEngine(db, trustGraph)
|
||||
|
||||
rs := &RegistryService{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
db: db,
|
||||
signer: signer,
|
||||
trustGraph: trustGraph,
|
||||
consensus: consensus,
|
||||
config: config,
|
||||
pendingProposals: make(map[string]*ProposalState),
|
||||
}
|
||||
|
||||
// Bootstrap trust graph if configured
|
||||
if len(config.BootstrapServices) > 0 {
|
||||
if err := rs.bootstrapTrustGraph(); chk.E(err) {
|
||||
lol.Err("failed to bootstrap trust graph:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// Start starts the registry service
|
||||
func (rs *RegistryService) Start() error {
|
||||
lol.Info("starting FIND registry service")
|
||||
|
||||
// Start proposal monitoring goroutine
|
||||
rs.wg.Add(1)
|
||||
go rs.monitorProposals()
|
||||
|
||||
// Start attestation collection goroutine
|
||||
rs.wg.Add(1)
|
||||
go rs.collectAttestations()
|
||||
|
||||
// Start trust graph refresh goroutine
|
||||
rs.wg.Add(1)
|
||||
go rs.refreshTrustGraph()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the registry service
|
||||
func (rs *RegistryService) Stop() error {
|
||||
lol.Info("stopping FIND registry service")
|
||||
|
||||
rs.cancel()
|
||||
rs.wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// monitorProposals monitors for new registration proposals
|
||||
func (rs *RegistryService) monitorProposals() {
|
||||
defer rs.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rs.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
rs.checkForNewProposals()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkForNewProposals checks database for new registration proposals
|
||||
func (rs *RegistryService) checkForNewProposals() {
|
||||
// Query recent kind 30100 events (registration proposals)
|
||||
// This would use the actual database query API
|
||||
// For now, this is a stub
|
||||
|
||||
// TODO: Implement database query for kind 30100 events
|
||||
// TODO: Parse proposals and add to pendingProposals map
|
||||
// TODO: Start attestation timer for each new proposal
|
||||
}
|
||||
|
||||
// OnProposalReceived is called when a new proposal is received
|
||||
func (rs *RegistryService) OnProposalReceived(proposal *RegistrationProposal) error {
|
||||
// Validate proposal
|
||||
if err := rs.consensus.ValidateProposal(proposal); chk.E(err) {
|
||||
lol.Warn("invalid proposal:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
proposalID := proposal.Event.GetIDString()
|
||||
|
||||
rs.mu.Lock()
|
||||
defer rs.mu.Unlock()
|
||||
|
||||
// Check if already processing
|
||||
if _, exists := rs.pendingProposals[proposalID]; exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
lol.Info("received new proposal:", proposalID, "name:", proposal.Name)
|
||||
|
||||
// Create proposal state
|
||||
state := &ProposalState{
|
||||
Proposal: proposal,
|
||||
Attestations: make([]*Attestation, 0),
|
||||
ReceivedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Start attestation timer
|
||||
state.Timer = time.AfterFunc(rs.config.AttestationDelay, func() {
|
||||
rs.processProposal(proposalID)
|
||||
})
|
||||
|
||||
rs.pendingProposals[proposalID] = state
|
||||
|
||||
// Publish attestation (if not using sparse or if dice roll succeeds)
|
||||
if rs.shouldAttest(proposalID) {
|
||||
go rs.publishAttestation(proposal, DecisionApprove, "valid_proposal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldAttest determines if this service should attest to a proposal
|
||||
func (rs *RegistryService) shouldAttest(proposalID string) bool {
|
||||
if !rs.config.SparseEnabled {
|
||||
return true
|
||||
}
|
||||
|
||||
// Sparse attestation: use hash of (proposal_id || service_pubkey) % K == 0
|
||||
// This provides deterministic but distributed attestation
|
||||
hash := hex.Dec(proposalID)
|
||||
if len(hash) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Simple modulo check using first byte of hash
|
||||
return int(hash[0])%rs.config.SamplingRate == 0
|
||||
}
|
||||
|
||||
// publishAttestation publishes an attestation for a proposal
|
||||
func (rs *RegistryService) publishAttestation(proposal *RegistrationProposal, decision string, reason string) {
|
||||
attestation := &Attestation{
|
||||
ProposalID: proposal.Event.GetIDString(),
|
||||
Decision: decision,
|
||||
Weight: 100,
|
||||
Reason: reason,
|
||||
ServiceURL: "", // TODO: Get from config
|
||||
Expiration: time.Now().Add(AttestationExpiry),
|
||||
}
|
||||
|
||||
// TODO: Create and sign attestation event (kind 20100)
|
||||
// TODO: Publish to database
|
||||
_ = attestation
|
||||
|
||||
lol.Debug("published attestation for proposal:", proposal.Name, "decision:", decision)
|
||||
}
|
||||
|
||||
// collectAttestations collects attestations from other registry services
|
||||
func (rs *RegistryService) collectAttestations() {
|
||||
defer rs.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rs.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
rs.updateAttestations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateAttestations fetches new attestations from database
|
||||
func (rs *RegistryService) updateAttestations() {
|
||||
rs.mu.RLock()
|
||||
proposalIDs := make([]string, 0, len(rs.pendingProposals))
|
||||
for id := range rs.pendingProposals {
|
||||
proposalIDs = append(proposalIDs, id)
|
||||
}
|
||||
rs.mu.RUnlock()
|
||||
|
||||
if len(proposalIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Query kind 20100 events (attestations) for pending proposals
|
||||
// TODO: Add attestations to proposal states
|
||||
}
|
||||
|
||||
// processProposal processes a proposal after the attestation window expires
|
||||
func (rs *RegistryService) processProposal(proposalID string) {
|
||||
rs.mu.Lock()
|
||||
state, exists := rs.pendingProposals[proposalID]
|
||||
if !exists {
|
||||
rs.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Mark as processed
|
||||
now := time.Now()
|
||||
state.ProcessedAt = &now
|
||||
rs.mu.Unlock()
|
||||
|
||||
lol.Info("processing proposal:", proposalID, "name:", state.Proposal.Name)
|
||||
|
||||
// Check for competing proposals for the same name
|
||||
competingProposals := rs.getCompetingProposals(state.Proposal.Name)
|
||||
|
||||
// Gather all attestations
|
||||
allAttestations := make([]*Attestation, 0)
|
||||
for _, p := range competingProposals {
|
||||
allAttestations = append(allAttestations, p.Attestations...)
|
||||
}
|
||||
|
||||
// Compute consensus
|
||||
proposalList := make([]*RegistrationProposal, 0, len(competingProposals))
|
||||
for _, p := range competingProposals {
|
||||
proposalList = append(proposalList, p.Proposal)
|
||||
}
|
||||
|
||||
result, err := rs.consensus.ComputeConsensus(proposalList, allAttestations)
|
||||
if chk.E(err) {
|
||||
lol.Err("consensus computation failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Log result
|
||||
if result.Conflicted {
|
||||
lol.Warn("consensus conflicted for name:", state.Proposal.Name, "reason:", result.Reason)
|
||||
return
|
||||
}
|
||||
|
||||
lol.Info("consensus reached for name:", state.Proposal.Name,
|
||||
"winner:", result.Winner.Event.GetIDString(),
|
||||
"confidence:", result.Confidence)
|
||||
|
||||
// Publish name state (kind 30102)
|
||||
if err := rs.publishNameState(result); chk.E(err) {
|
||||
lol.Err("failed to publish name state:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up processed proposals
|
||||
rs.cleanupProposals(state.Proposal.Name)
|
||||
}
|
||||
|
||||
// getCompetingProposals returns all pending proposals for the same name
|
||||
func (rs *RegistryService) getCompetingProposals(name string) []*ProposalState {
|
||||
rs.mu.RLock()
|
||||
defer rs.mu.RUnlock()
|
||||
|
||||
proposals := make([]*ProposalState, 0)
|
||||
for _, state := range rs.pendingProposals {
|
||||
if state.Proposal.Name == name {
|
||||
proposals = append(proposals, state)
|
||||
}
|
||||
}
|
||||
|
||||
return proposals
|
||||
}
|
||||
|
||||
// publishNameState publishes a name state event after consensus
|
||||
func (rs *RegistryService) publishNameState(result *ConsensusResult) error {
|
||||
nameState, err := rs.consensus.CreateNameState(result, rs.signer.Pub())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Create kind 30102 event
|
||||
// TODO: Sign with registry service key
|
||||
// TODO: Publish to database
|
||||
_ = nameState
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupProposals removes processed proposals from the pending map
|
||||
func (rs *RegistryService) cleanupProposals(name string) {
|
||||
rs.mu.Lock()
|
||||
defer rs.mu.Unlock()
|
||||
|
||||
for id, state := range rs.pendingProposals {
|
||||
if state.Proposal.Name == name && state.ProcessedAt != nil {
|
||||
// Cancel timer if still running
|
||||
if state.Timer != nil {
|
||||
state.Timer.Stop()
|
||||
}
|
||||
delete(rs.pendingProposals, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// refreshTrustGraph periodically refreshes the trust graph from other services
|
||||
func (rs *RegistryService) refreshTrustGraph() {
|
||||
defer rs.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-rs.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
rs.updateTrustGraph()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateTrustGraph fetches trust graphs from other services
|
||||
func (rs *RegistryService) updateTrustGraph() {
|
||||
lol.Debug("updating trust graph")
|
||||
|
||||
// TODO: Query kind 30101 events (trust graphs) from database
|
||||
// TODO: Parse and update trust graph
|
||||
// TODO: Remove expired trust graphs
|
||||
}
|
||||
|
||||
// bootstrapTrustGraph initializes trust relationships with bootstrap services
|
||||
func (rs *RegistryService) bootstrapTrustGraph() error {
|
||||
lol.Info("bootstrapping trust graph with", len(rs.config.BootstrapServices), "services")
|
||||
|
||||
for _, pubkeyHex := range rs.config.BootstrapServices {
|
||||
entry := TrustEntry{
|
||||
Pubkey: pubkeyHex,
|
||||
ServiceURL: "",
|
||||
TrustScore: 0.7, // Medium trust for bootstrap services
|
||||
}
|
||||
|
||||
if err := rs.trustGraph.AddEntry(entry); chk.E(err) {
|
||||
lol.Warn("failed to add bootstrap trust entry:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrustGraph returns the current trust graph
|
||||
func (rs *RegistryService) GetTrustGraph() *TrustGraph {
|
||||
return rs.trustGraph
|
||||
}
|
||||
|
||||
// GetMetrics returns registry service metrics
|
||||
func (rs *RegistryService) GetMetrics() *RegistryMetrics {
|
||||
rs.mu.RLock()
|
||||
defer rs.mu.RUnlock()
|
||||
|
||||
metrics := &RegistryMetrics{
|
||||
PendingProposals: len(rs.pendingProposals),
|
||||
TrustMetrics: rs.trustGraph.CalculateTrustMetrics(),
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// RegistryMetrics holds metrics about the registry service
|
||||
type RegistryMetrics struct {
|
||||
PendingProposals int
|
||||
TrustMetrics *TrustMetrics
|
||||
}
|
||||
|
||||
// QueryNameOwnership queries the ownership state of a name
|
||||
func (rs *RegistryService) QueryNameOwnership(name string) (*NameState, error) {
|
||||
return rs.consensus.QueryNameState(name)
|
||||
}
|
||||
|
||||
// ValidateProposal validates a proposal without adding it to pending
|
||||
func (rs *RegistryService) ValidateProposal(proposal *RegistrationProposal) error {
|
||||
return rs.consensus.ValidateProposal(proposal)
|
||||
}
|
||||
|
||||
// HandleEvent processes incoming FIND-related events
|
||||
func (rs *RegistryService) HandleEvent(ev *event.E) error {
|
||||
switch ev.Kind {
|
||||
case KindRegistrationProposal:
|
||||
// Parse proposal
|
||||
proposal, err := ParseRegistrationProposal(ev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rs.OnProposalReceived(proposal)
|
||||
|
||||
case KindAttestation:
|
||||
// Parse attestation
|
||||
// TODO: Implement attestation parsing and handling
|
||||
return nil
|
||||
|
||||
case KindTrustGraph:
|
||||
// Parse trust graph
|
||||
// TODO: Implement trust graph parsing and integration
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
383
pkg/find/trust.go
Normal file
383
pkg/find/trust.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package find
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
)
|
||||
|
||||
// TrustGraph manages trust relationships between registry services
|
||||
type TrustGraph struct {
|
||||
mu sync.RWMutex
|
||||
entries map[string][]TrustEntry // pubkey -> trust entries
|
||||
selfPubkey []byte // This registry service's pubkey
|
||||
lastUpdated map[string]time.Time // pubkey -> last update time
|
||||
decayFactors map[int]float64 // hop distance -> decay factor
|
||||
}
|
||||
|
||||
// NewTrustGraph creates a new trust graph
|
||||
func NewTrustGraph(selfPubkey []byte) *TrustGraph {
|
||||
return &TrustGraph{
|
||||
entries: make(map[string][]TrustEntry),
|
||||
selfPubkey: selfPubkey,
|
||||
lastUpdated: make(map[string]time.Time),
|
||||
decayFactors: map[int]float64{
|
||||
0: 1.0, // Direct trust (0-hop)
|
||||
1: 0.8, // 1-hop trust
|
||||
2: 0.6, // 2-hop trust
|
||||
3: 0.4, // 3-hop trust
|
||||
4: 0.0, // 4+ hops not counted
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddTrustGraph adds a trust graph from another registry service
|
||||
func (tg *TrustGraph) AddTrustGraph(graph *TrustGraph) error {
|
||||
tg.mu.Lock()
|
||||
defer tg.mu.Unlock()
|
||||
|
||||
sourcePubkey := hex.Enc(graph.selfPubkey)
|
||||
|
||||
// Copy entries from the source graph
|
||||
for pubkey, entries := range graph.entries {
|
||||
// Store the trust entries
|
||||
tg.entries[pubkey] = make([]TrustEntry, len(entries))
|
||||
copy(tg.entries[pubkey], entries)
|
||||
}
|
||||
|
||||
// Update last modified time
|
||||
tg.lastUpdated[sourcePubkey] = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddEntry adds a trust entry to the graph
|
||||
func (tg *TrustGraph) AddEntry(entry TrustEntry) error {
|
||||
if err := ValidateTrustScore(entry.TrustScore); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tg.mu.Lock()
|
||||
defer tg.mu.Unlock()
|
||||
|
||||
selfPubkey := hex.Enc(tg.selfPubkey)
|
||||
tg.entries[selfPubkey] = append(tg.entries[selfPubkey], entry)
|
||||
tg.lastUpdated[selfPubkey] = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTrustLevel returns the trust level for a given pubkey (0.0 to 1.0)
|
||||
// This computes both direct trust and inherited trust through the web of trust
|
||||
func (tg *TrustGraph) GetTrustLevel(pubkey []byte) float64 {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
pubkeyStr := hex.Enc(pubkey)
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
|
||||
// Check for direct trust first (0-hop)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
for _, entry := range entries {
|
||||
if entry.Pubkey == pubkeyStr {
|
||||
return entry.TrustScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute inherited trust through web of trust
|
||||
// Use breadth-first search to find shortest trust path
|
||||
maxHops := 3 // Maximum path length (configurable)
|
||||
visited := make(map[string]bool)
|
||||
queue := []trustPath{{pubkey: selfPubkeyStr, trust: 1.0, hops: 0}}
|
||||
visited[selfPubkeyStr] = true
|
||||
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
// Stop if we've exceeded max hops
|
||||
if current.hops > maxHops {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we found the target
|
||||
if current.pubkey == pubkeyStr {
|
||||
// Apply hop-based decay
|
||||
decayFactor := tg.decayFactors[current.hops]
|
||||
return current.trust * decayFactor
|
||||
}
|
||||
|
||||
// Expand to neighbors
|
||||
if entries, ok := tg.entries[current.pubkey]; ok {
|
||||
for _, entry := range entries {
|
||||
if !visited[entry.Pubkey] {
|
||||
visited[entry.Pubkey] = true
|
||||
queue = append(queue, trustPath{
|
||||
pubkey: entry.Pubkey,
|
||||
trust: current.trust * entry.TrustScore,
|
||||
hops: current.hops + 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No trust path found - return default minimal trust for unknown services
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// trustPath represents a path in the trust graph during BFS
|
||||
type trustPath struct {
|
||||
pubkey string
|
||||
trust float64
|
||||
hops int
|
||||
}
|
||||
|
||||
// GetDirectTrust returns direct trust relationships (0-hop only)
|
||||
func (tg *TrustGraph) GetDirectTrust() []TrustEntry {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
result := make([]TrustEntry, len(entries))
|
||||
copy(result, entries)
|
||||
return result
|
||||
}
|
||||
|
||||
return []TrustEntry{}
|
||||
}
|
||||
|
||||
// RemoveEntry removes a trust entry for a given pubkey
|
||||
func (tg *TrustGraph) RemoveEntry(pubkey string) {
|
||||
tg.mu.Lock()
|
||||
defer tg.mu.Unlock()
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
filtered := make([]TrustEntry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Pubkey != pubkey {
|
||||
filtered = append(filtered, entry)
|
||||
}
|
||||
}
|
||||
tg.entries[selfPubkeyStr] = filtered
|
||||
tg.lastUpdated[selfPubkeyStr] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateEntry updates an existing trust entry
|
||||
func (tg *TrustGraph) UpdateEntry(pubkey string, newScore float64) error {
|
||||
if err := ValidateTrustScore(newScore); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tg.mu.Lock()
|
||||
defer tg.mu.Unlock()
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
for i, entry := range entries {
|
||||
if entry.Pubkey == pubkey {
|
||||
tg.entries[selfPubkeyStr][i].TrustScore = newScore
|
||||
tg.lastUpdated[selfPubkeyStr] = time.Now()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("trust entry not found for pubkey: %s", pubkey)
|
||||
}
|
||||
|
||||
// GetAllEntries returns all trust entries in the graph (for debugging/export)
|
||||
func (tg *TrustGraph) GetAllEntries() map[string][]TrustEntry {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
result := make(map[string][]TrustEntry)
|
||||
for pubkey, entries := range tg.entries {
|
||||
result[pubkey] = make([]TrustEntry, len(entries))
|
||||
copy(result[pubkey], entries)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTrustedServices returns a list of all directly trusted service pubkeys
|
||||
func (tg *TrustGraph) GetTrustedServices() []string {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
pubkeys := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
pubkeys = append(pubkeys, entry.Pubkey)
|
||||
}
|
||||
return pubkeys
|
||||
}
|
||||
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// GetInheritedTrust computes inherited trust from one service to another
|
||||
// This is useful for debugging and understanding trust propagation
|
||||
func (tg *TrustGraph) GetInheritedTrust(fromPubkey, toPubkey string) (float64, []string) {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
// BFS to find shortest path and trust level
|
||||
type pathNode struct {
|
||||
pubkey string
|
||||
trust float64
|
||||
hops int
|
||||
path []string
|
||||
}
|
||||
|
||||
visited := make(map[string]bool)
|
||||
queue := []pathNode{{pubkey: fromPubkey, trust: 1.0, hops: 0, path: []string{fromPubkey}}}
|
||||
visited[fromPubkey] = true
|
||||
|
||||
maxHops := 3
|
||||
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
if current.hops > maxHops {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found target
|
||||
if current.pubkey == toPubkey {
|
||||
decayFactor := tg.decayFactors[current.hops]
|
||||
return current.trust * decayFactor, current.path
|
||||
}
|
||||
|
||||
// Expand neighbors
|
||||
if entries, ok := tg.entries[current.pubkey]; ok {
|
||||
for _, entry := range entries {
|
||||
if !visited[entry.Pubkey] {
|
||||
visited[entry.Pubkey] = true
|
||||
newPath := make([]string, len(current.path))
|
||||
copy(newPath, current.path)
|
||||
newPath = append(newPath, entry.Pubkey)
|
||||
|
||||
queue = append(queue, pathNode{
|
||||
pubkey: entry.Pubkey,
|
||||
trust: current.trust * entry.TrustScore,
|
||||
hops: current.hops + 1,
|
||||
path: newPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No path found
|
||||
return 0.0, nil
|
||||
}
|
||||
|
||||
// ExportTrustGraph exports the trust graph for this service as a TrustGraph event
|
||||
func (tg *TrustGraph) ExportTrustGraph() *TrustGraph {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
entries := tg.entries[selfPubkeyStr]
|
||||
|
||||
exported := &TrustGraph{
|
||||
Event: nil, // TODO: Create event
|
||||
Entries: make([]TrustEntry, len(entries)),
|
||||
Expiration: time.Now().Add(TrustGraphExpiry),
|
||||
}
|
||||
|
||||
copy(exported.Entries, entries)
|
||||
|
||||
return exported
|
||||
}
|
||||
|
||||
// CalculateTrustMetrics computes metrics about the trust graph
|
||||
func (tg *TrustGraph) CalculateTrustMetrics() *TrustMetrics {
|
||||
tg.mu.RLock()
|
||||
defer tg.mu.RUnlock()
|
||||
|
||||
metrics := &TrustMetrics{
|
||||
TotalServices: len(tg.entries),
|
||||
DirectTrust: 0,
|
||||
IndirectTrust: 0,
|
||||
AverageTrust: 0.0,
|
||||
TrustDistribution: make(map[string]int),
|
||||
}
|
||||
|
||||
selfPubkeyStr := hex.Enc(tg.selfPubkey)
|
||||
if entries, ok := tg.entries[selfPubkeyStr]; ok {
|
||||
metrics.DirectTrust = len(entries)
|
||||
|
||||
var trustSum float64
|
||||
for _, entry := range entries {
|
||||
trustSum += entry.TrustScore
|
||||
|
||||
// Categorize trust level
|
||||
if entry.TrustScore >= 0.8 {
|
||||
metrics.TrustDistribution["high"]++
|
||||
} else if entry.TrustScore >= 0.5 {
|
||||
metrics.TrustDistribution["medium"]++
|
||||
} else if entry.TrustScore >= 0.2 {
|
||||
metrics.TrustDistribution["low"]++
|
||||
} else {
|
||||
metrics.TrustDistribution["minimal"]++
|
||||
}
|
||||
}
|
||||
|
||||
if len(entries) > 0 {
|
||||
metrics.AverageTrust = trustSum / float64(len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate indirect trust (services reachable via multi-hop)
|
||||
// This is approximate - counts unique services reachable within 3 hops
|
||||
reachable := make(map[string]bool)
|
||||
queue := []string{selfPubkeyStr}
|
||||
visited := make(map[string]int) // pubkey -> hop count
|
||||
visited[selfPubkeyStr] = 0
|
||||
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
|
||||
currentHops := visited[current]
|
||||
if currentHops >= 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
if entries, ok := tg.entries[current]; ok {
|
||||
for _, entry := range entries {
|
||||
if _, seen := visited[entry.Pubkey]; !seen {
|
||||
visited[entry.Pubkey] = currentHops + 1
|
||||
queue = append(queue, entry.Pubkey)
|
||||
reachable[entry.Pubkey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metrics.IndirectTrust = len(reachable) - metrics.DirectTrust
|
||||
if metrics.IndirectTrust < 0 {
|
||||
metrics.IndirectTrust = 0
|
||||
}
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// TrustMetrics holds metrics about the trust graph
|
||||
type TrustMetrics struct {
|
||||
TotalServices int
|
||||
DirectTrust int
|
||||
IndirectTrust int
|
||||
AverageTrust float64
|
||||
TrustDistribution map[string]int // high/medium/low/minimal counts
|
||||
}
|
||||
Reference in New Issue
Block a user