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