1// Package jury is a commit-reveal adversarial review protocol for disputes2// over agent work.3//4// The hard part of an agent economy is not paying agents — it is deciding5// what happens when an agent's output is disputed. This realm runs a small6// jury: a fixed panel of reviewers each *commit* to a hidden verdict, then7// *reveal* it. Commit-reveal means no juror can copy another's vote or be8// 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 claim11// id, an off-chain artifact hash — so this realm composes with the receipt12// and gnomem demos without depending on them.13//14// Bonding and slashing (a juror stakes coins, loses them for provably bad15// verdicts) are the natural economic layer on top; this demo shows the16// mechanism without the money.17package jury1819import (20 "chain"21 "chain/runtime"22 "strings"2324 "gno.land/p/moul/agents/commit/v0"25 "gno.land/p/nt/ufmt/v0"26)2728// Phase is a case's position in the commit-reveal lifecycle.29type Phase string3031const (32 PhaseCommit Phase = "commit"33 PhaseReveal Phase = "reveal"34 PhaseClosed Phase = "closed"35)3637// Juror is one panelist and their (eventually revealed) verdict.38type Juror struct {39 Addr address40 Commitment string // commit.Verdict(verdict, salt)41 Committed bool42 Revealed bool43 Verdict bool // meaningful once Revealed44 Height int6445}4647// Case is a single dispute under review by a fixed panel.48type Case struct {49 ID uint6450 Subject string // opaque reference to the disputed thing51 Opener address52 Phase Phase53 Jurors []*Juror54 OpenedAt int6455 Outcome string // "upheld" | "rejected" | "tie" once closed56 Yes int57 No int58}5960var cases []*Case // index 0 => ID 16162// OpenCase starts a dispute with a fixed panel of jurors. An odd panel size63// 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")6768 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] = true74 panel = append(panel, &Juror{Addr: j})75 }7677 id := uint64(len(cases)) + 178 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 id88}8990// Commit locks in a juror's hidden verdict. commitment must equal91// commit.Verdict(verdict, salt) — computed off-chain. When the last juror92// 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 = commitment100 j.Committed = true101 j.Height = runtime.ChainHeight()102103 if allCommitted(c) {104 c.Phase = PhaseReveal105 chain.Emit("CasePhase", "id", ufmt.Sprintf("%d", caseID), "phase", string(PhaseReveal))106 }107}108109// Reveal opens a juror's verdict. The (verdict, salt) must reproduce the110// commitment made earlier, or the reveal is rejected. When the last juror111// 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")118119 j.Revealed = true120 j.Verdict = verdict121 if verdict {122 c.Yes++123 } else {124 c.No++125 }126 chain.Emit("VerdictRevealed", "id", ufmt.Sprintf("%d", caseID), "verdict", boolStr(verdict))127128 if allRevealed(c) {129 closeCase(c)130 }131}132133// ---- read-only API ----134135// Get returns a copy of a case (jurors flattened separately via Jury).136func Get(caseID uint64) Case { return *mustCase(caseID) }137138// Outcome returns the recorded outcome, or "" if the case is still open.139func Outcome(caseID uint64) string { return mustCase(caseID).Outcome }140141// Count returns the number of cases.142func Count() int { return len(cases) }143144// ---- internal ----145146func 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 = PhaseClosed156 chain.Emit("CaseClosed", "id", ufmt.Sprintf("%d", c.ID), "outcome", c.Outcome)157}158159func allCommitted(c *Case) bool {160 for _, j := range c.Jurors {161 if !j.Committed {162 return false163 }164 }165 return true166}167168func allRevealed(c *Case) bool {169 for _, j := range c.Jurors {170 if !j.Revealed {171 return false172 }173 }174 return true175}176177func mustCase(id uint64) *Case {178 assert(id >= 1 && id <= uint64(len(cases)), "unknown case")179 return cases[id-1]180}181182func mustJuror(c *Case, addr address) *Juror {183 for _, j := range c.Jurors {184 if j.Addr == addr {185 return j186 }187 }188 panic("caller is not on this jury")189}190191func boolStr(b bool) string {192 if b {193 return "yes"194 }195 return "no"196}197198func assert(cond bool, msg string) {199 if !cond {200 panic(msg)201 }202}203204// 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}211212func renderIndex() string {213 var sb strings.Builder214 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.Outcome224 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}232233func renderCase(id uint64) string {234 c := mustCase(id)235 var sb strings.Builder236 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 closed246 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}259260func parseID(s string) uint64 {261 var n uint64262 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 n268}269270func short(a address) string {271 s := a.String()272 if len(s) <= 12 {273 return s274 }275 return s[:8] + "…" + s[len(s)-4:]276}277Commit(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, caseID uint64, commitment string)
Count() int
Get(caseID uint64) struct{ID uint64; Subject string; Opener .uverse.address; Phase gno.land/r/moul/agents/jury/v0.Phase; Jurors []*gno.land/r/moul/agents/jury/v0.Juror; OpenedAt int64; Outcome string; Yes int; No int}
OpenCase(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, subject string, jurors [].uverse.address) uint64
Outcome(caseID uint64) string
Render(path string) string
Reveal(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, caseID uint64, verdict bool, salt string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.