forked from Snider/Poindexter
feat: Refactor kdtree_analytics.go and create API audit
Decomposed the "God Class" `kdtree_analytics.go` into three distinct files: - `kdtree_analytics.go`: Core tree analytics - `peer_trust.go`: Peer trust scoring logic - `nat_metrics.go`: NAT-related metrics Renamed `ComputeDistanceDistribution` to `ComputeAxisDistributions` for clarity. Created `AUDIT-API.md` to document the findings and changes. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
This commit is contained in:
parent
91146b212a
commit
93b41ed07e
7 changed files with 245 additions and 200 deletions
33
AUDIT-API.md
Normal file
33
AUDIT-API.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
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