Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions AUDIT-API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# API Design and Ergonomics Audit

## Findings

### 1. "God Class" in `kdtree_analytics.go`

The file `kdtree_analytics.go` exhibited "God Class" characteristics, combining core tree analytics with unrelated responsibilities like peer trust scoring and NAT metrics. This made the code difficult to maintain and understand.

### 2. Inconsistent Naming

The method `ComputeDistanceDistribution` in `kdtree.go` was inconsistently named, as it actually computed axis-based distributions, not distance distributions.

## Changes Made

### 1. Decomposed `kdtree_analytics.go`

To address the "God Class" issue, I decomposed `kdtree_analytics.go` into three distinct files:

* `kdtree_analytics.go`: Now contains only the core tree analytics.
* `peer_trust.go`: Contains the peer trust scoring logic.
* `nat_metrics.go`: Contains the NAT-related metrics.

### 2. Renamed `ComputeDistanceDistribution`

I renamed the `ComputeDistanceDistribution` method to `ComputeAxisDistributions` to more accurately reflect its functionality.

### 3. Refactored `kdtree.go`

I updated `kdtree.go` to use the new, more focused modules. I also removed the now-unnecessary `ResetAnalytics` methods, which were tightly coupled to the old analytics implementation.

## Conclusion

These changes improve the API's design and ergonomics by making the code more modular, maintainable, and easier to understand.
4 changes: 2 additions & 2 deletions kdtree.go
Original file line number Diff line number Diff line change
Expand Up @@ -564,8 +564,8 @@ func (t *KDTree[T]) GetTopPeers(n int) []PeerStats {
return t.peerAnalytics.GetTopPeers(n)
}

// ComputeDistanceDistribution analyzes the distribution of current point coordinates.
func (t *KDTree[T]) ComputeDistanceDistribution(axisNames []string) []AxisDistribution {
// ComputeAxisDistributions analyzes the distribution of current point coordinates.
func (t *KDTree[T]) ComputeAxisDistributions(axisNames []string) []AxisDistribution {
return ComputeAxisDistributions(t.points, axisNames)
}

Expand Down
197 changes: 6 additions & 191 deletions kdtree_analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,197 +395,6 @@ func ComputeAxisDistributions[T any](points []KDPoint[T], axisNames []string) []
return result
}

// NATRoutingMetrics provides metrics specifically for NAT traversal routing decisions.
type NATRoutingMetrics struct {
// Connectivity score (0-1): higher means better reachability
ConnectivityScore float64 `json:"connectivityScore"`
// Symmetry score (0-1): higher means more symmetric NAT (easier to traverse)
SymmetryScore float64 `json:"symmetryScore"`
// Relay requirement probability (0-1): likelihood peer needs relay
RelayProbability float64 `json:"relayProbability"`
// Direct connection success rate (historical)
DirectSuccessRate float64 `json:"directSuccessRate"`
// Average RTT in milliseconds
AvgRTTMs float64 `json:"avgRttMs"`
// Jitter (RTT variance) in milliseconds
JitterMs float64 `json:"jitterMs"`
// Packet loss rate (0-1)
PacketLossRate float64 `json:"packetLossRate"`
// Bandwidth estimate in Mbps
BandwidthMbps float64 `json:"bandwidthMbps"`
// NAT type classification
NATType string `json:"natType"`
// Last probe timestamp
LastProbeAt time.Time `json:"lastProbeAt"`
}

// NATTypeClassification enumerates common NAT types for routing decisions.
type NATTypeClassification string

