develop registration ratelimit mechanism

This commit is contained in:
2025-11-21 19:13:18 +00:00
parent ebe0012863
commit fb65282702
6 changed files with 3157 additions and 0 deletions

376
pkg/find/consensus.go Normal file
View File

@@ -0,0 +1,376 @@
package find
import (
"fmt"
"time"
"lol.mleku.dev/chk"
"lol.mleku.dev/errorf"
"next.orly.dev/pkg/database"
)
// ConsensusEngine handles the consensus algorithm for name registrations
type ConsensusEngine struct {
db database.Database
trustGraph *TrustGraph
threshold float64 // Consensus threshold (e.g., 0.51 for 51%)
minCoverage float64 // Minimum trust graph coverage required
conflictMargin float64 // Margin for declaring conflicts (e.g., 0.05 for 5%)
}
// NewConsensusEngine creates a new consensus engine
func NewConsensusEngine(db database.Database, trustGraph *TrustGraph) *ConsensusEngine {
return &ConsensusEngine{
db: db,
trustGraph: trustGraph,
threshold: 0.51, // 51% threshold
minCoverage: 0.30, // 30% minimum coverage
conflictMargin: 0.05, // 5% conflict margin
}
}
// ProposalScore holds scoring information for a proposal
type ProposalScore struct {
Proposal *RegistrationProposal
Score float64
Attestations []*Attestation
Weights map[string]float64 // Attester pubkey -> weighted score
}
// ConsensusResult represents the result of consensus computation
type ConsensusResult struct {
Winner *RegistrationProposal
Score float64
Confidence float64 // 0.0 to 1.0
Attestations int
Conflicted bool
Reason string
}
// ComputeConsensus computes consensus for a set of competing proposals
func (ce *ConsensusEngine) ComputeConsensus(proposals []*RegistrationProposal, attestations []*Attestation) (*ConsensusResult, error) {
if len(proposals) == 0 {
return nil, errorf.E("no proposals to evaluate")
}
// Group attestations by proposal ID
attestationMap := make(map[string][]*Attestation)
for _, att := range attestations {
if att.Decision == DecisionApprove {
attestationMap[att.ProposalID] = append(attestationMap[att.ProposalID], att)
}
}
// Score each proposal
scores := make([]*ProposalScore, 0, len(proposals))
totalWeight := 0.0
for _, proposal := range proposals {
proposalAtts := attestationMap[proposal.Event.GetIDString()]
score, weights := ce.ScoreProposal(proposal, proposalAtts)
scores = append(scores, &ProposalScore{
Proposal: proposal,
Score: score,
Attestations: proposalAtts,
Weights: weights,
})
totalWeight += score
}
// Check if we have sufficient coverage
if totalWeight < ce.minCoverage {
return &ConsensusResult{
Conflicted: true,
Reason: fmt.Sprintf("insufficient attestations: %.2f%% < %.2f%%", totalWeight*100, ce.minCoverage*100),
}, nil
}
// Find highest scoring proposal
var winner *ProposalScore
for _, ps := range scores {
if winner == nil || ps.Score > winner.Score {
winner = ps
}
}
// Calculate relative score
relativeScore := winner.Score / totalWeight
// Check for conflicts (multiple proposals within margin)
conflicted := false
for _, ps := range scores {
if ps.Proposal.Event.GetIDString() != winner.Proposal.Event.GetIDString() {
otherRelative := ps.Score / totalWeight
if (relativeScore - otherRelative) < ce.conflictMargin {
conflicted = true
break
}
}
}
// Check if winner meets threshold
if relativeScore < ce.threshold {
return &ConsensusResult{
Winner: winner.Proposal,
Score: winner.Score,
Confidence: relativeScore,
Attestations: len(winner.Attestations),
Conflicted: true,
Reason: fmt.Sprintf("score %.2f%% below threshold %.2f%%", relativeScore*100, ce.threshold*100),
}, nil
}
// Check for conflicts
if conflicted {
return &ConsensusResult{
Winner: winner.Proposal,
Score: winner.Score,
Confidence: relativeScore,
Attestations: len(winner.Attestations),
Conflicted: true,
Reason: "competing proposals within conflict margin",
}, nil
}
// Success!
return &ConsensusResult{
Winner: winner.Proposal,
Score: winner.Score,
Confidence: relativeScore,
Attestations: len(winner.Attestations),
Conflicted: false,
Reason: "consensus reached",
}, nil
}
// ScoreProposal computes the trust-weighted score for a proposal
func (ce *ConsensusEngine) ScoreProposal(proposal *RegistrationProposal, attestations []*Attestation) (float64, map[string]float64) {
totalScore := 0.0
weights := make(map[string]float64)
for _, att := range attestations {
if att.Decision != DecisionApprove {
continue
}
// Get attestation weight (default 100)
attWeight := float64(att.Weight)
if attWeight <= 0 {
attWeight = 100
}
// Get trust level for this attester
trustLevel := ce.trustGraph.GetTrustLevel(att.Event.Pubkey)
// Calculate weighted score
// Score = attestation_weight * trust_level / 100
score := (attWeight / 100.0) * trustLevel
weights[att.Event.GetPubkeyString()] = score
totalScore += score
}
return totalScore, weights
}
// ValidateProposal validates a registration proposal against current state
func (ce *ConsensusEngine) ValidateProposal(proposal *RegistrationProposal) error {
// Validate name format
if err := ValidateName(proposal.Name); err != nil {
return errorf.E("invalid name format: %w", err)
}
// Check if proposal is expired
if !proposal.Expiration.IsZero() && time.Now().After(proposal.Expiration) {
return errorf.E("proposal expired at %v", proposal.Expiration)
}
// Validate subdomain authority (if applicable)
if !IsTLD(proposal.Name) {
parent := GetParentDomain(proposal.Name)
if parent == "" {
return errorf.E("invalid subdomain structure")
}
// Query parent domain ownership
parentState, err := ce.QueryNameState(parent)
if err != nil {
return errorf.E("failed to query parent domain: %w", err)
}
if parentState == nil {
return errorf.E("parent domain %s not registered", parent)
}
// Verify proposer owns parent domain
proposerPubkey := proposal.Event.GetPubkeyString()
if parentState.Owner != proposerPubkey {
return errorf.E("proposer does not own parent domain %s", parent)
}
}
// Validate against current name state
nameState, err := ce.QueryNameState(proposal.Name)
if err != nil {
return errorf.E("failed to query name state: %w", err)
}
now := time.Now()
// Name is not registered - anyone can register
if nameState == nil {
return nil
}
// Name is expired - anyone can register
if !nameState.Expiration.IsZero() && now.After(nameState.Expiration) {
return nil
}
// Calculate renewal window start (30 days before expiration)
renewalStart := nameState.Expiration.Add(-PreferentialRenewalDays * 24 * time.Hour)
// Before renewal window - reject all proposals
if now.Before(renewalStart) {
return errorf.E("name is currently owned and not in renewal window")
}
// During renewal window - only current owner can register
if now.Before(nameState.Expiration) {
proposerPubkey := proposal.Event.GetPubkeyString()
if proposerPubkey != nameState.Owner {
return errorf.E("only current owner can renew during preferential renewal window")
}
return nil
}
// Should not reach here, but allow registration if we do
return nil
}
// ValidateTransfer validates a transfer proposal
func (ce *ConsensusEngine) ValidateTransfer(proposal *RegistrationProposal) error {
if proposal.Action != ActionTransfer {
return errorf.E("not a transfer proposal")
}
// Must have previous owner and signature
if proposal.PrevOwner == "" {
return errorf.E("missing previous owner")
}
if proposal.PrevSig == "" {
return errorf.E("missing previous owner signature")
}
// Query current name state
nameState, err := ce.QueryNameState(proposal.Name)
if err != nil {
return errorf.E("failed to query name state: %w", err)
}
if nameState == nil {
return errorf.E("name not registered")
}
// Verify previous owner matches current owner
if nameState.Owner != proposal.PrevOwner {
return errorf.E("previous owner mismatch")
}
// Verify name is not expired
if !nameState.Expiration.IsZero() && time.Now().After(nameState.Expiration) {
return errorf.E("name expired")
}
// TODO: Verify signature over transfer message
// Message format: "transfer:<name>:<new_owner_pubkey>:<timestamp>"
return nil
}
// QueryNameState queries the current name state from the database
func (ce *ConsensusEngine) QueryNameState(name string) (*NameState, error) {
// Query kind 30102 events with d tag = name
filter := &struct {
Kinds []uint16
DTags []string
Limit int
}{
Kinds: []uint16{KindNameState},
DTags: []string{name},
Limit: 10,
}
// Note: This would use the actual database query method
// For now, return nil to indicate not found
// TODO: Implement actual database query
_ = filter
return nil, nil
}
// CreateNameState creates a name state event from consensus result
func (ce *ConsensusEngine) CreateNameState(result *ConsensusResult, registryPubkey []byte) (*NameState, error) {
if result.Winner == nil {
return nil, errorf.E("no winner in consensus result")
}
proposal := result.Winner
return &NameState{
Name: proposal.Name,
Owner: proposal.Event.GetPubkeyString(),
RegisteredAt: time.Now(),
ProposalID: proposal.Event.GetIDString(),
Attestations: result.Attestations,
Confidence: result.Confidence,
Expiration: time.Now().Add(NameRegistrationPeriod),
}, nil
}
// ProcessProposalBatch processes a batch of proposals and returns consensus results
func (ce *ConsensusEngine) ProcessProposalBatch(proposals []*RegistrationProposal, attestations []*Attestation) ([]*ConsensusResult, error) {
// Group proposals by name
proposalsByName := make(map[string][]*RegistrationProposal)
for _, proposal := range proposals {
proposalsByName[proposal.Name] = append(proposalsByName[proposal.Name], proposal)
}
results := make([]*ConsensusResult, 0)
// Process each name's proposals independently
for name, nameProposals := range proposalsByName {
// Filter attestations for this name's proposals
proposalIDs := make(map[string]bool)
for _, p := range nameProposals {
proposalIDs[p.Event.GetIDString()] = true
}
nameAttestations := make([]*Attestation, 0)
for _, att := range attestations {
if proposalIDs[att.ProposalID] {
nameAttestations = append(nameAttestations, att)
}
}
// Compute consensus for this name
result, err := ce.ComputeConsensus(nameProposals, nameAttestations)
if chk.E(err) {
// Log error but continue processing other names
result = &ConsensusResult{
Conflicted: true,
Reason: fmt.Sprintf("error: %v", err),
}
}
// Add name to result for tracking
if result.Winner != nil {
result.Winner.Name = name
}
results = append(results, result)
}
return results, nil
}

