forked from Snider/Poindexter
Merge PR #34: api-audit-refactor-3674138560719972443
This commit is contained in:
commit
69489db652
7 changed files with 238 additions and 201 deletions
27
AUDIT-API.md
27
AUDIT-API.md
|
|
@ -42,11 +42,36 @@ This document audits the public API of the Poindexter Go library, focusing on de
|
||||||
|
|
||||||
* The public API appears to be free of potential panics. The library consistently uses error returns and input validation to handle exceptional cases.
|
* The public API appears to be free of potential panics. The library consistently uses error returns and input validation to handle exceptional cases.
|
||||||
|
|
||||||
|
## 4. Recent API Design and Ergonomics Improvements
|
||||||
|
|
||||||
|
### 4.1. "God Class" Refactoring 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.
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
To address the "God Class" issue, `kdtree_analytics.go` was decomposed 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.
|
||||||
|
|
||||||
|
### 4.2. Method Naming Improvements
|
||||||
|
|
||||||
|
The method `ComputeDistanceDistribution` in `kdtree.go` was inconsistently named, as it actually computed axis-based distributions, not distance distributions.
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
Renamed the `ComputeDistanceDistribution` method to `ComputeAxisDistributions` to more accurately reflect its functionality.
|
||||||
|
|
||||||
|
### 4.3. Refactored `kdtree.go`
|
||||||
|
|
||||||
|
Updated `kdtree.go` to use the new, more focused modules. Removed the now-unnecessary `ResetAnalytics` methods, which were tightly coupled to the old analytics implementation.
|
||||||
|
|
||||||
## Summary and Recommendations
|
## Summary and Recommendations
|
||||||
|
|
||||||
The Poindexter library's public API is well-designed, consistent, and follows Go best practices. The use of generics, the options pattern, and clear error handling make it a robust and user-friendly library.
|
The Poindexter library's public API is well-designed, consistent, and follows Go best practices. The use of generics, the options pattern, and clear error handling make it a robust and user-friendly library. Recent refactoring efforts have improved modularity and maintainability.
|
||||||
|
|
||||||
**Recommendations:**
|
**Recommendations:**
|
||||||
|
|
||||||
1. **Naming Consistency:** Consider renaming `IsSorted`, `IsSortedStrings`, and `IsSortedFloat64s` to `IntsAreSorted`, `StringsAreSorted`, and `Float64sAreSorted` to align more closely with the standard library's `sort` package.
|
1. **Naming Consistency:** Consider renaming `IsSorted`, `IsSortedStrings`, and `IsSortedFloat64s` to `IntsAreSorted`, `StringsAreSorted`, and `Float64sAreSorted` to align more closely with the standard library's `sort` package.
|
||||||
2. **Defensive Copying:** The `Points()` method returns a copy of the internal slice, which is excellent. Ensure that any future methods that expose internal state also return copies to prevent mutation by callers.
|
2. **Defensive Copying:** The `Points()` method returns a copy of the internal slice, which is excellent. Ensure that any future methods that expose internal state also return copies to prevent mutation by callers.
|
||||||
|
3. **Continued Modularization:** The recent decomposition of `kdtree_analytics.go` is a positive step. Continue to evaluate the codebase for opportunities to separate concerns and improve maintainability.
|
||||||
|
|
|
||||||
|
|
@ -564,8 +564,8 @@ func (t *KDTree[T]) GetTopPeers(n int) []PeerStats {
|
||||||
return t.peerAnalytics.GetTopPeers(n)
|
return t.peerAnalytics.GetTopPeers(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ComputeDistanceDistribution analyzes the distribution of current point coordinates.
|
// ComputeAxisDistributions analyzes the distribution of current point coordinates.
|
||||||
func (t *KDTree[T]) ComputeDistanceDistribution(axisNames []string) []AxisDistribution {
|
func (t *KDTree[T]) ComputeAxisDistributions(axisNames []string) []AxisDistribution {
|
||||||
return ComputeAxisDistributions(t.points, axisNames)
|
return ComputeAxisDistributions(t.points, axisNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -395,197 +395,6 @@ func ComputeAxisDistributions[T any](points []KDPoint[T], axisNames []string) []
|
||||||
return result
|
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.
|
// NetworkHealthSummary aggregates overall network health metrics.
|
||||||
type NetworkHealthSummary struct {
|
type NetworkHealthSummary struct {
|
||||||
TotalPeers int `json:"totalPeers"`
|
TotalPeers int `json:"totalPeers"`
|
||||||
|
|
@ -657,6 +466,12 @@ type FeatureRanges struct {
|
||||||
Ranges []AxisStats `json:"ranges"`
|
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.
|
// DefaultPeerFeatureRanges returns sensible default ranges for peer features.
|
||||||
func DefaultPeerFeatureRanges() FeatureRanges {
|
func DefaultPeerFeatureRanges() FeatureRanges {
|
||||||
return FeatureRanges{
|
return FeatureRanges{
|
||||||
|
|
|
||||||
|
|
@ -618,7 +618,7 @@ func TestKDTreeDistanceDistribution(t *testing.T) {
|
||||||
}
|
}
|
||||||
tree, _ := NewKDTree(points)
|
tree, _ := NewKDTree(points)
|
||||||
|
|
||||||
dists := tree.ComputeDistanceDistribution([]string{"x", "y"})
|
dists := tree.ComputeAxisDistributions([]string{"x", "y"})
|
||||||
if len(dists) != 2 {
|
if len(dists) != 2 {
|
||||||
t.Errorf("expected 2 axis distributions, got %d", len(dists))
|
t.Errorf("expected 2 axis distributions, got %d", len(dists))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,6 @@ var (
|
||||||
ErrStatsDimMismatch = errors.New("kdtree: stats dimensionality mismatch")
|
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.
|
// NormStats holds per-axis normalisation statistics.
|
||||||
// For D dimensions, Stats has length D.
|
// For D dimensions, Stats has length D.
|
||||||
type NormStats struct {
|
type NormStats struct {
|
||||||
|
|
|
||||||
142
nat_metrics.go
Normal file
142
nat_metrics.go
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
package poindexter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
61
peer_trust.go
Normal file
61
peer_trust.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package poindexter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue