provenance.gno
9.08 Kb · 267 lines
1package core
2
3import (
4 "chain"
5 "chain/runtime"
6
7 "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/safe/v1"
8 "gno.land/p/g1pq6pxhgxk2uw5shg5le6glfydpxcu74k0zh069/sinopia/types/v1"
9)
10
11// Commit–reveal v2 (design §4.2–4.3, closes RA H-3, M-5, M-6).
12//
13// The map is fixed by three inputs: the creator's secret sc, the beacon's
14// secret sb and the public mint-entropy accumulator acc. Neither committer
15// knows the other's secret while minting is open, so neither can time the
16// last mint for a good map. Order of reveals:
17//
18// 1. nothing is accepted before Closed (an early reveal informs the other
19// side);
20// 2. the creator may reveal until revealDeadline - BeaconGraceBlocks; the
21// beacon follows at any time before the deadline;
22// 3. inside the last BeaconGraceBlocks only the beacon may reveal. If the
23// creator stayed silent, the beacon's reveal finalizes at once
24// ("beacon-only"), so the creator never sees sb while it can still
25// choose (SA-M1; this also closes the mempool race of a creator
26// front-running the beacon's grace reveal);
27// 4. after revealDeadline anyone finalizes with whatever was revealed.
28//
29// What stays open, by design: the beacon, as last revealer after the
30// creator, can still choose between revealing and not (one bit). It runs
31// public code and the badge shows the outcome. The transfer lock
32// (tokens.gno) keeps anyone from trading on early knowledge until Final.
33//
34// A collection launched without the beacon (only possible while no beacon
35// is configured, SA-M2) gives the creator the whole choice of the map; it
36// can have no creator reserve and is never reported as a fair launch
37// (FairLaunch, Provenance "fair").
38type provenance struct {
39 creatorCommit, beaconCommit string
40 acc string
41 creatorSecret, beaconSecret string
42 creatorRevealed bool
43 beaconRevealed bool
44 salt, badge string
45 finalHeight int64
46}
47
48const (
49 roleCreator = "creator"
50 roleBeacon = "beacon"
51)
52
53// BeaconCommit is the beacon's commitment, before any token exists.
54func BeaconCommit(cur realm, cid, commitHex string) {
55 assertNoDeposit(cur)
56 c := mustCollection(cid)
57 if beacon == "" || caller(cur) != beacon {
58 panic("only the configured beacon can commit")
59 }
60 if c.state == types.StateDraft || c.prov == nil || !c.useBeacon {
61 panic("this collection is not waiting for a beacon")
62 }
63 if c.prov.beaconCommit != "" {
64 panic("the beacon already committed")
65 }
66 if c.minted() > 0 {
67 panic("too late: tokens were minted before the beacon committed")
68 }
69 commit := safe.AssertHex("commit", commitHex, 32)
70 if isBurned(c, commit) {
71 panic("this commitment's secret was already revealed for this collection")
72 }
73 c.prov.beaconCommit = commit
74 chain.Emit("BeaconCommitted", "cid", cid, "commit", commit)
75}
76
77func RevealCreatorSecret(cur realm, cid, secret string) {
78 assertNoDeposit(cur)
79 c := mustCollection(cid)
80 assertCreator(cur, c)
81 h := height()
82 assertRevealOpen(c, h)
83 if c.prov.creatorRevealed {
84 panic("the creator secret is already revealed")
85 }
86 if c.useBeacon {
87 // SA-M1: once the grace period starts only the beacon may reveal, so
88 // the creator can never reveal after seeing sb.
89 if c.prov.beaconRevealed {
90 panic("the beacon revealed first; the outcome is fixed")
91 }
92 if h >= revealDeadline(c)-BeaconGraceBlocks {
93 panic("the creator's reveal window ended at height " + itoa(revealDeadline(c)-BeaconGraceBlocks-1) +
94 "; only the beacon may reveal now")
95 }
96 }
97 assertSecret(c, roleCreator, secret, c.prov.creatorCommit)
98 c.prov.creatorSecret = secret
99 c.prov.creatorRevealed = true
100 chain.Emit("CreatorRevealed", "cid", cid, "secret", secret)
101 if revealsComplete(c) {
102 finalize(c, h)
103 }
104}
105
106func RevealBeaconSecret(cur realm, cid, secret string) {
107 assertNoDeposit(cur)
108 c := mustCollection(cid)
109 if beacon == "" || caller(cur) != beacon {
110 panic("only the configured beacon can reveal")
111 }
112 h := height()
113 assertRevealOpen(c, h)
114 if !c.useBeacon || c.prov.beaconCommit == "" {
115 panic("this collection has no beacon commitment")
116 }
117 if c.prov.beaconRevealed {
118 panic("the beacon secret is already revealed")
119 }
120 if !c.prov.creatorRevealed && h < revealDeadline(c)-BeaconGraceBlocks {
121 panic("the beacon reveals after the creator, or in the window's last " + itoa(BeaconGraceBlocks) + " blocks")
122 }
123 assertSecret(c, roleBeacon, secret, c.prov.beaconCommit)
124 c.prov.beaconSecret = secret
125 c.prov.beaconRevealed = true
126 chain.Emit("BeaconRevealed", "cid", cid, "secret", secret)
127 // After the creator: both secrets are in (2/2). First, in the grace
128 // period: the creator can no longer reveal, so the map is fixed now
129 // (beacon-only) — nobody is left holding a choice (SA-M1).
130 finalize(c, h)
131}
132
133// Finalize is permissionless and never paused (I-2): immediately once every
134// expected secret is revealed, otherwise after revealDeadline (fallback).
135func Finalize(cur realm, cid string) {
136 assertNoDeposit(cur)
137 c := mustCollection(cid)
138 h := height()
139 refresh(c, h)
140 if c.state != types.StateClosed {
141 panic("only a closed collection can be finalized (state " + c.state.String() + ")")
142 }
143 if !revealsComplete(c) && h <= revealDeadline(c) {
144 panic("the reveal window is open until height " + itoa(revealDeadline(c)))
145 }
146 // SA-I9: a sale that minted nothing stays Closed, so its creator can
147 // still abort and relaunch; nobody else can lock it into Final.
148 if c.minted() == 0 {
149 panic("nothing was minted; the creator may abort and relaunch instead")
150 }
151 finalize(c, h)
152}
153
154func assertRevealOpen(c *collection, h int64) {
155 refresh(c, h)
156 if c.state != types.StateClosed {
157 panic("secrets are accepted only after the sale closed (state " + c.state.String() + ")")
158 }
159 if h > revealDeadline(c) {
160 panic("the reveal window closed; call Finalize")
161 }
162}
163
164func assertSecret(c *collection, role, secret, commit string) {
165 if len(secret) < MinSecretLen || len(secret) > MaxSecretLen {
166 panic("secret must be " + itoa(MinSecretLen) + ".." + itoa(MaxSecretLen) + " bytes")
167 }
168 safe.AssertText("secret", secret, MaxSecretLen)
169 if safe.Commit(runtime.ChainID(), c.id, role, secret) != commit {
170 panic("secret does not match the " + role + " commitment")
171 }
172}
173
174func revealsComplete(c *collection) bool {
175 return c.prov.creatorRevealed && (!c.useBeacon || c.prov.beaconRevealed)
176}
177
178// finalize runs at most once per collection (I-10): every caller checks
179// state Closed, and this sets Final.
180func finalize(c *collection, h int64) {
181 p := c.prov
182 p.salt = safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc)
183 switch {
184 case p.creatorRevealed && p.beaconRevealed:
185 p.badge = BadgeBoth
186 case p.creatorRevealed:
187 p.badge = BadgeCreatorOnly
188 case p.beaconRevealed:
189 p.badge = BadgeBeaconOnly
190 default:
191 p.badge = BadgeFallback
192 }
193 p.finalHeight = h
194 c.state = types.StateFinal
195 c.st.final = true
196 chain.Emit("CollectionFinal", "cid", c.id, "salt", p.salt, "badge", p.badge)
197}
198
199// VerifyProvenance recomputes every commitment and the salt from public
200// data. True only for a Final collection whose record is consistent.
201func VerifyProvenance(cid string) bool {
202 c := mustCollection(cid)
203 if c.state != types.StateFinal {
204 return false
205 }
206 p := c.prov
207 chainID := runtime.ChainID()
208 if p.creatorRevealed && safe.Commit(chainID, c.id, roleCreator, p.creatorSecret) != p.creatorCommit {
209 return false
210 }
211 if p.beaconRevealed && safe.Commit(chainID, c.id, roleBeacon, p.beaconSecret) != p.beaconCommit {
212 return false
213 }
214 return p.salt == safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc)
215}
216
217// FairLaunch is true only for a Final, consistent collection whose map no
218// single party could choose: launched with the beacon and finalized with
219// both secrets or with the beacon's alone (after the creator stayed
220// silent). Creator-only collections (no beacon: the creator alone knew the
221// only secret and could steer the last mint, SA-M2) and fallbacks are not
222// fair launches, even when VerifyProvenance is true.
223func FairLaunch(cid string) bool {
224 c := mustCollection(cid)
225 if !VerifyProvenance(cid) || !c.useBeacon {
226 return false
227 }
228 return c.prov.badge == BadgeBoth || c.prov.badge == BadgeBeaconOnly
229}
230
231func isBurned(c *collection, commit string) bool {
232 for _, b := range c.burnedCommits {
233 if b == commit {
234 return true
235 }
236 }
237 return false
238}
239
240// Provenance is the whole record as JSON; secrets and salt stay "" until
241// revealed / final.
242func Provenance(cid string) string {
243 c := mustCollection(cid)
244 w := &safe.JSONWriter{}
245 w.BeginObject()
246 w.StrField("cid", c.id)
247 w.StrField("chainId", runtime.ChainID())
248 w.StrField("state", stateAt(c, height()).String())
249 w.IntField("minted", c.minted())
250 w.IntField("reserved", c.reserved)
251 if p := c.prov; p != nil {
252 w.BoolField("useBeacon", c.useBeacon)
253 w.StrField("creatorCommit", p.creatorCommit)
254 w.StrField("beaconCommit", p.beaconCommit)
255 w.StrField("entropy", p.acc)
256 w.StrField("creatorSecret", p.creatorSecret)
257 w.StrField("beaconSecret", p.beaconSecret)
258 w.StrField("salt", p.salt)
259 w.StrField("badge", p.badge)
260 w.IntField("revealDeadline", revealDeadline(c))
261 w.IntField("finalHeight", p.finalHeight)
262 w.BoolField("verified", VerifyProvenance(cid))
263 w.BoolField("fair", FairLaunch(cid))
264 }
265 w.EndObject()
266 return w.String()
267}