Skip to main content
Witnesses are nodes that observe and report on the liveness of other nodes. Styx maintains a trust score for each witness and aggregates their reports into a unified belief distribution.

Witness Reports

A witness report contains:
  • Witness: The NodeID of the reporting node
  • Target: The NodeID being observed
  • Belief: The witness’s belief distribution about the target
  • Trust: The current trust score of this witness
source/witness/aggregator.go
type WitnessReport struct {
    Witness types.NodeID
    Target  types.NodeID
    Belief  types.Belief
    Trust   TrustScore
}

Trust Scoring

Each witness has a trust score in the range [0.1, 1.0] that determines how much weight their reports carry.

Trust Constants

source/witness/registry.go
type TrustScore float64

const (
    // MaxTrust is full trust in a witness
    MaxTrust TrustScore = 1.0
    // MinTrust is minimum trust (but never zero - always some weight)
    MinTrust TrustScore = 0.1
    // DefaultTrust for new witnesses
    DefaultTrust TrustScore = 0.8
    // DecayRate per incorrect report
    DecayRate = 0.1
    // RecoveryRate per correct report
    RecoveryRate = 0.05
)
Trust never drops to zero (minimum 0.1). Even unreliable witnesses retain some influence, preventing complete exclusion.

Trust Adjustment

Trust scores are adjusted based on report accuracy:
source/witness/registry.go
// RecordCorrect marks a witness report as correct
// Trust increases slightly
func (r *Registry) RecordCorrect(id types.NodeID) {
    w := r.getOrCreate(id)
    w.CorrectReports++
    w.Trust += TrustScore(RecoveryRate)
    if w.Trust > MaxTrust {
        w.Trust = MaxTrust
    }
}

// RecordWrong marks a witness report as wrong
// P12: Trust decays for bad witnesses
func (r *Registry) RecordWrong(id types.NodeID) {
    w := r.getOrCreate(id)
    w.WrongReports++
    w.Trust -= TrustScore(DecayRate)
    if w.Trust < MinTrust {
        w.Trust = MinTrust
    }
}
Witnesses that provide incorrect reports lose trust faster (0.1 per wrong) than they gain it from correct reports (0.05 per correct). This asymmetry creates a high bar for reliability.

Witness Registry

The registry tracks all known witnesses and their trust levels:
source/witness/registry.go
type WitnessRecord struct {
    ID             types.NodeID
    Trust          TrustScore
    CorrectReports int
    WrongReports   int
    LastReport     types.Belief
}

type Registry struct {
    mu        sync.RWMutex
    witnesses map[types.NodeID]*WitnessRecord
}

Example Usage

reg := witness.NewRegistry()

// Register a new witness (starts with DefaultTrust = 0.8)
reg.Register(nodeID)

// Get current trust
trust := reg.GetTrust(nodeID) // Returns 0.8

// Record correct report
reg.RecordCorrect(nodeID)
trust = reg.GetTrust(nodeID) // Now 0.85

// Record wrong report
reg.RecordWrong(nodeID)
trust = reg.GetTrust(nodeID) // Now 0.75

Belief Aggregation

The aggregator combines multiple witness reports into a single belief distribution using trust-weighted averaging.

Aggregation Algorithm

source/witness/aggregator.go
func (a *Aggregator) Aggregate(reports []WitnessReport) AggregateResult {
    // Calculate weighted average of beliefs
    var totalWeight float64
    var aliveSum, deadSum, unknownSum float64

    for _, r := range reports {
        trust := float64(a.registry.GetTrust(r.Witness))
        totalWeight += trust

        aliveSum += r.Belief.Alive().Value() * trust
        deadSum += r.Belief.Dead().Value() * trust
        unknownSum += r.Belief.Unknown().Value() * trust
    }

    avgAlive := aliveSum / totalWeight
    avgDead := deadSum / totalWeight
    avgUnknown := unknownSum / totalWeight

    // ... additional processing for disagreement and correlation
}

Aggregation Result

source/witness/aggregator.go
type AggregateResult struct {
    Belief       types.Belief
    Disagreement float64 // 0 = all agree, 1 = max disagreement
    WitnessCount int
    Reports      []WitnessReport
}

Disagreement Detection

When witnesses disagree, Styx tracks the disagreement level and increases uncertainty rather than hiding the conflict.
source/witness/aggregator.go
// calculateDisagreement measures variance in witness opinions
// P10: We track this, not hide it
func (a *Aggregator) calculateDisagreement(
    reports []WitnessReport,
    avgAlive, avgDead float64,
) float64 {
    if len(reports) < 2 {
        return 0
    }

    var variance float64
    for _, r := range reports {
        diffAlive := r.Belief.Alive().Value() - avgAlive
        diffDead := r.Belief.Dead().Value() - avgDead
        variance += diffAlive*diffAlive + diffDead*diffDead
    }
    variance /= float64(len(reports))

    // Normalize to [0,1]
    return math.Min(math.Sqrt(variance), 1.0)
}
When disagreement exceeds 0.3 (30%), the aggregator increases the unknown component:
source/witness/aggregator.go
// P10: High disagreement increases unknown
if disagreement > 0.3 {
    // Significant disagreement - widen uncertainty
    reduction := disagreement * 0.5
    avgAlive *= (1 - reduction)
    avgDead *= (1 - reduction)
    avgUnknown = 1.0 - avgAlive - avgDead
}

