provenance.gno
6.88 Kb · 217 lines
1package core
2
3import (
4 "chain"
5 "chain/runtime"
6
7 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/safe/v1"
8 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/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 reveals first; the beacon follows, or reveals first
21// inside the last BeaconGraceBlocks of the window;
22// 3. after revealDeadline anyone finalizes with whatever was revealed.
23//
24// What stays open, by design: the last revealer can still choose between
25// revealing and not (one bit). The beacon is last, runs public code, and
26// the badge shows the outcome. The transfer lock (tokens.gno) keeps anyone
27// from trading on early knowledge until Final.
28type provenance struct {
29 creatorCommit, beaconCommit string
30 acc string
31 creatorSecret, beaconSecret string
32 creatorRevealed bool
33 beaconRevealed bool
34 salt, badge string
35 finalHeight int64
36}
37
38const (
39 roleCreator = "creator"
40 roleBeacon = "beacon"
41)
42
43// BeaconCommit is the beacon's commitment, before any token exists.
44func BeaconCommit(cur realm, cid, commitHex string) {
45 assertNoDeposit(cur)
46 c := mustCollection(cid)
47 if beacon == "" || caller(cur) != beacon {
48 panic("only the configured beacon can commit")
49 }
50 if c.state == types.StateDraft || c.prov == nil || !c.useBeacon {
51 panic("this collection is not waiting for a beacon")
52 }
53 if c.prov.beaconCommit != "" {
54 panic("the beacon already committed")
55 }
56 if c.minted() > 0 {
57 panic("too late: tokens were minted before the beacon committed")
58 }
59 commit := safe.AssertHex("commit", commitHex, 32)
60 if usedCommits.Has(commit) {
61 panic("this commitment was already used")
62 }
63 usedCommits.Set(commit, cid)
64 c.prov.beaconCommit = commit
65 chain.Emit("BeaconCommitted", "cid", cid, "commit", commit)
66}
67
68func RevealCreatorSecret(cur realm, cid, secret string) {
69 assertNoDeposit(cur)
70 c := mustCollection(cid)
71 assertCreator(cur, c)
72 h := height()
73 assertRevealOpen(c, h)
74 if c.prov.creatorRevealed {
75 panic("the creator secret is already revealed")
76 }
77 assertSecret(c, roleCreator, secret, c.prov.creatorCommit)
78 c.prov.creatorSecret = secret
79 c.prov.creatorRevealed = true
80 chain.Emit("CreatorRevealed", "cid", cid, "secret", secret)
81 if revealsComplete(c) {
82 finalize(c, h)
83 }
84}
85
86func RevealBeaconSecret(cur realm, cid, secret string) {
87 assertNoDeposit(cur)
88 c := mustCollection(cid)
89 if beacon == "" || caller(cur) != beacon {
90 panic("only the configured beacon can reveal")
91 }
92 h := height()
93 assertRevealOpen(c, h)
94 if !c.useBeacon || c.prov.beaconCommit == "" {
95 panic("this collection has no beacon commitment")
96 }
97 if c.prov.beaconRevealed {
98 panic("the beacon secret is already revealed")
99 }
100 if !c.prov.creatorRevealed && h < revealDeadline(c)-BeaconGraceBlocks {
101 panic("the beacon reveals after the creator, or in the window's last " + itoa(BeaconGraceBlocks) + " blocks")
102 }
103 assertSecret(c, roleBeacon, secret, c.prov.beaconCommit)
104 c.prov.beaconSecret = secret
105 c.prov.beaconRevealed = true
106 chain.Emit("BeaconRevealed", "cid", cid, "secret", secret)
107 if revealsComplete(c) {
108 finalize(c, h)
109 }
110}
111
112// Finalize is permissionless and never paused (I-2): immediately once every
113// expected secret is revealed, otherwise after revealDeadline (fallback).
114func Finalize(cur realm, cid string) {
115 assertNoDeposit(cur)
116 c := mustCollection(cid)
117 h := height()
118 refresh(c, h)
119 if c.state != types.StateClosed {
120 panic("only a closed collection can be finalized (state " + c.state.String() + ")")
121 }
122 if !revealsComplete(c) && h <= revealDeadline(c) {
123 panic("the reveal window is open until height " + itoa(revealDeadline(c)))
124 }
125 finalize(c, h)
126}
127
128func assertRevealOpen(c *collection, h int64) {
129 refresh(c, h)
130 if c.state != types.StateClosed {
131 panic("secrets are accepted only after the sale closed (state " + c.state.String() + ")")
132 }
133 if h > revealDeadline(c) {
134 panic("the reveal window closed; call Finalize")
135 }
136}
137
138func assertSecret(c *collection, role, secret, commit string) {
139 if len(secret) < MinSecretLen || len(secret) > MaxSecretLen {
140 panic("secret must be " + itoa(MinSecretLen) + ".." + itoa(MaxSecretLen) + " bytes")
141 }
142 safe.AssertText("secret", secret, MaxSecretLen)
143 if safe.Commit(runtime.ChainID(), c.id, role, secret) != commit {
144 panic("secret does not match the " + role + " commitment")
145 }
146}
147
148func revealsComplete(c *collection) bool {
149 return c.prov.creatorRevealed && (!c.useBeacon || c.prov.beaconRevealed)
150}
151
152// finalize runs at most once per collection (I-10): every caller checks
153// state Closed, and this sets Final.
154func finalize(c *collection, h int64) {
155 p := c.prov
156 p.salt = safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc)
157 switch {
158 case p.creatorRevealed && p.beaconRevealed:
159 p.badge = BadgeBoth
160 case p.creatorRevealed:
161 p.badge = BadgeCreatorOnly
162 case p.beaconRevealed:
163 p.badge = BadgeBeaconOnly
164 default:
165 p.badge = BadgeFallback
166 }
167 p.finalHeight = h
168 c.state = types.StateFinal
169 c.st.final = true
170 chain.Emit("CollectionFinal", "cid", c.id, "salt", p.salt, "badge", p.badge)
171}
172
173// VerifyProvenance recomputes every commitment and the salt from public
174// data. True only for a Final collection whose record is consistent.
175func VerifyProvenance(cid string) bool {
176 c := mustCollection(cid)
177 if c.state != types.StateFinal {
178 return false
179 }
180 p := c.prov
181 chainID := runtime.ChainID()
182 if p.creatorRevealed && safe.Commit(chainID, c.id, roleCreator, p.creatorSecret) != p.creatorCommit {
183 return false
184 }
185 if p.beaconRevealed && safe.Commit(chainID, c.id, roleBeacon, p.beaconSecret) != p.beaconCommit {
186 return false
187 }
188 return p.salt == safe.Salt(c.id, p.creatorSecret, p.beaconSecret, p.acc)
189}
190
191// Provenance is the whole record as JSON; secrets and salt stay "" until
192// revealed / final.
193func Provenance(cid string) string {
194 c := mustCollection(cid)
195 w := &safe.JSONWriter{}
196 w.BeginObject()
197 w.StrField("cid", c.id)
198 w.StrField("chainId", runtime.ChainID())
199 w.StrField("state", stateAt(c, height()).String())
200 w.IntField("minted", c.minted())
201 w.IntField("reserved", c.reserved)
202 if p := c.prov; p != nil {
203 w.BoolField("useBeacon", c.useBeacon)
204 w.StrField("creatorCommit", p.creatorCommit)
205 w.StrField("beaconCommit", p.beaconCommit)
206 w.StrField("entropy", p.acc)
207 w.StrField("creatorSecret", p.creatorSecret)
208 w.StrField("beaconSecret", p.beaconSecret)
209 w.StrField("salt", p.salt)
210 w.StrField("badge", p.badge)
211 w.IntField("revealDeadline", revealDeadline(c))
212 w.IntField("finalHeight", p.finalHeight)
213 w.BoolField("verified", VerifyProvenance(cid))
214 }
215 w.EndObject()
216 return w.String()
217}