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

jury.gno

7.43 Kb · 276 lines
  1// Package jury is a commit-reveal adversarial review protocol for disputes
  2// over agent work.
  3//
  4// The hard part of an agent economy is not paying agents — it is deciding
  5// what happens when an agent's output is disputed. This realm runs a small
  6// jury: a fixed panel of reviewers each *commit* to a hidden verdict, then
  7// *reveal* it. Commit-reveal means no juror can copy another's vote or be
  8// swayed after seeing the tally; every verdict is locked in blind.
  9//
 10// The subject of a case is an opaque string — a receipt sequence, a claim
 11// id, an off-chain artifact hash — so this realm composes with the receipt
 12// and gnomem demos without depending on them.
 13//
 14// Bonding and slashing (a juror stakes coins, loses them for provably bad
 15// verdicts) are the natural economic layer on top; this demo shows the
 16// mechanism without the money.
 17package jury
 18
 19import (
 20	"chain"
 21	"chain/runtime"
 22	"strings"
 23
 24	"gno.land/p/moul/agents/commit/v0"
 25	"gno.land/p/nt/ufmt/v0"
 26)
 27
 28// Phase is a case's position in the commit-reveal lifecycle.
 29type Phase string
 30
 31const (
 32	PhaseCommit Phase = "commit"
 33	PhaseReveal Phase = "reveal"
 34	PhaseClosed Phase = "closed"
 35)
 36
 37// Juror is one panelist and their (eventually revealed) verdict.
 38type Juror struct {
 39	Addr       address
 40	Commitment string // commit.Verdict(verdict, salt)
 41	Committed  bool
 42	Revealed   bool
 43	Verdict    bool // meaningful once Revealed
 44	Height     int64
 45}
 46
 47// Case is a single dispute under review by a fixed panel.
 48type Case struct {
 49	ID       uint64
 50	Subject  string // opaque reference to the disputed thing
 51	Opener   address
 52	Phase    Phase
 53	Jurors   []*Juror
 54	OpenedAt int64
 55	Outcome  string // "upheld" | "rejected" | "tie" once closed
 56	Yes      int
 57	No       int
 58}
 59
 60var cases []*Case // index 0 => ID 1
 61
 62// OpenCase starts a dispute with a fixed panel of jurors. An odd panel size
 63// avoids ties; duplicates are rejected.
 64func OpenCase(cur realm, subject string, jurors []address) uint64 {
 65	assert(subject != "", "empty subject")
 66	assert(len(jurors) >= 1, "need at least one juror")
 67
 68	seen := make(map[string]bool)
 69	panel := make([]*Juror, 0, len(jurors))
 70	for _, j := range jurors {
 71		key := j.String()
 72		assert(!seen[key], "duplicate juror: "+key)
 73		seen[key] = true
 74		panel = append(panel, &Juror{Addr: j})
 75	}
 76
 77	id := uint64(len(cases)) + 1
 78	cases = append(cases, &Case{
 79		ID:       id,
 80		Subject:  subject,
 81		Opener:   cur.Previous().Address(),
 82		Phase:    PhaseCommit,
 83		Jurors:   panel,
 84		OpenedAt: runtime.ChainHeight(),
 85	})
 86	chain.Emit("CaseOpened", "id", ufmt.Sprintf("%d", id), "jurors", ufmt.Sprintf("%d", len(panel)))
 87	return id
 88}
 89
 90// Commit locks in a juror's hidden verdict. commitment must equal
 91// commit.Verdict(verdict, salt) — computed off-chain. When the last juror
 92// commits, the case advances to the reveal phase automatically.
 93func Commit(cur realm, caseID uint64, commitment string) {
 94	c := mustCase(caseID)
 95	assert(c.Phase == PhaseCommit, "not in commit phase")
 96	assert(commitment != "", "empty commitment")
 97	j := mustJuror(c, cur.Previous().Address())
 98	assert(!j.Committed, "already committed")
 99	j.Commitment = commitment
100	j.Committed = true
101	j.Height = runtime.ChainHeight()
102
103	if allCommitted(c) {
104		c.Phase = PhaseReveal
105		chain.Emit("CasePhase", "id", ufmt.Sprintf("%d", caseID), "phase", string(PhaseReveal))
106	}
107}
108
109// Reveal opens a juror's verdict. The (verdict, salt) must reproduce the
110// commitment made earlier, or the reveal is rejected. When the last juror
111// reveals, the case closes and the majority outcome is recorded.
112func Reveal(cur realm, caseID uint64, verdict bool, salt string) {
113	c := mustCase(caseID)
114	assert(c.Phase == PhaseReveal, "not in reveal phase")
115	j := mustJuror(c, cur.Previous().Address())
116	assert(j.Committed && !j.Revealed, "nothing to reveal")
117	assert(commit.Verdict(verdict, salt) == j.Commitment, "reveal does not match commitment")
118
119	j.Revealed = true
120	j.Verdict = verdict
121	if verdict {
122		c.Yes++
123	} else {
124		c.No++
125	}
126	chain.Emit("VerdictRevealed", "id", ufmt.Sprintf("%d", caseID), "verdict", boolStr(verdict))
127
128	if allRevealed(c) {
129		closeCase(c)
130	}
131}
132
133// ---- read-only API ----
134
135// Get returns a copy of a case (jurors flattened separately via Jury).
136func Get(caseID uint64) Case { return *mustCase(caseID) }
137
138// Outcome returns the recorded outcome, or "" if the case is still open.
139func Outcome(caseID uint64) string { return mustCase(caseID).Outcome }
140
141// Count returns the number of cases.
142func Count() int { return len(cases) }
143
144// ---- internal ----
145
146func closeCase(c *Case) {
147	switch {
148	case c.Yes > c.No:
149		c.Outcome = "upheld"
150	case c.No > c.Yes:
151		c.Outcome = "rejected"
152	default:
153		c.Outcome = "tie"
154	}
155	c.Phase = PhaseClosed
156	chain.Emit("CaseClosed", "id", ufmt.Sprintf("%d", c.ID), "outcome", c.Outcome)
157}
158
159func allCommitted(c *Case) bool {
160	for _, j := range c.Jurors {
161		if !j.Committed {
162			return false
163		}
164	}
165	return true
166}
167
168func allRevealed(c *Case) bool {
169	for _, j := range c.Jurors {
170		if !j.Revealed {
171			return false
172		}
173	}
174	return true
175}
176
177func mustCase(id uint64) *Case {
178	assert(id >= 1 && id <= uint64(len(cases)), "unknown case")
179	return cases[id-1]
180}
181
182func mustJuror(c *Case, addr address) *Juror {
183	for _, j := range c.Jurors {
184		if j.Addr == addr {
185			return j
186		}
187	}
188	panic("caller is not on this jury")
189}
190
191func boolStr(b bool) string {
192	if b {
193		return "yes"
194	}
195	return "no"
196}
197
198func assert(cond bool, msg string) {
199	if !cond {
200		panic(msg)
201	}
202}
203
204// Render shows all cases, or one case's panel + tally at :<id>.
205func Render(path string) string {
206	if path == "" {
207		return renderIndex()
208	}
209	return renderCase(parseID(path))
210}
211
212func renderIndex() string {
213	var sb strings.Builder
214	sb.WriteString("# Agent Jury\n\n")
215	sb.WriteString("_Who checks the agents?_ Blind commit-reveal review by a fixed panel.\n\n")
216	if len(cases) == 0 {
217		sb.WriteString("_No cases yet._\n")
218		return sb.String()
219	}
220	sb.WriteString("| # | Subject | Phase | ✅ | ❌ | Outcome |\n")
221	sb.WriteString("|---|---|---|---|---|---|\n")
222	for _, c := range cases {
223		out := c.Outcome
224		if out == "" {
225			out = "—"
226		}
227		sb.WriteString(ufmt.Sprintf("| [%d](/r/moul/agents/jury/v0:%d) | %s | %s | %d | %d | %s |\n",
228			c.ID, c.ID, c.Subject, string(c.Phase), c.Yes, c.No, out))
229	}
230	return sb.String()
231}
232
233func renderCase(id uint64) string {
234	c := mustCase(id)
235	var sb strings.Builder
236	sb.WriteString(ufmt.Sprintf("# Case #%d\n\n", c.ID))
237	sb.WriteString(ufmt.Sprintf("- **Subject:** %s\n", c.Subject))
238	sb.WriteString(ufmt.Sprintf("- **Phase:** %s\n", string(c.Phase)))
239	sb.WriteString(ufmt.Sprintf("- **Opened by:** %s at height %d\n", short(c.Opener), c.OpenedAt))
240	if c.Outcome != "" {
241		sb.WriteString(ufmt.Sprintf("- **Outcome:** **%s** (%d ✅ / %d ❌)\n", c.Outcome, c.Yes, c.No))
242	}
243	sb.WriteString("\n## Panel\n\n")
244	sb.WriteString("| Juror | Committed | Revealed | Verdict | Dissent |\n|---|---|---|---|---|\n")
245	majority := c.Yes >= c.No // for dissent marking once closed
246	for _, j := range c.Jurors {
247		verdict, dissent := "—", ""
248		if j.Revealed {
249			verdict = boolStr(j.Verdict)
250			if c.Phase == PhaseClosed && c.Outcome != "tie" && j.Verdict != majority {
251				dissent = "⚠"
252			}
253		}
254		sb.WriteString(ufmt.Sprintf("| %s | %t | %t | %s | %s |\n",
255			short(j.Addr), j.Committed, j.Revealed, verdict, dissent))
256	}
257	return sb.String()
258}
259
260func parseID(s string) uint64 {
261	var n uint64
262	for i := 0; i < len(s); i++ {
263		ch := s[i]
264		assert(ch >= '0' && ch <= '9', "invalid case path: "+s)
265		n = n*10 + uint64(ch-'0')
266	}
267	return n
268}
269
270func short(a address) string {
271	s := a.String()
272	if len(s) <= 12 {
273		return s
274	}
275	return s[:8] + "…" + s[len(s)-4:]
276}