const MaxReasonLen
MaxReasonLen is the longest reason accepted, in bytes. Long enough to say how you know somebody, short enough that one call cannot lock an unbounded storage deposit the realm's deployer is paying for.
Package vouch is the engine behind a web of trust: one address says it stands behind another, in writing, optionally ...
gno.land/p/moul/x/social/vouch/v0The engine behind a web of trust: NewGraph, Record, Revoke,
Withdraw, ScoreOf, IsTrusted, VouchedBy, VouchesOf, Mutual,
Leaderboard.
1g := vouch.NewGraph()
2g.Record(alice, bob, "worked with them for a year", 1_000_000, height)
3g.IsTrusted(bob, 2) // the one call another realm makes
4g.Revoke(alice, bob) // credits the bond back, withdrawn separately
It is a sybil gate, and it exists because the apps around it do not have one. A realm that mints a point per distinct replier is farmed by two addresses replying to each other; a realm that counts one vote per address is farmed by holding a hundred. Neither can fix that alone, because neither knows anything about the people behind the addresses. This package knows one thing: who was willing to say, on chain and under their own name, that an address is somebody they stand behind.
A score counts people, not transactions. A vouch is directed and at most one
exists per ordered pair, so a second Record from the same address to the same
target updates the reason and adds to the bond rather than counting twice. An
address cannot vouch for itself. A pair vouching for each other both reach a
score of one, which is exactly why a real gate asks for two.
The engine counts the bond, it never moves it. The amount arrives as an
argument, Revoke moves it into a withdrawal ledger, and the realm holding the
coins transfers only after Withdraw has zeroed the credit. That ordering is
the pull-payment pattern and it is not optional.
There is no slashing in v0, and the missing half is not the accounting. Slashing needs an arbiter: somebody has to decide that a vouch was a lie. A DAO vote is a popularity contest against whoever is unpopular this month, a challenge market pays whoever is loudest, an oracle is one key that can confiscate anybody's money. Shipping any of them by default is shipping the wrong one. The bond is still worth having: an illiquid deposit is a cost a hundred throwaway addresses cannot all pay at once.
Ordering is a total order on the address everywhere it matters, so two identical
Render calls produce identical bytes: the sets are p/moul/addrset, the
leaderboard breaks ties on the address, and nothing is built by ranging a map.
A reason is free text, bounded to one line of MaxReasonLen bytes with control
characters refused, and still has to be escaped where it is shown.
Live realm: r/moul/x/social/vouch,
which also carries the reasoning for why this one issues no token.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Package vouch is the engine behind a web of trust: one address says it stands behind another, in writing, optionally with its own money locked against the claim.
It is a sybil gate, and it exists because the apps around it do not have one. A realm that mints a point per distinct replier is farmed by two addresses replying to each other; a realm that counts one vote per address is farmed by holding a hundred addresses. Neither can fix that alone, because neither knows anything about the people behind the addresses.
This package knows one thing: who was willing to say, on chain and under their own name, that an address is a person they stand behind. A realm asks Graph.IsTrusted and gates on the answer. That is the whole product, and every other read here exists to make that one legible.
1Graph every vouch, in both directions, plus the refund ledger
2Vouch a directed statement: from, for, reason, bond, heights
A vouch is directed and at most one exists per ordered pair. A second Graph.Record from the same address to the same target UPDATES the reason and ADDS to the bond rather than counting twice, which is what makes the score a count of people instead of a count of transactions. An address cannot vouch for itself.
A vouch may lock coins. The engine only counts them: it takes the amount as an argument, tracks who posted how much on whom, and on Graph.Revoke moves that amount into a withdrawal ledger the voucher pulls from. It moves nothing itself. The realm holding the coins performs the transfer AFTER Graph.Withdraw has zeroed the credit, which is the ordering the pull-payment pattern demands.
A bond here is value at risk only in the sense that it is illiquid: it can be withdrawn by revoking, and nothing can take it away. That is deliberate for a v0, and the missing half is not the accounting.
Slashing needs an arbiter: somebody has to decide that a vouch was a lie. Every candidate is a design question with teeth. A DAO vote is a popularity contest against whoever is unpopular this month. A challenge market pays whoever is loudest and turns the graph into a griefing surface. An oracle is one key that can confiscate anyone's money. Shipping any of them by default would be shipping the wrong one, so this version ships the part that is uncontroversial: who said what, who put money behind it, and the gate that reads it.
A reason is free text. ValidReason bounds it to MaxReasonLen bytes on one line and refuses control characters, but everything inside that is allowed and must be escaped where it is shown: ui.Inline in prose, ui.Cell in a table cell.
MaxReasonLen is the longest reason accepted, in bytes. Long enough to say how you know somebody, short enough that one call cannot lock an unbounded storage deposit the realm's deployer is paying for.
1var (
2 ErrSelfVouch = errors.New("vouch: an address cannot vouch for itself")
3 ErrBadReason = errors.New("vouch: reason is empty, too long, multi-line, or has control characters")
4 ErrBadBond = errors.New("vouch: a bond cannot be negative")
5 ErrNoVouch = errors.New("vouch: no such vouch")
6 ErrNothingOwed = errors.New("vouch: nothing to withdraw")
7 ErrOverflow = errors.New("vouch: bond would overflow")
8)The errors a caller can get back. A p/ returns them; the realm decides to abort.
AddrURL is the gnoweb path of one address's page on the hosting realm.
Badge is the one-line trust mark, for a realm that wants to show what the gate it just called was reading.
It takes the numbers rather than the graph because the realm holding the graph is the only one that can read it: everybody else has the two integers from a cross-realm call and needs nothing more to render them.
RealmURL is the gnoweb path of a realm given as a package path.
The chain domain is the first element of a package path and a gnoweb path is the rest of it, so this is a prefix strip and not a hostname this package has to know.
ValidReason reports whether reason can be stored: non-empty after trimming, within MaxReasonLen, on one line, and free of control characters.
One line is a deliberate bound rather than a rendering workaround. A reason is shown in a table cell beside the address it is about, and a writer who needs a second paragraph is writing something other than a reason.
Control characters are refused rather than stripped because the text is shown back to whoever wrote it, and silently rewriting what somebody said is worse than telling them it was refused. Everything else is allowed and escaped at render time, since a validator and an escaper protect against different mistakes.
NewGraph returns an empty graph.
1type Graph struct {
2 edges map[string]*Vouch // "from|for" -> the vouch
3 inbound map[string]*addrset.Set // target -> who vouches for them
4 outbound map[string]*addrset.Set // voucher -> who they vouch for
5 bonded map[string]int64 // target -> total bonded on them
6
7 // people is every address with at least one inbound vouch, sorted, so
8 // a listing never has to iterate a map to build rendered output.
9 people addrset.Set
10
11 refunds *pullpayment.Ledger
12 count int
13 totalBonded int64
14}Graph is the whole web of trust: every vouch, both directions of every edge, what is bonded on whom, and what revoking owes back to whom.
BondFrom is what from locked on target, or zero.
BondedFor is the total locked on addr by everyone vouching for them.
Count is how many vouches exist, across everybody.
Get returns one vouch.
IsTrusted reports whether addr is vouched for by at least min distinct addresses. It is the gate another realm calls, and the reason this package exists.
A min below one is raised to one. A gate that lets everybody through is a bug at the call site rather than an answer worth returning, and silently agreeing with it is how a sybil check ships disabled.
What it cannot tell you is whether those vouchers are distinct PEOPLE. Two addresses vouching for each other both reach a score of one for the price of two transactions, which is why Graph.Mutual is exported and why a gate that matters should ask for more than one.
Leaderboard is the most vouched for addresses, highest score first, ties broken by address so the order is total and a Render never reshuffles.
limit at or below zero returns nothing.
Mutual reports whether a and b vouch for each other.
A mutual pair is the cheapest sybil shape there is, so this is here to be discounted by a caller that cares, not as a badge.
Owed is what revoking has credited to addr and nobody has withdrawn yet.
People is how many addresses have at least one vouch for them.
ReasonFrom is what from wrote about target, or the empty string when there is no such vouch. It is raw caller text: escape it where it is shown.
1func (g *Graph) Record(from, target address, reason string, bond, at int64) (updated bool, err error)Record writes from's vouch for target and reports whether it replaced one that already existed.
A repeated vouch is an update and not a second voice: the reason is replaced, the bond is added to the one already posted, and the score does not move. bond may be zero, which is the ordinary case.
Revoke removes from's vouch for target and credits the bond back to from, returning the amount credited.
The coins are not sent here and this package never holds any: the credit waits in the refund ledger until from calls Graph.Withdraw.
ScoreOf is how many distinct addresses vouch for addr.
It counts people and not statements: restating a vouch does not raise it, and revoking lowers it.
TotalBonded is everything locked on every vouch that still stands.
TotalOwed is every unwithdrawn refund. The realm must hold at least TotalOwed plus Graph.TotalBonded to be solvent.
VouchedBy is every address that vouches for addr, sorted.
Sorted and not chronological: the set is the storage, the order is a total order on the address, and two identical calls therefore render identically.
VouchesOf is every address addr vouches for, sorted. It is the other direction of Graph.VouchedBy.
Withdraw zeroes what the graph owes who and returns it, so the realm can send exactly that much.
The credit is gone from the ledger before this returns, which is what makes a reentrant call find nothing: the realm transfers after, never before.
Ranked is one row of Graph.Leaderboard.
1type Vouch struct {
2 From address
3 For address
4 Reason string
5
6 // Bond is the amount locked on this vouch, in the realm's denom. It is
7 // the sum of every bond sent with this pair, since a repeated vouch
8 // adds to it rather than replacing it.
9 Bond int64
10
11 // At is the height the vouch was first made, UpdatedAt the height it
12 // last changed. They are equal until the voucher restates it.
13 At int64
14 UpdatedAt int64
15}Vouch is one directed statement of trust.