const (
NATTypeOpen NATTypeClassification = "open" // No NAT / Public IP
NATTypeFullCone NATTypeClassification = "full_cone" // Easy to traverse
NATTypeRestrictedCone NATTypeClassification = "restricted_cone" // Moderate difficulty
NATTypePortRestricted NATTypeClassification = "port_restricted" // Harder to traverse
NATTypeSymmetric NATTypeClassification = "symmetric" // Hardest to traverse
NATTypeSymmetricUDP NATTypeClassification = "symmetric_udp" // UDP-only symmetric
NATTypeUnknown NATTypeClassification = "unknown" // Not yet classified
NATTypeBehindCGNAT NATTypeClassification = "cgnat" // Carrier-grade NAT
NATTypeFirewalled NATTypeClassification = "firewalled" // Blocked by firewall
NATTypeRelayRequired NATTypeClassification = "relay_required" // Must use relay
)

// PeerQualityScore computes a composite quality score for peer selection.
// Higher scores indicate better peers for routing.
// Weights can be customized; default weights emphasize latency and reliability.
func PeerQualityScore(metrics NATRoutingMetrics, weights *QualityWeights) float64 {
w := DefaultQualityWeights()
if weights != nil {
w = *weights
}

// Normalize metrics to 0-1 scale (higher is better)
latencyScore := 1.0 - math.Min(metrics.AvgRTTMs/1000.0, 1.0) // <1000ms is acceptable
jitterScore := 1.0 - math.Min(metrics.JitterMs/100.0, 1.0) // <100ms jitter
lossScore := 1.0 - metrics.PacketLossRate // 0 loss is best
bandwidthScore := math.Min(metrics.BandwidthMbps/100.0, 1.0) // 100Mbps is excellent
connectivityScore := metrics.ConnectivityScore // Already 0-1
symmetryScore := metrics.SymmetryScore // Already 0-1
directScore := metrics.DirectSuccessRate // Already 0-1
relayPenalty := 1.0 - metrics.RelayProbability // Prefer non-relay

// NAT type bonus/penalty
natScore := natTypeScore(metrics.NATType)

// Weighted combination
score := (w.Latency*latencyScore +
w.Jitter*jitterScore +
w.PacketLoss*lossScore +
w.Bandwidth*bandwidthScore +
w.Connectivity*connectivityScore +
w.Symmetry*symmetryScore +
w.DirectSuccess*directScore +
w.RelayPenalty*relayPenalty +
w.NATType*natScore) / w.Total()

return math.Max(0, math.Min(1, score))
}

// QualityWeights configures the importance of each metric in peer selection.
type QualityWeights struct {
Latency float64 `json:"latency"`
Jitter float64 `json:"jitter"`
PacketLoss float64 `json:"packetLoss"`
Bandwidth float64 `json:"bandwidth"`
Connectivity float64 `json:"connectivity"`
Symmetry float64 `json:"symmetry"`
DirectSuccess float64 `json:"directSuccess"`
RelayPenalty float64 `json:"relayPenalty"`
NATType float64 `json:"natType"`
}

// Total returns the sum of all weights for normalization.
func (w QualityWeights) Total() float64 {
return w.Latency + w.Jitter + w.PacketLoss + w.Bandwidth +
w.Connectivity + w.Symmetry + w.DirectSuccess + w.RelayPenalty + w.NATType
}

// DefaultQualityWeights returns sensible defaults for peer selection.
func DefaultQualityWeights() QualityWeights {
return QualityWeights{
Latency: 3.0, // Most important
Jitter: 1.5,
PacketLoss: 2.0,
Bandwidth: 1.0,
Connectivity: 2.0,
Symmetry: 1.0,
DirectSuccess: 2.0,
RelayPenalty: 1.5,
NATType: 1.0,
}
}

// natTypeScore returns a 0-1 score based on NAT type (higher is better for routing).
func natTypeScore(natType string) float64 {
switch NATTypeClassification(natType) {
case NATTypeOpen:
return 1.0
case NATTypeFullCone:
return 0.9
case NATTypeRestrictedCone:
return 0.7
case NATTypePortRestricted:
return 0.5
case NATTypeSymmetric:
return 0.3
case NATTypeSymmetricUDP:
return 0.25
case NATTypeBehindCGNAT:
return 0.2
case NATTypeFirewalled:
return 0.1
case NATTypeRelayRequired:
return 0.05
default:
return 0.4 // Unknown gets middle score
}
}