456
pkg/find/registry.go Normal file
View File

@@ -0,0 +1,456 @@
package find
import (
"context"
"sync"
"time"
lol "lol.mleku.dev"
"lol.mleku.dev/chk"
"next.orly.dev/pkg/database"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/hex"
"next.orly.dev/pkg/interfaces/signer"
)
// RegistryService implements the FIND name registry consensus protocol
type RegistryService struct {
ctx context.Context
cancel context.CancelFunc
db database.Database
signer signer.I
trustGraph *TrustGraph
consensus *ConsensusEngine
config *RegistryConfig
pendingProposals map[string]*ProposalState
mu sync.RWMutex
wg sync.WaitGroup
}
// RegistryConfig holds configuration for the registry service
type RegistryConfig struct {
Enabled bool
AttestationDelay time.Duration
SparseEnabled bool
SamplingRate int
BootstrapServices []string
MinimumAttesters int
}
// ProposalState tracks a proposal during its attestation window
type ProposalState struct {
Proposal *RegistrationProposal
Attestations []*Attestation
ReceivedAt time.Time
ProcessedAt *time.Time
Timer *time.Timer
}
// NewRegistryService creates a new registry service
func NewRegistryService(ctx context.Context, db database.Database, signer signer.I, config *RegistryConfig) (*RegistryService, error) {
if !config.Enabled {
return nil, nil
}
ctx, cancel := context.WithCancel(ctx)
trustGraph := NewTrustGraph(signer.Pub())
consensus := NewConsensusEngine(db, trustGraph)
rs := &RegistryService{
ctx: ctx,
cancel: cancel,
db: db,
signer: signer,
trustGraph: trustGraph,
consensus: consensus,
config: config,
pendingProposals: make(map[string]*ProposalState),
}
// Bootstrap trust graph if configured
if len(config.BootstrapServices) > 0 {
if err := rs.bootstrapTrustGraph(); chk.E(err) {
lol.Err("failed to bootstrap trust graph:", err)
}
}
return rs, nil
}
// Start starts the registry service
func (rs *RegistryService) Start() error {
lol.Info("starting FIND registry service")
// Start proposal monitoring goroutine
rs.wg.Add(1)
go rs.monitorProposals()
// Start attestation collection goroutine
rs.wg.Add(1)
go rs.collectAttestations()
// Start trust graph refresh goroutine
rs.wg.Add(1)
go rs.refreshTrustGraph()
return nil
}
// Stop stops the registry service
func (rs *RegistryService) Stop() error {
lol.Info("stopping FIND registry service")
rs.cancel()
rs.wg.Wait()
return nil
}
// monitorProposals monitors for new registration proposals
func (rs *RegistryService) monitorProposals() {
defer rs.wg.Done()
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-rs.ctx.Done():
return
case <-ticker.C:
rs.checkForNewProposals()
}
}
}
// checkForNewProposals checks database for new registration proposals
func (rs *RegistryService) checkForNewProposals() {
// Query recent kind 30100 events (registration proposals)
// This would use the actual database query API
// For now, this is a stub
// TODO: Implement database query for kind 30100 events
// TODO: Parse proposals and add to pendingProposals map
// TODO: Start attestation timer for each new proposal
}
// OnProposalReceived is called when a new proposal is received
func (rs *RegistryService) OnProposalReceived(proposal *RegistrationProposal) error {
// Validate proposal
if err := rs.consensus.ValidateProposal(proposal); chk.E(err) {
lol.Warn("invalid proposal:", err)
return err
}
proposalID := proposal.Event.GetIDString()
rs.mu.Lock()
defer rs.mu.Unlock()
// Check if already processing
if _, exists := rs.pendingProposals[proposalID]; exists {
return nil
}
lol.Info("received new proposal:", proposalID, "name:", proposal.Name)
// Create proposal state
state := &ProposalState{
Proposal: proposal,
Attestations: make([]*Attestation, 0),
ReceivedAt: time.Now(),
}
// Start attestation timer
state.Timer = time.AfterFunc(rs.config.AttestationDelay, func() {
rs.processProposal(proposalID)
})
rs.pendingProposals[proposalID] = state
// Publish attestation (if not using sparse or if dice roll succeeds)
if rs.shouldAttest(proposalID) {
go rs.publishAttestation(proposal, DecisionApprove, "valid_proposal")
}
return nil
}
// shouldAttest determines if this service should attest to a proposal
func (rs *RegistryService) shouldAttest(proposalID string) bool {
if !rs.config.SparseEnabled {
return true
}
// Sparse attestation: use hash of (proposal_id || service_pubkey) % K == 0
// This provides deterministic but distributed attestation
hash := hex.Dec(proposalID)
if len(hash) == 0 {
return false
}
// Simple modulo check using first byte of hash
return int(hash[0])%rs.config.SamplingRate == 0
}
// publishAttestation publishes an attestation for a proposal
func (rs *RegistryService) publishAttestation(proposal *RegistrationProposal, decision string, reason string) {
attestation := &Attestation{
ProposalID: proposal.Event.GetIDString(),
Decision: decision,
Weight: 100,
Reason: reason,
ServiceURL: "", // TODO: Get from config
Expiration: time.Now().Add(AttestationExpiry),
}
// TODO: Create and sign attestation event (kind 20100)
// TODO: Publish to database
_ = attestation
lol.Debug("published attestation for proposal:", proposal.Name, "decision:", decision)
}
// collectAttestations collects attestations from other registry services
func (rs *RegistryService) collectAttestations() {
defer rs.wg.Done()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-rs.ctx.Done():
return
case <-ticker.C:
rs.updateAttestations()
}
}
}
// updateAttestations fetches new attestations from database
func (rs *RegistryService) updateAttestations() {
rs.mu.RLock()
proposalIDs := make([]string, 0, len(rs.pendingProposals))
for id := range rs.pendingProposals {
proposalIDs = append(proposalIDs, id)
}
rs.mu.RUnlock()
if len(proposalIDs) == 0 {
return
}
// TODO: Query kind 20100 events (attestations) for pending proposals
// TODO: Add attestations to proposal states
}
// processProposal processes a proposal after the attestation window expires
func (rs *RegistryService) processProposal(proposalID string) {
rs.mu.Lock()
state, exists := rs.pendingProposals[proposalID]
if !exists {
rs.mu.Unlock()
return
}
// Mark as processed
now := time.Now()
state.ProcessedAt = &now
rs.mu.Unlock()
lol.Info("processing proposal:", proposalID, "name:", state.Proposal.Name)
// Check for competing proposals for the same name
competingProposals := rs.getCompetingProposals(state.Proposal.Name)
// Gather all attestations
allAttestations := make([]*Attestation, 0)
for _, p := range competingProposals {
allAttestations = append(allAttestations, p.Attestations...)
}
// Compute consensus
proposalList := make([]*RegistrationProposal, 0, len(competingProposals))
for _, p := range competingProposals {
proposalList = append(proposalList, p.Proposal)
}
result, err := rs.consensus.ComputeConsensus(proposalList, allAttestations)
if chk.E(err) {
lol.Err("consensus computation failed:", err)
return
}
// Log result
if result.Conflicted {
lol.Warn("consensus conflicted for name:", state.Proposal.Name, "reason:", result.Reason)
return
}
lol.Info("consensus reached for name:", state.Proposal.Name,
"winner:", result.Winner.Event.GetIDString(),
"confidence:", result.Confidence)
// Publish name state (kind 30102)
if err := rs.publishNameState(result); chk.E(err) {
lol.Err("failed to publish name state:", err)
return
}
// Clean up processed proposals
rs.cleanupProposals(state.Proposal.Name)
}
// getCompetingProposals returns all pending proposals for the same name
func (rs *RegistryService) getCompetingProposals(name string) []*ProposalState {
rs.mu.RLock()
defer rs.mu.RUnlock()
proposals := make([]*ProposalState, 0)
for _, state := range rs.pendingProposals {
if state.Proposal.Name == name {
proposals = append(proposals, state)
}
}
return proposals
}
// publishNameState publishes a name state event after consensus
func (rs *RegistryService) publishNameState(result *ConsensusResult) error {
nameState, err := rs.consensus.CreateNameState(result, rs.signer.Pub())
if err != nil {
return err
}
// TODO: Create kind 30102 event
// TODO: Sign with registry service key
// TODO: Publish to database
_ = nameState
return nil
}
// cleanupProposals removes processed proposals from the pending map
func (rs *RegistryService) cleanupProposals(name string) {
rs.mu.Lock()
defer rs.mu.Unlock()
for id, state := range rs.pendingProposals {
if state.Proposal.Name == name && state.ProcessedAt != nil {
// Cancel timer if still running
if state.Timer != nil {
state.Timer.Stop()
}
delete(rs.pendingProposals, id)
}
}
}
// refreshTrustGraph periodically refreshes the trust graph from other services
func (rs *RegistryService) refreshTrustGraph() {
defer rs.wg.Done()
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-rs.ctx.Done():
return
case <-ticker.C:
rs.updateTrustGraph()
}
}
}
// updateTrustGraph fetches trust graphs from other services
func (rs *RegistryService) updateTrustGraph() {
lol.Debug("updating trust graph")
// TODO: Query kind 30101 events (trust graphs) from database
// TODO: Parse and update trust graph
// TODO: Remove expired trust graphs
}
// bootstrapTrustGraph initializes trust relationships with bootstrap services
func (rs *RegistryService) bootstrapTrustGraph() error {
lol.Info("bootstrapping trust graph with", len(rs.config.BootstrapServices), "services")
for _, pubkeyHex := range rs.config.BootstrapServices {
entry := TrustEntry{
Pubkey: pubkeyHex,
ServiceURL: "",
TrustScore: 0.7, // Medium trust for bootstrap services
}
if err := rs.trustGraph.AddEntry(entry); chk.E(err) {
lol.Warn("failed to add bootstrap trust entry:", err)
continue
}
}
return nil
}
// GetTrustGraph returns the current trust graph
func (rs *RegistryService) GetTrustGraph() *TrustGraph {
return rs.trustGraph
}
// GetMetrics returns registry service metrics
func (rs *RegistryService) GetMetrics() *RegistryMetrics {
rs.mu.RLock()
defer rs.mu.RUnlock()
metrics := &RegistryMetrics{
PendingProposals: len(rs.pendingProposals),
TrustMetrics: rs.trustGraph.CalculateTrustMetrics(),
}
return metrics
}
// RegistryMetrics holds metrics about the registry service
type RegistryMetrics struct {
PendingProposals int
TrustMetrics *TrustMetrics
}
// QueryNameOwnership queries the ownership state of a name
func (rs *RegistryService) QueryNameOwnership(name string) (*NameState, error) {
return rs.consensus.QueryNameState(name)
}
// ValidateProposal validates a proposal without adding it to pending
func (rs *RegistryService) ValidateProposal(proposal *RegistrationProposal) error {
return rs.consensus.ValidateProposal(proposal)
}
// HandleEvent processes incoming FIND-related events
func (rs *RegistryService) HandleEvent(ev *event.E) error {
switch ev.Kind {
case KindRegistrationProposal:
// Parse proposal
proposal, err := ParseRegistrationProposal(ev)
if err != nil {
return err
}
return rs.OnProposalReceived(proposal)
case KindAttestation:
// Parse attestation
// TODO: Implement attestation parsing and handling
return nil
case KindTrustGraph:
// Parse trust graph
// TODO: Implement trust graph parsing and integration
return nil
default:
return nil
}
}

