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

jury.gno

7.50 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/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/commit"
 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/g1manfred47kzduec920z88wfr64ylksmdcedlf5/agents/jury:%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}