// TrustMetrics tracks trust and reputation for peer selection.
type TrustMetrics struct {
// ReputationScore (0-1): aggregated trust score
ReputationScore float64 `json:"reputationScore"`
// SuccessfulTransactions: count of successful exchanges
SuccessfulTransactions int64 `json:"successfulTransactions"`
// FailedTransactions: count of failed/aborted exchanges
FailedTransactions int64 `json:"failedTransactions"`
// AgeSeconds: how long this peer has been known
AgeSeconds int64 `json:"ageSeconds"`
// LastSuccessAt: last successful interaction
LastSuccessAt time.Time `json:"lastSuccessAt"`
// LastFailureAt: last failed interaction
LastFailureAt time.Time `json:"lastFailureAt"`
// VouchCount: number of other peers vouching for this peer
VouchCount int `json:"vouchCount"`
// FlagCount: number of reports against this peer
FlagCount int `json:"flagCount"`
// ProofOfWork: computational proof of stake/work
ProofOfWork float64 `json:"proofOfWork"`
}

// ComputeTrustScore calculates a composite trust score from trust metrics.
func ComputeTrustScore(t TrustMetrics) float64 {
total := t.SuccessfulTransactions + t.FailedTransactions
if total == 0 {
// New peer with no history: moderate trust with age bonus
ageBonus := math.Min(float64(t.AgeSeconds)/(86400*30), 0.2) // Up to 0.2 for 30 days
return 0.5 + ageBonus
}

// Base score from success rate
successRate := float64(t.SuccessfulTransactions) / float64(total)

// Volume confidence (more transactions = more confident)
volumeConfidence := 1 - 1/(1+float64(total)/10)

// Vouch/flag adjustment
vouchBonus := math.Min(float64(t.VouchCount)*0.02, 0.15)
flagPenalty := math.Min(float64(t.FlagCount)*0.05, 0.3)

// Recency bonus (recent success = better)
recencyBonus := 0.0
if !t.LastSuccessAt.IsZero() {
hoursSince := time.Since(t.LastSuccessAt).Hours()
recencyBonus = 0.1 * math.Exp(-hoursSince/168) // Decays over ~1 week
}

// Proof of work bonus
powBonus := math.Min(t.ProofOfWork*0.1, 0.1)

score := successRate*volumeConfidence + vouchBonus - flagPenalty + recencyBonus + powBonus
return math.Max(0, math.Min(1, score))
}

// NetworkHealthSummary aggregates overall network health metrics.
type NetworkHealthSummary struct {
TotalPeers int `json:"totalPeers"`
Expand Down Expand Up @@ -657,6 +466,12 @@ type FeatureRanges struct {
Ranges []AxisStats `json:"ranges"`
}

// AxisStats holds statistics for a single axis.
type AxisStats struct {
Min float64 `json:"min"`
Max float64 `json:"max"`
}

// DefaultPeerFeatureRanges returns sensible default ranges for peer features.
func DefaultPeerFeatureRanges() FeatureRanges {
return FeatureRanges{
Expand Down
2 changes: 1 addition & 1 deletion kdtree_analytics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ func TestKDTreeDistanceDistribution(t *testing.T) {
}
tree, _ := NewKDTree(points)

dists := tree.ComputeDistanceDistribution([]string{"x", "y"})
dists := tree.ComputeAxisDistributions([]string{"x", "y"})
if len(dists) != 2 {
t.Errorf("expected 2 axis distributions, got %d", len(dists))
}
Expand Down
6 changes: 0 additions & 6 deletions kdtree_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ var (
ErrStatsDimMismatch = errors.New("kdtree: stats dimensionality mismatch")
)

// AxisStats holds the min/max observed for a single axis.
type AxisStats struct {
Min float64
Max float64
}

// NormStats holds per-axis normalisation statistics.
// For D dimensions, Stats has length D.
type NormStats struct {
Expand Down
Loading
Loading