trust.gno
5.14 Kb · 182 lines
1// Package trust implements the scoring rules behind Guildhall reputation.
2//
3// Reputation is earned one record at a time. When a piece of work is paid
4// out, the realm that recorded it asks this package how many points the work
5// is worth. The answer depends on who vouched for it: every approving
6// reviewer contributes their current trust, and the record is worth
7// Base points scaled by the average trust of its reviewers.
8//
9// Trust is expressed in basis points (0..10000):
10//
11// - seed reviewers (chosen by the realm that uses this package) always carry full trust;
12// - everyone else carries trust proportional to their own score, reaching
13// full trust at TrustThreshold and never exceeding it;
14// - a reviewer who is also the worker carries no trust at all.
15//
16// With the default TrustFloorBps of 0, work vouched for only by strangers is
17// still recorded but earns nothing. This is what makes the score resistant to
18// sybils: a ring of fresh addresses reviewing each other stays at zero until
19// someone already trusted vouches for one of them. Trust flows outward from
20// the seeds, EigenTrust-style, one paid record at a time.
21//
22// Scores are computed incrementally at record time, never retroactively, so
23// every point on a profile can be traced to one record and the trust its
24// reviewers held when they approved it.
25package trust
26
27import (
28 "errors"
29)
30
31// MaxBps is the basis-point denominator used for trust and shares.
32const MaxBps int64 = 10000
33
34// Params tunes the scoring rules.
35type Params struct {
36 // Base is the number of points a record earns when every reviewer
37 // carries full trust and the worker received the full payout.
38 Base int64
39 // TrustThreshold is the score at which a non-seed reviewer reaches
40 // full trust.
41 TrustThreshold int64
42 // TrustFloorBps is the minimum trust any eligible reviewer carries.
43 TrustFloorBps int64
44 // SelfFundedFactorBps scales the points of work funded by the worker
45 // themselves. Zero means self-funded work earns nothing.
46 SelfFundedFactorBps int64
47}
48
49// DefaultParams returns the parameters Guildhall launches with.
50func DefaultParams() Params {
51 return Params{
52 Base: 100,
53 TrustThreshold: 500,
54 TrustFloorBps: 0,
55 SelfFundedFactorBps: 0,
56 }
57}
58
59// Validate reports whether the parameters are usable.
60func (p Params) Validate() error {
61 if p.Base <= 0 {
62 return errors.New("trust: base must be positive")
63 }
64 if p.TrustThreshold <= 0 {
65 return errors.New("trust: threshold must be positive")
66 }
67 if p.TrustFloorBps < 0 || p.TrustFloorBps > MaxBps {
68 return errors.New("trust: floor must be within 0..10000 bps")
69 }
70 if p.SelfFundedFactorBps < 0 || p.SelfFundedFactorBps > MaxBps {
71 return errors.New("trust: self-funded factor must be within 0..10000 bps")
72 }
73 return nil
74}
75
76// Reviewer describes a reviewer at the moment they vouched for a record.
77type Reviewer struct {
78 Addr address
79 Score int64
80 Seed bool
81}
82
83// Input describes a paid piece of work.
84type Input struct {
85 Worker address
86 Poster address
87 Reviewers []Reviewer
88 // ShareBps is the fraction of the milestone paid to the worker, in basis
89 // points. Approved milestones pay 10000; arbitrated disputes may pay
90 // less.
91 ShareBps int64
92}
93
94// Result is the scoring outcome for one record.
95type Result struct {
96 // Points awarded to the worker.
97 Points int64
98 // TrustBps holds each reviewer's trust, index-aligned with
99 // Input.Reviewers.
100 TrustBps []int64
101 // AvgTrustBps is the mean trust across reviewers.
102 AvgTrustBps int64
103}
104
105// TrustOf returns the trust, in basis points, carried by a reviewer.
106func TrustOf(r Reviewer, worker address, p Params) int64 {
107 if r.Addr == worker {
108 return 0
109 }
110 if r.Seed {
111 return MaxBps
112 }
113 t := int64(0)
114 if r.Score > 0 {
115 if r.Score >= p.TrustThreshold {
116 t = MaxBps
117 } else {
118 t = r.Score * MaxBps / p.TrustThreshold
119 }
120 }
121 if t < p.TrustFloorBps {
122 t = p.TrustFloorBps
123 }
124 return t
125}
126
127// Evaluate scores a paid piece of work.
128func Evaluate(in Input, p Params) (Result, error) {
129 if err := p.Validate(); err != nil {
130 return Result{}, err
131 }
132 if in.ShareBps < 0 || in.ShareBps > MaxBps {
133 return Result{}, errors.New("trust: share must be within 0..10000 bps")
134 }
135 res := Result{TrustBps: make([]int64, len(in.Reviewers))}
136 if len(in.Reviewers) == 0 {
137 return res, nil
138 }
139
140 seen := map[address]bool{}
141 sum := int64(0)
142 counted := int64(0)
143 for i, r := range in.Reviewers {
144 if seen[r.Addr] {
145 // A reviewer counts once, however many times they appear.
146 continue
147 }
148 seen[r.Addr] = true
149 t := TrustOf(r, in.Worker, p)
150 res.TrustBps[i] = t
151 sum += t
152 counted++
153 }
154 res.AvgTrustBps = sum / counted
155
156 points := p.Base * res.AvgTrustBps / MaxBps
157 points = points * in.ShareBps / MaxBps
158 if in.Poster == in.Worker {
159 points = points * p.SelfFundedFactorBps / MaxBps
160 }
161 res.Points = points
162 return res, nil
163}
164
165// Level names a score band. Levels are cosmetic; realms that gate on
166// reputation should compare scores directly.
167func Level(score int64) string {
168 switch {
169 case score >= 5000:
170 return "Grandmaster"
171 case score >= 2000:
172 return "Master"
173 case score >= 800:
174 return "Artisan"
175 case score >= 300:
176 return "Journeyman"
177 case score > 0:
178 return "Apprentice"
179 default:
180 return "Newcomer"
181 }
182}