6 changed files with 3157 additions and 0 deletions
@ -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 |
||||||
@ -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! |
||||||
@ -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. |
||||||
@ -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 |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
@ -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
|
||||||
|
} |
||||||
Loading…
Reference in new issue