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.
┌─────────────────────────────────────────────────────────────┐
│ 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 │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
`
User → FIND CLI → Registration Proposal (kind 30100) → Relay → Database
↓
Registry Service (attestation)
↓
Attestation (kind 20100) → Other Registry Services
↓
Consensus → Name State (kind 30102)
`
`
Client → Query kind 30102 (name state) → Relay → Database → Response
Client → Query kind 30103 (records) → Relay → Database → Response
`
The relay already stores all parameterized replaceable events (kind 30xxx) and ephemeral events (kind 20xxx), which includes all FIND event types:
Status: No changes needed. The existing event storage system handles these automatically.
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 servicepkg/find/consensus.go - Consensus algorithm implementationpkg/find/trust.go - Trust graph calculationapp/find-service.go - Integration with relay serverKey Components:
// 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:
- Fetch attestations (kind 20100) from other registry services - Compute trust-weighted consensus - Publish name state (kind 30102) if consensus reached
Enhance existing query handlers to optimize FIND event queries.
Enhancements:
d tag indexes)New Helper Functions:
// 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)
Add FIND-specific configuration options to app/config/config.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"`
}
Add HTTP API endpoints for FIND operations (optional, for user convenience):
New Endpoints:
GET /api/find/names/:name - Query name stateGET /api/find/names/:name/records - Query all records for a nameGET /api/find/names/:name/records/:type - Query specific record typePOST /api/find/register - Submit registration proposalPOST /api/find/transfer - Submit transfer proposalGET /api/find/trust-graph - Query this relay's trust graphImplementation:
// 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)
}
Provide example code for clients to use FIND:
Example: Query name ownership
// 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
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
}
}
pkg/find/validation_test.go - already exists)- Valid names - Invalid names (too long, invalid characters, etc.) - Subdomain authority validation
pkg/find/consensus_test.go - new)- Single proposal scenario - Competing proposals - Trust-weighted scoring - Attestation window expiry
pkg/find/trust_test.go - new)- Direct trust relationships - Multi-hop trust inheritance - Trust decay calculation
pkg/find/integration_test.go - new)- Submit proposal - Generate attestations - Compute consensus - Verify name state
pkg/find/renewal_test.go - new)- Renewal during preferential window - Rejection outside renewal window - Expiration handling
pkg/find/records_test.go - new)- Publish DNS-style records - Verify owner authorization - Query records by type
- Trust-weighted consensus prevents new services from dominating - Age-weighted trust (new services have reduced influence)
- Diverse trust graphs make network-wide censorship difficult - Users can query different registry services aligned with their values
- Mandatory 1-year expiration - Preferential 30-day renewal window - No indefinite holding
- 30-day window reduces attack surface - Owner can submit multiple renewal attempts - Registry services filter by pubkey during renewal window
app/config/config.goQueryNameState, QueryNameRecords, IsNameAvailable)app/main.goimport { finalizeEvent, getPublicKey } from 'nostr-tools'
async function registerName(relay, privkey, name) {
const pubkey = getPublicKey(privkey)
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)
// Wait ~2 min for consensus, then check kind 30102
}
async function resolveNameToIP(relay, name) {
const nameState = await relay.get({ kinds: [30102], '#d': [name] })
if (!nameState) throw new Error('Name not registered')
const owner = nameState.tags.find(t => t[0] === 'owner')[1]
const records = await relay.list([{
kinds: [30103], '#name': [name], '#type': ['A'], authors: [owner]
}])
return records.map(e => e.tags.find(t => t[0] === 'value')[1])
}