Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package vouch is the engine behind a web of trust: one address says it stands behind another, in writing, optionally ...

Readme View source

gno.land/p/moul/x/social/vouch/v0

The 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:

gno.land/p/moul/x/social/vouch/v0 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.

Overview

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.

What it is for

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.

The model

Example
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.

The bond

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.

No slashing, and why that is the hard part

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.

Reasons are attacker-controlled markdown

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.

Constants 1

const MaxReasonLen

1const MaxReasonLen = 200
source

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.

Variables 1

var ErrSelfVouch, ErrBadReason, ErrBadBond, ErrNoVouch, ErrNothingOwed, ErrOverflow

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)
source

The errors a caller can get back. A p/ returns them; the realm decides to abort.

Functions 5

func AddrURL

1func AddrURL(realmPath string, addr address) string
source

AddrURL is the gnoweb path of one address's page on the hosting realm.

func Badge

1func Badge(realmPath string, addr address, score int, bonded int64) string
source

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.

func RealmURL

1func RealmURL(realmPath string) string
source

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.

func ValidReason

1func ValidReason(reason string) bool
source

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.

func NewGraph

1func NewGraph() *Graph
source

NewGraph returns an empty graph.

Types 3

type Graph

struct
 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}
source

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.

Methods on Graph

func BondFrom

method on Graph
1func (g *Graph) BondFrom(from, target address) int64
source

BondFrom is what from locked on target, or zero.

func BondedFor

method on Graph
1func (g *Graph) BondedFor(addr address) int64
source

BondedFor is the total locked on addr by everyone vouching for them.

func Count

method on Graph
1func (g *Graph) Count() int
source

Count is how many vouches exist, across everybody.

func Get

method on Graph
1func (g *Graph) Get(from, target address) (*Vouch, bool)
source

Get returns one vouch.

func IsTrusted

method on Graph
1func (g *Graph) IsTrusted(addr address, min int) bool
source

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.

func Leaderboard

method on Graph
1func (g *Graph) Leaderboard(limit int) []Ranked
source

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.

func Mutual

method on Graph
1func (g *Graph) Mutual(a, b address) bool
source

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.

func Owed

method on Graph
1func (g *Graph) Owed(addr address) int64
source

Owed is what revoking has credited to addr and nobody has withdrawn yet.

func People

method on Graph
1func (g *Graph) People() int
source

People is how many addresses have at least one vouch for them.

func ReasonFrom

method on Graph
1func (g *Graph) ReasonFrom(from, target address) string
source

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.

func Record

method on Graph
1func (g *Graph) Record(from, target address, reason string, bond, at int64) (updated bool, err error)
source

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.

func Revoke

method on Graph
1func (g *Graph) Revoke(from, target address) (refund int64, err error)
source

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.

func ScoreOf

method on Graph
1func (g *Graph) ScoreOf(addr address) int
source

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.

func TotalBonded

method on Graph
1func (g *Graph) TotalBonded() int64
source

TotalBonded is everything locked on every vouch that still stands.

func TotalOwed

method on Graph
1func (g *Graph) TotalOwed() int64
source

TotalOwed is every unwithdrawn refund. The realm must hold at least TotalOwed plus Graph.TotalBonded to be solvent.

func VouchedBy

method on Graph
1func (g *Graph) VouchedBy(addr address) []address
source

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.

func VouchesOf

method on Graph
1func (g *Graph) VouchesOf(addr address) []address
source

VouchesOf is every address addr vouches for, sorted. It is the other direction of Graph.VouchedBy.

func Withdraw

method on Graph
1func (g *Graph) Withdraw(who address) (int64, error)
source

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.

type Vouch

struct
 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}
source

Vouch is one directed statement of trust.

Imports 8

Source Files 4