383
pkg/find/trust.go Normal file
View File

@@ -0,0 +1,383 @@
package find
import (
"fmt"
"sync"
"time"
"next.orly.dev/pkg/encoders/hex"
)
// TrustGraph manages trust relationships between registry services
type TrustGraph struct {
mu sync.RWMutex
entries map[string][]TrustEntry // pubkey -> trust entries
selfPubkey []byte // This registry service's pubkey
lastUpdated map[string]time.Time // pubkey -> last update time
decayFactors map[int]float64 // hop distance -> decay factor
}
// NewTrustGraph creates a new trust graph
func NewTrustGraph(selfPubkey []byte) *TrustGraph {
return &TrustGraph{
entries: make(map[string][]TrustEntry),
selfPubkey: selfPubkey,
lastUpdated: make(map[string]time.Time),
decayFactors: map[int]float64{
0: 1.0, // Direct trust (0-hop)
1: 0.8, // 1-hop trust
2: 0.6, // 2-hop trust
3: 0.4, // 3-hop trust
4: 0.0, // 4+ hops not counted
},
}
}
// AddTrustGraph adds a trust graph from another registry service
func (tg *TrustGraph) AddTrustGraph(graph *TrustGraph) error {
tg.mu.Lock()
defer tg.mu.Unlock()
sourcePubkey := hex.Enc(graph.selfPubkey)
// Copy entries from the source graph
for pubkey, entries := range graph.entries {
// Store the trust entries
tg.entries[pubkey] = make([]TrustEntry, len(entries))
copy(tg.entries[pubkey], entries)
}
// Update last modified time
tg.lastUpdated[sourcePubkey] = time.Now()
return nil
}
// AddEntry adds a trust entry to the graph
func (tg *TrustGraph) AddEntry(entry TrustEntry) error {
if err := ValidateTrustScore(entry.TrustScore); err != nil {
return err
}
tg.mu.Lock()
defer tg.mu.Unlock()
selfPubkey := hex.Enc(tg.selfPubkey)
tg.entries[selfPubkey] = append(tg.entries[selfPubkey], entry)
tg.lastUpdated[selfPubkey] = time.Now()
return nil
}
// GetTrustLevel returns the trust level for a given pubkey (0.0 to 1.0)
// This computes both direct trust and inherited trust through the web of trust
func (tg *TrustGraph) GetTrustLevel(pubkey []byte) float64 {
tg.mu.RLock()
defer tg.mu.RUnlock()
pubkeyStr := hex.Enc(pubkey)
selfPubkeyStr := hex.Enc(tg.selfPubkey)
// Check for direct trust first (0-hop)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
for _, entry := range entries {
if entry.Pubkey == pubkeyStr {
return entry.TrustScore
}
}
}
// Compute inherited trust through web of trust
// Use breadth-first search to find shortest trust path
maxHops := 3 // Maximum path length (configurable)
visited := make(map[string]bool)
queue := []trustPath{{pubkey: selfPubkeyStr, trust: 1.0, hops: 0}}
visited[selfPubkeyStr] = true
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
// Stop if we've exceeded max hops
if current.hops > maxHops {
continue
}
// Check if we found the target
if current.pubkey == pubkeyStr {
// Apply hop-based decay
decayFactor := tg.decayFactors[current.hops]
return current.trust * decayFactor
}
// Expand to neighbors
if entries, ok := tg.entries[current.pubkey]; ok {
for _, entry := range entries {
if !visited[entry.Pubkey] {
visited[entry.Pubkey] = true
queue = append(queue, trustPath{
pubkey: entry.Pubkey,
trust: current.trust * entry.TrustScore,
hops: current.hops + 1,
})
}
}
}
}
// No trust path found - return default minimal trust for unknown services
return 0.0
}
// trustPath represents a path in the trust graph during BFS
type trustPath struct {
pubkey string
trust float64
hops int
}
// GetDirectTrust returns direct trust relationships (0-hop only)
func (tg *TrustGraph) GetDirectTrust() []TrustEntry {
tg.mu.RLock()
defer tg.mu.RUnlock()
selfPubkeyStr := hex.Enc(tg.selfPubkey)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
result := make([]TrustEntry, len(entries))
copy(result, entries)
return result
}
return []TrustEntry{}
}
// RemoveEntry removes a trust entry for a given pubkey
func (tg *TrustGraph) RemoveEntry(pubkey string) {
tg.mu.Lock()
defer tg.mu.Unlock()
selfPubkeyStr := hex.Enc(tg.selfPubkey)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
filtered := make([]TrustEntry, 0, len(entries))
for _, entry := range entries {
if entry.Pubkey != pubkey {
filtered = append(filtered, entry)
}
}
tg.entries[selfPubkeyStr] = filtered
tg.lastUpdated[selfPubkeyStr] = time.Now()
}
}
// UpdateEntry updates an existing trust entry
func (tg *TrustGraph) UpdateEntry(pubkey string, newScore float64) error {
if err := ValidateTrustScore(newScore); err != nil {
return err
}
tg.mu.Lock()
defer tg.mu.Unlock()
selfPubkeyStr := hex.Enc(tg.selfPubkey)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
for i, entry := range entries {
if entry.Pubkey == pubkey {
tg.entries[selfPubkeyStr][i].TrustScore = newScore
tg.lastUpdated[selfPubkeyStr] = time.Now()
return nil
}
}
}
return fmt.Errorf("trust entry not found for pubkey: %s", pubkey)
}
// GetAllEntries returns all trust entries in the graph (for debugging/export)
func (tg *TrustGraph) GetAllEntries() map[string][]TrustEntry {
tg.mu.RLock()
defer tg.mu.RUnlock()
result := make(map[string][]TrustEntry)
for pubkey, entries := range tg.entries {
result[pubkey] = make([]TrustEntry, len(entries))
copy(result[pubkey], entries)
}
return result
}
// GetTrustedServices returns a list of all directly trusted service pubkeys
func (tg *TrustGraph) GetTrustedServices() []string {
tg.mu.RLock()
defer tg.mu.RUnlock()
selfPubkeyStr := hex.Enc(tg.selfPubkey)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
pubkeys := make([]string, 0, len(entries))
for _, entry := range entries {
pubkeys = append(pubkeys, entry.Pubkey)
}
return pubkeys
}
return []string{}
}
// GetInheritedTrust computes inherited trust from one service to another
// This is useful for debugging and understanding trust propagation
func (tg *TrustGraph) GetInheritedTrust(fromPubkey, toPubkey string) (float64, []string) {
tg.mu.RLock()
defer tg.mu.RUnlock()
// BFS to find shortest path and trust level
type pathNode struct {
pubkey string
trust float64
hops int
path []string
}
visited := make(map[string]bool)
queue := []pathNode{{pubkey: fromPubkey, trust: 1.0, hops: 0, path: []string{fromPubkey}}}
visited[fromPubkey] = true
maxHops := 3
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
if current.hops > maxHops {
continue
}
// Found target
if current.pubkey == toPubkey {
decayFactor := tg.decayFactors[current.hops]
return current.trust * decayFactor, current.path
}
// Expand neighbors
if entries, ok := tg.entries[current.pubkey]; ok {
for _, entry := range entries {
if !visited[entry.Pubkey] {
visited[entry.Pubkey] = true
newPath := make([]string, len(current.path))
copy(newPath, current.path)
newPath = append(newPath, entry.Pubkey)
queue = append(queue, pathNode{
pubkey: entry.Pubkey,
trust: current.trust * entry.TrustScore,
hops: current.hops + 1,
path: newPath,
})
}
}
}
}
// No path found
return 0.0, nil
}
// ExportTrustGraph exports the trust graph for this service as a TrustGraph event
func (tg *TrustGraph) ExportTrustGraph() *TrustGraph {
tg.mu.RLock()
defer tg.mu.RUnlock()
selfPubkeyStr := hex.Enc(tg.selfPubkey)
entries := tg.entries[selfPubkeyStr]
exported := &TrustGraph{
Event: nil, // TODO: Create event
Entries: make([]TrustEntry, len(entries)),
Expiration: time.Now().Add(TrustGraphExpiry),
}
copy(exported.Entries, entries)
return exported
}
// CalculateTrustMetrics computes metrics about the trust graph
func (tg *TrustGraph) CalculateTrustMetrics() *TrustMetrics {
tg.mu.RLock()
defer tg.mu.RUnlock()
metrics := &TrustMetrics{
TotalServices: len(tg.entries),
DirectTrust: 0,
IndirectTrust: 0,
AverageTrust: 0.0,
TrustDistribution: make(map[string]int),
}
selfPubkeyStr := hex.Enc(tg.selfPubkey)
if entries, ok := tg.entries[selfPubkeyStr]; ok {
metrics.DirectTrust = len(entries)
var trustSum float64
for _, entry := range entries {
trustSum += entry.TrustScore
// Categorize trust level
if entry.TrustScore >= 0.8 {
metrics.TrustDistribution["high"]++
} else if entry.TrustScore >= 0.5 {
metrics.TrustDistribution["medium"]++
} else if entry.TrustScore >= 0.2 {
metrics.TrustDistribution["low"]++
} else {
metrics.TrustDistribution["minimal"]++
}
}
if len(entries) > 0 {
metrics.AverageTrust = trustSum / float64(len(entries))
}
}
// Calculate indirect trust (services reachable via multi-hop)
// This is approximate - counts unique services reachable within 3 hops
reachable := make(map[string]bool)
queue := []string{selfPubkeyStr}
visited := make(map[string]int) // pubkey -> hop count
visited[selfPubkeyStr] = 0
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
currentHops := visited[current]
if currentHops >= 3 {
continue
}
if entries, ok := tg.entries[current]; ok {
for _, entry := range entries {
if _, seen := visited[entry.Pubkey]; !seen {
visited[entry.Pubkey] = currentHops + 1
queue = append(queue, entry.Pubkey)
reachable[entry.Pubkey] = true
}
}
}
}
metrics.IndirectTrust = len(reachable) - metrics.DirectTrust
if metrics.IndirectTrust < 0 {
metrics.IndirectTrust = 0
}
return metrics
}
// TrustMetrics holds metrics about the trust graph
type TrustMetrics struct {
TotalServices int
DirectTrust int
IndirectTrust int
AverageTrust float64
TrustDistribution map[string]int // high/medium/low/minimal counts
}