vouch.gno
8.73 Kb · 221 lines
1// Package vouch is a web of trust other realms can gate on: one address
2// vouches for another, in writing, optionally with GNOT locked behind it.
3//
4// The one call that matters is a plain read:
5//
6// import "gno.land/r/moul/x/social/vouch/v0"
7//
8// func Claim(cur realm) {
9// if !vouch.IsTrusted(cur.Previous().Address(), 2) {
10// panic("get two people to vouch for you first")
11// }
12// …
13// }
14//
15// [IsTrusted] takes no cur realm on purpose. A read with no realm token is
16// borrowed: gno opens no realm frame for it, so it costs the caller nothing
17// beyond the read and cannot be tricked into acting as anybody. Nothing here
18// branches on who is asking, and no read in this realm ever will: the answer
19// to "is this address trusted" must not depend on who wants to know.
20//
21// # What it does, and what it refuses to do
22//
23// [Vouch] is payable. Any coins sent with it are locked as a bond on that
24// vouch, which is the voucher putting their own money where their claim is. A
25// bond is optional and zero is the ordinary case. [Revoke] takes the vouch
26// back and credits the bond to the voucher, who collects it with [Withdraw].
27//
28// There is no slashing in v0 and that is the interesting half. Slashing needs
29// an arbiter, somebody who decides a vouch was a lie, and every candidate is
30// a design question rather than a feature: a DAO vote is a popularity contest,
31// a challenge market pays whoever is loudest, an oracle is one key that can
32// confiscate anybody's money. The bond is still worth having without it,
33// because an illiquid deposit is a cost a hundred throwaway addresses cannot
34// all pay at once.
35//
36// # No token
37//
38// This realm issues none, deliberately. A transferable vouch is a bought
39// reputation, and the moment a vouch can be sold the score stops measuring
40// what it says it measures. The bond is GNOT: value at risk, without being a
41// market in trust itself. The README says what would change the answer.
42package vouch
43
44import (
45 "chain"
46 "chain/banker"
47 "chain/runtime"
48 "strconv"
49
50 "gno.land/p/moul/x/envelope/v0"
51 vo "gno.land/p/moul/x/social/vouch/v0"
52)
53
54// realmPath is this realm's own path, the one its gnomod.toml module line
55// declares. It is written out rather than read from the frame, because every
56// read this realm exports is borrowed and would report whichever realm called
57// it, building every link against somebody else's page.
58const realmPath = "gno.land/r/moul/x/social/vouch/v0"
59
60// denom is the only coin a bond can be posted in.
61const denom = "ugnot"
62
63// BoardSize is how many addresses the index page ranks.
64const BoardSize = 10
65
66// graph holds every vouch and the refund ledger. A redeploy would wipe it
67// while leaving the bonded coins at this address, which is why this realm is
68// not private (see gnomod.toml).
69var graph = vo.NewGraph()
70
71// Vouch records that the caller stands behind target, for the stated reason.
72//
73// It is payable: coins sent with the call are locked as a bond on this vouch
74// and are returned by [Revoke], never by anything else. Sending nothing is
75// fine and is the ordinary case.
76//
77// Vouching again for the same address UPDATES the reason and ADDS to the
78// bond. It does not count twice: the score this realm exists to publish is a
79// count of people, so a second transaction from the same address buys
80// precisely nothing. Vouching for yourself is refused.
81func Vouch(cur realm, target address, reason string) {
82 who, userCall := caller(cur)
83 bond := bondOf(userCall)
84
85 updated, err := graph.Record(who, target, reason, bond, runtime.ChainHeight())
86 if err != nil {
87 panic(err.Error())
88 }
89 event := "Vouch"
90 if updated {
91 event = "Revouch"
92 }
93 chain.Emit(event, "from", who.String(), "for", target.String(),
94 "bond", strconv.FormatInt(bond, 10))
95}
96
97// Revoke withdraws the caller's vouch for target and credits any bond back to
98// them. The coins do not move here: call [Withdraw] to collect them.
99//
100// Splitting it in two is the pull-payment pattern. A realm that sent on revoke
101// would be handing control to the recipient in the middle of its own state
102// change, and a recipient that refuses the coins could make revoking
103// impossible.
104func Revoke(cur realm, target address) int64 {
105 who, _ := caller(cur)
106 refund, err := graph.Revoke(who, target)
107 if err != nil {
108 panic(err.Error())
109 }
110 chain.Emit("Revoke", "from", who.String(), "for", target.String(),
111 "refund", strconv.FormatInt(refund, 10))
112 return refund
113}
114
115// Withdraw pays the caller every bond their revoked vouches freed, and
116// returns the amount sent.
117//
118// The credit is zeroed before the coins leave, so a recipient that calls
119// straight back in finds nothing to take.
120func Withdraw(cur realm) int64 {
121 who, _ := caller(cur)
122 amount, err := graph.Withdraw(who)
123 if err != nil {
124 panic(err.Error())
125 }
126 bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
127 bnk.SendCoins(cur.Address(), who, chain.NewCoins(chain.NewCoin(denom, amount)))
128 chain.Emit("Withdraw", "to", who.String(), "amount", strconv.FormatInt(amount, 10))
129 return amount
130}
131
132// IsTrusted reports whether addr is vouched for by at least min distinct
133// addresses. This is the gate, and it is one line at the call site.
134//
135// A min below one is read as one: a gate that lets everybody through is a bug
136// in the caller rather than an answer this realm will agree to.
137//
138// It cannot tell you that the vouchers are distinct people. Two addresses
139// vouching for each other both reach a score of one, which [Mutual] exposes
140// and which is the reason to ask for more than one.
141func IsTrusted(addr address, min int) bool { return graph.IsTrusted(addr, min) }
142
143// ScoreOf is how many distinct addresses vouch for addr.
144func ScoreOf(addr address) int { return graph.ScoreOf(addr) }
145
146// BondedFor is the total ugnot locked on addr by everyone vouching for them.
147func BondedFor(addr address) int64 { return graph.BondedFor(addr) }
148
149// VouchedBy is every address that vouches for addr, sorted.
150func VouchedBy(addr address) []address { return graph.VouchedBy(addr) }
151
152// VouchesOf is every address addr vouches for, sorted.
153func VouchesOf(addr address) []address { return graph.VouchesOf(addr) }
154
155// Mutual reports whether a and b vouch for each other, which is the cheapest
156// sybil shape and therefore worth discounting.
157func Mutual(a, b address) bool { return graph.Mutual(a, b) }
158
159// ReasonFrom is what from wrote about target, or the empty string. It is raw
160// caller text: escape it before rendering it anywhere.
161func ReasonFrom(from, target address) string { return graph.ReasonFrom(from, target) }
162
163// BondFrom is what from locked on target, or zero.
164func BondFrom(from, target address) int64 { return graph.BondFrom(from, target) }
165
166// Count is how many vouches exist.
167func Count() int { return graph.Count() }
168
169// People is how many addresses have at least one vouch for them.
170func People() int { return graph.People() }
171
172// Owed is what addr can collect with [Withdraw].
173func Owed(addr address) int64 { return graph.Owed(addr) }
174
175// TotalBonded is everything locked on vouches that still stand.
176func TotalBonded() int64 { return graph.TotalBonded() }
177
178// TotalOwed is every revoked bond nobody has collected yet. This realm holds
179// TotalBonded plus TotalOwed on behalf of other people.
180func TotalOwed() int64 { return graph.TotalOwed() }
181
182// Badge is the one-line trust mark for addr, for a host realm that wants to
183// show what its gate just read. Like [IsTrusted] it is a borrowed read and
184// takes no realm token.
185func Badge(addr address) string {
186 return vo.Badge(realmPath, addr, graph.ScoreOf(addr), graph.BondedFor(addr))
187}
188
189// bondOf reads the coins attached to this call, and only when the chain
190// credited them to THIS realm.
191//
192// The envelope is the transaction's, not the frame's: a realm that was itself
193// paid and then calls here would report coins sitting at its own address, and
194// crediting them would let it bond money this realm never received. A realm
195// may vouch, with no bond; a realm may not vouch while holding somebody's
196// payment, and saying so loudly beats silently reading the bond as zero.
197//
198// It takes the answer rather than the frame because a realm argument in this
199// package must be named cur, which would make this a crossing function.
200func bondOf(userCall bool) int64 {
201 if userCall {
202 return envelope.Amount(denom)
203 }
204 if !envelope.IsEmpty() {
205 panic("a realm cannot forward a bond: those coins were credited to the realm the user paid, not to this one")
206 }
207 return 0
208}
209
210// caller is the address that called us, checked the one way that is safe,
211// plus whether it reached us as a direct user transaction.
212//
213// Both come from here so the realm reads cur.Previous() in exactly one place,
214// the one guarded by cur.IsCurrent().
215func caller(cur realm) (who address, userCall bool) {
216 if !cur.IsCurrent() {
217 panic("spoofed realm: cur is not the live crossing frame")
218 }
219 prev := cur.Previous()
220 return prev.Address(), prev.IsUserCall()
221}