You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
12 KiB
457 lines
12 KiB
package find |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"sync" |
|
"time" |
|
|
|
"lol.mleku.dev/chk" |
|
"next.orly.dev/pkg/database" |
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/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) { |
|
fmt.Printf("failed to bootstrap trust graph: %v\n", err) |
|
} |
|
} |
|
|
|
return rs, nil |
|
} |
|
|
|
// Start starts the registry service |
|
func (rs *RegistryService) Start() error { |
|
fmt.Println("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 { |
|
fmt.Println("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) { |
|
fmt.Printf("invalid proposal: %v\n", err) |
|
return err |
|
} |
|
|
|
proposalID := hex.Enc(proposal.Event.ID) |
|
|
|
rs.mu.Lock() |
|
defer rs.mu.Unlock() |
|
|
|
// Check if already processing |
|
if _, exists := rs.pendingProposals[proposalID]; exists { |
|
return nil |
|
} |
|
|
|
fmt.Printf("received new proposal: %s name: %s\n", proposalID, 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, err := hex.Dec(proposalID) |
|
if err != nil || 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: hex.Enc(proposal.Event.ID), |
|
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 |
|
|
|
fmt.Printf("published attestation for proposal: %s decision: %s\n", proposal.Name, 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() |
|
|
|
fmt.Printf("processing proposal: %s name: %s\n", proposalID, 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) { |
|
fmt.Printf("consensus computation failed: %v\n", err) |
|
return |
|
} |
|
|
|
// Log result |
|
if result.Conflicted { |
|
fmt.Printf("consensus conflicted for name: %s reason: %s\n", state.Proposal.Name, result.Reason) |
|
return |
|
} |
|
|
|
fmt.Printf("consensus reached for name: %s winner: %s confidence: %f\n", |
|
state.Proposal.Name, |
|
hex.Enc(result.Winner.Event.ID), |
|
result.Confidence) |
|
|
|
// Publish name state (kind 30102) |
|
if err := rs.publishNameState(result); chk.E(err) { |
|
fmt.Printf("failed to publish name state: %v\n", 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() { |
|
fmt.Println("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 { |
|
fmt.Printf("bootstrapping trust graph with %d services\n", len(rs.config.BootstrapServices)) |
|
|
|
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) { |
|
fmt.Printf("failed to add bootstrap trust entry: %v\n", 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 |
|
} |
|
}
|
|
|