Correlation Detection

If all witnesses report very similar values (correlation > 0.9), they may share a failure mode. Confidence is reduced by 30% to account for this.
source/witness/aggregator.go
// detectCorrelation checks if witnesses are too similar
// P11: Correlated witnesses weaken confidence
func (a *Aggregator) detectCorrelation(reports []WitnessReport) float64 {
    if len(reports) < 2 {
        return 0
    }

    // Check how similar all reports are
    first := reports[0].Belief
    var totalDiff float64

    for i := 1; i < len(reports); i++ {
        b := reports[i].Belief
        diffAlive := math.Abs(first.Alive().Value() - b.Alive().Value())
        diffDead := math.Abs(first.Dead().Value() - b.Dead().Value())
        totalDiff += diffAlive + diffDead
    }

    avgDiff := totalDiff / float64(len(reports)-1)

    // Low difference = high correlation
    return 1.0 - math.Min(avgDiff*2, 1.0)
}
When correlation is too high:
source/witness/aggregator.go
// P11: Correlated witnesses reduce confidence
// If witnesses are too similar, increase unknown
correlation := a.detectCorrelation(reports)
if correlation > 0.9 {
    // Too correlated - reduce confidence
    factor := 0.7
    avgAlive *= factor
    avgDead *= factor
    avgUnknown = 1.0 - avgAlive - avgDead
}
High correlation suggests witnesses may share infrastructure or failure modes. Styx compensates by reducing confidence in the aggregated result.

Aggregation Examples

Example 1: Agreement

Three witnesses all report high alive confidence:
reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.8},
    {Witness: w2, Belief: MustBelief(0.85, 0.10, 0.05), Trust: 0.9},
    {Witness: w3, Belief: MustBelief(0.95, 0.03, 0.02), Trust: 0.7},
}

result := aggregator.Aggregate(reports)
// result.Belief ≈ [A:90% D:6% U:4%]
// result.Disagreement ≈ 0.05 (low)

Example 2: Disagreement

Witnesses split on the status:
reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.8, 0.1, 0.1), Trust: 0.8},
    {Witness: w2, Belief: MustBelief(0.2, 0.7, 0.1), Trust: 0.8},
    {Witness: w3, Belief: MustBelief(0.4, 0.4, 0.2), Trust: 0.7},
}

result := aggregator.Aggregate(reports)
// result.Belief ≈ [A:35% D:35% U:30%] (uncertainty increased)
// result.Disagreement ≈ 0.4 (high)

Example 3: Trust Weighting

Low-trust witness has less influence:
reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.9}, // Trusted
    {Witness: w2, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.85}, // Trusted
    {Witness: w3, Belief: MustBelief(0.1, 0.8, 0.1), Trust: 0.2},  // Unreliable
}

result := aggregator.Aggregate(reports)
// Result weighted toward w1 and w2 due to higher trust
// result.Belief ≈ [A:80% D:15% U:5%]

Example 4: High Correlation

All witnesses report identical values:
reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.8},
    {Witness: w2, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.8},
    {Witness: w3, Belief: MustBelief(0.9, 0.05, 0.05), Trust: 0.8},
}

result := aggregator.Aggregate(reports)
// Correlation detected (> 0.9)
// Confidence reduced by 30%
// result.Belief ≈ [A:63% D:3.5% U:33.5%]

Edge Cases

No Reports

result := aggregator.Aggregate([]WitnessReport{})
// Returns: UnknownBelief() [A:0% D:0% U:100%]

Single Report

reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.8, 0.1, 0.1), Trust: 0.9},
}

result := aggregator.Aggregate(reports)
// Returns the single report's belief unchanged
// Disagreement = 0 (no other reports to disagree with)

All Witnesses Untrusted

reports := []WitnessReport{
    {Witness: w1, Belief: MustBelief(0.8, 0.1, 0.1), Trust: 0.1},
    {Witness: w2, Belief: MustBelief(0.7, 0.2, 0.1), Trust: 0.1},
}

result := aggregator.Aggregate(reports)
// Still aggregates (trust never reaches zero)
// But effectively low confidence in result

Key Takeaways

Trust-Weighted

Reports are weighted by witness trust scores, giving reliable witnesses more influence

Disagreement Tracked

High disagreement increases uncertainty rather than being hidden

Correlation Detected

Too-similar reports trigger a 30% confidence reduction to account for shared failure modes

Trust Decays

Incorrect reports reduce trust faster (0.1) than correct ones increase it (0.05)

Next Steps

Finality

Learn how overwhelming witness agreement triggers irreversible death

Partition Detection

See how extreme disagreement indicates network partitions

Build docs developers (and LLMs) love