reveal.gno
7.15 Kb · 252 lines
1package guardians_pilot_v1
2
3import (
4 "chain"
5 "chain/runtime"
6 "crypto/sha256"
7 "encoding/hex"
8 "strconv"
9 "strings"
10 "time"
11
12 "gno.land/p/nt/grc721/v0"
13 "gno.land/p/nt/ufmt/v0"
14)
15
16const (
17 revealStateVeiled = "VEILED"
18 revealStateCommitted = "REVEAL_COMMITTED"
19 revealStateRevealing = "REVEALING"
20 revealStateRevealed = "REVEALED"
21 revealStateUnveiledFrozen = "UNVEILED_FROZEN"
22 maxRevealBatch = 20
23)
24
25var (
26 revealState = revealStateVeiled
27 provenanceRoot string
28 revealCommitment string
29 finalBaseURI string
30 revealDeadline int64
31 revealSeedHash string
32 permutationA int64
33 permutationB int64
34 revealTarget int64
35 revealProcessed int64
36)
37
38func RevealState() string { return revealState }
39
40func ProvenanceRoot() string { return provenanceRoot }
41
42func RevealCommitment() string { return revealCommitment }
43
44func FinalBaseURI() string { return finalBaseURI }
45
46func RevealDeadline() int64 { return revealDeadline }
47
48func RevealSeedHash() string { return revealSeedHash }
49
50func RevealTarget() int64 { return revealTarget }
51
52func RevealProcessed() int64 { return revealProcessed }
53
54func RevealOverdue() bool {
55 return revealState == revealStateCommitted && time.Now().Unix() > revealDeadline
56}
57
58// ConfigureReveal permanently commits provenance, secret hash and final URI
59// layout before the first token can be minted.
60func ConfigureReveal(cur realm, provenance, commitment, baseURI string, deadline int64) {
61 assertAdmin(cur.Previous().Address())
62 if revealState != revealStateVeiled {
63 panic("guardians: reveal configuration already committed")
64 }
65 if token.TotalSupply() != 0 || !salePaused {
66 panic("guardians: configure reveal before minting and sale activation")
67 }
68 normalizedProvenance := normalizeHash(provenance)
69 if normalizedProvenance == "" {
70 panic("guardians: invalid provenance root")
71 }
72 normalizedCommitment := normalizeHash(commitment)
73 if normalizedCommitment == "" {
74 panic("guardians: invalid reveal commitment")
75 }
76 if !validFinalBaseURI(baseURI) {
77 panic("guardians: final base uri must be ipfs/https and end with slash")
78 }
79 if deadline <= time.Now().Unix() {
80 panic("guardians: reveal deadline must be in the future")
81 }
82
83 provenanceRoot = normalizedProvenance
84 revealCommitment = normalizedCommitment
85 finalBaseURI = baseURI
86 revealDeadline = deadline
87 revealState = revealStateCommitted
88
89 chain.Emit(
90 "RevealConfigured",
91 "provenance", provenanceRoot,
92 "commitment", revealCommitment,
93 "deadline", ufmt.Sprintf("%d", revealDeadline),
94 )
95}
96
97// ComputeRevealCommitment is deterministic but production secrets should be
98// committed with the offline helper, never exposed through a public RPC query.
99func ComputeRevealCommitment(secret string) string {
100 payload := runtime.ChainID() + "|" + CollectionID() + "|REVEAL|" + secret
101 return hashBytes([]byte(payload))
102}
103
104// The pilot owner reveals manually after the full 24-hour sale window.
105// Selling out early never starts reveal, and no timer invokes this function.
106func Reveal(cur realm, secret string) {
107 assertAdmin(cur.Previous().Address())
108 if revealState != revealStateCommitted {
109 panic("guardians: reveal is not committed")
110 }
111 if len(secret) < 8 || len(secret) > 256 {
112 panic("guardians: reveal secret length must be 8..256")
113 }
114 if !saleConfigured || time.Now().Unix() < saleConfig.publicEnd {
115 panic("guardians: public sale has not ended")
116 }
117 if ComputeRevealCommitment(secret) != revealCommitment {
118 panic("guardians: reveal secret does not match commitment")
119 }
120 if token.TotalSupply() == 0 {
121 panic("guardians: cannot reveal an empty collection")
122 }
123
124 revealSeedHash = hashBytes([]byte("GUARDIANS|SEED|" + secret))
125 permutationA = derivePermutationA(secret)
126 permutationB = deriveSeedNumber("B", secret) % maxSupply
127 revealTarget = token.TotalSupply()
128 revealProcessed = 0
129 revealState = revealStateRevealing
130
131 chain.Emit(
132 "RevealStarted",
133 "seedHash", revealSeedHash,
134 "target", ufmt.Sprintf("%d", revealTarget),
135 )
136}
137
138// ApplyRevealBatch updates sequentially minted tokens. Anyone may execute the
139// deterministic batch, so the project operator cannot block progress.
140func ApplyRevealBatch(startTokenID int64, quantity int) {
141 if revealState != revealStateRevealing {
142 panic("guardians: reveal batch is not active")
143 }
144 if quantity < 1 || quantity > maxRevealBatch {
145 panic("guardians: reveal batch quantity must be 1..20")
146 }
147 if startTokenID != revealProcessed+1 {
148 panic("guardians: reveal batch must continue sequentially")
149 }
150 end := startTokenID + int64(quantity) - 1
151 if end > revealTarget {
152 panic("guardians: reveal batch exceeds target")
153 }
154
155 for tokenID := startTokenID; tokenID <= end; tokenID++ {
156 metadataID := MetadataID(tokenID)
157 uri := finalBaseURI + metadataFilename(metadataID)
158 if err := metadataLedger.SetTokenURI(grc721.TokenID(ufmt.Sprintf("%d", tokenID)), uri); err != nil {
159 panic(err)
160 }
161 }
162 revealProcessed = end
163 if revealProcessed == revealTarget {
164 revealState = revealStateRevealed
165 }
166
167 chain.Emit(
168 "RevealBatchApplied",
169 "startTokenId", ufmt.Sprintf("%d", startTokenID),
170 "endTokenId", ufmt.Sprintf("%d", end),
171 "processed", ufmt.Sprintf("%d", revealProcessed),
172 )
173}
174
175// metadataFilename matches the immutable IPFS release layout (0001.json–3333.json).
176func metadataFilename(metadataID int64) string {
177 serial := ufmt.Sprintf("%d", metadataID)
178 for len(serial) < 4 {
179 serial = "0" + serial
180 }
181 return serial + ".json"
182}
183
184func MetadataID(tokenID int64) int64 {
185 if revealState != revealStateRevealing &&
186 revealState != revealStateRevealed &&
187 revealState != revealStateUnveiledFrozen {
188 panic("guardians: metadata mapping is not revealed")
189 }
190 if tokenID < 1 || tokenID > revealTarget {
191 panic("guardians: token id outside reveal target")
192 }
193 return (permutationA*(tokenID-1)+permutationB)%maxSupply + 1
194}
195
196func FreezeMetadata(cur realm) {
197 assertAdmin(cur.Previous().Address())
198 if revealState != revealStateRevealed || revealProcessed != revealTarget {
199 panic("guardians: complete reveal before freezing metadata")
200 }
201 revealState = revealStateUnveiledFrozen
202 chain.Emit("MetadataFrozen", "provenance", provenanceRoot, "seedHash", revealSeedHash)
203}
204
205func assertMintingAllowed() {
206 if revealState != revealStateVeiled && revealState != revealStateCommitted {
207 panic("guardians: minting is closed after reveal starts")
208 }
209}
210
211func validFinalBaseURI(uri string) bool {
212 return (strings.HasPrefix(uri, "ipfs://") || strings.HasPrefix(uri, "https://")) &&
213 strings.HasSuffix(uri, "/")
214}
215
216func derivePermutationA(secret string) int64 {
217 candidate := deriveSeedNumber("A", secret) % maxSupply
218 if candidate == 0 {
219 candidate = 1
220 }
221 for gcd(candidate, maxSupply) != 1 {
222 candidate++
223 if candidate >= maxSupply {
224 candidate = 1
225 }
226 }
227 return candidate
228}
229
230func deriveSeedNumber(label, secret string) int64 {
231 hash := hashBytes([]byte("GUARDIANS|PERMUTATION|" + label + "|" + secret))
232 value, err := strconv.ParseInt(hash[:15], 16, 64)
233 if err != nil {
234 panic(err)
235 }
236 return value
237}
238
239func gcd(first, second int64) int64 {
240 for second != 0 {
241 first, second = second, first%second
242 }
243 return first
244}
245
246func normalizeHash(value string) string {
247 decoded, err := hex.DecodeString(value)
248 if err != nil || len(decoded) != sha256.Size {
249 return ""
250 }
251 return hex.EncodeToString(decoded)
252}