engines.gno
7.51 Kb · 239 lines
1package core
2
3import (
4 "chain"
5 "chain/runtime"
6 "strings"
7
8 "gno.land/p/nt/avl/v0"
9
10 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/engine/v1"
11 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/safe/v1"
12)
13
14// Engine registry ("art as code", design §5).
15//
16// An engine registers itself from its own /r/ adapter realm; its id is that
17// realm's package path, so nobody can register in someone else's name. The
18// stored interface value's methods are declared in the adapter, so every
19// call runs with the adapter's authority, not core's (interrealm v2 borrow
20// rule #1): engine code cannot write core state. It also cannot call back
21// into a core mutator: all of core's state-changing functions are crossing
22// and need a live cur the engine never has.
23//
24// Honest limits, surfaced to users rather than hidden:
25// - an adapter realm with state could change its art later; the sample
26// fingerprint (VerifyEngine) detects a change of the sample, and review
27// means reading the adapter for statelessness;
28// - a private (re-deployable) adapter cannot register: the VM aborts the
29// registration when core's state would retain the private realm's type
30// (spike A9, observed in gno v1.7.0 test mode; confirm on Onyx).
31type engineRecord struct {
32 id string
33 impl engine.Engine
34 manifest engine.Manifest // core-owned copy
35 fingerprint string // sha256 of Render(SampleInput())
36 claimed bool
37 deprecated bool
38 reviewed bool
39 registeredHeight int64
40}
41
42var (
43 engines avl.Tree // engine id -> *engineRecord
44 engineOrder avl.Tree // safe.PadInt(seq) -> engine id
45 engineCount int64
46)
47
48// RegisterEngine stores the calling realm's engine (I-12 exception: one
49// sample render in the engine's own registration transaction). The record
50// is immutable afterwards except for the claim/deprecate/review flags.
51func RegisterEngine(cur realm, e engine.Engine) string {
52 id := cur.Previous().PkgPath()
53 if !strings.HasPrefix(id, runtime.ChainDomain()+"/r/") || strings.Contains(id, "#") {
54 panic("an engine registers from its own realm (an /r/ package, not a sub-realm or wallet)")
55 }
56 if e == nil {
57 panic("nil engine")
58 }
59 if engines.Has(id) {
60 panic("engine already registered: " + id + " (publish a new version under a new path)")
61 }
62 m := copyManifest(e.Manifest())
63 if err := engine.ValidateManifest(m); err != nil {
64 panic(err)
65 }
66 if err := e.ValidateParams(""); err != nil {
67 panic("engine must accept empty params (its defaults): " + err.Error())
68 }
69 sample := e.Render(engine.SampleInput())
70 if err := engine.CheckSVG(m, sample); err != nil {
71 panic("sample render: " + err.Error())
72 }
73 if err := engine.ValidateTraits(m, e.Traits(engine.SampleInput())); err != nil {
74 panic("sample traits: " + err.Error())
75 }
76
77 rec := &engineRecord{
78 id: id,
79 impl: e,
80 manifest: m,
81 fingerprint: safe.Sha256Hex(sample),
82 registeredHeight: height(),
83 }
84 engines.Set(id, rec)
85 engineCount++
86 engineOrder.Set(safe.PadInt(engineCount, cidWidth), id)
87 chain.Emit("EngineRegistered", "id", id, "author", m.Author.String(), "fingerprint", rec.fingerprint)
88 return id
89}
90
91// ClaimEngine proves the manifest author controls that address. Unclaimed
92// engines cannot back a collection.
93func ClaimEngine(cur realm, id string) {
94 assertNoDeposit(cur)
95 rec := mustEngine(id)
96 if caller(cur) != rec.manifest.Author {
97 panic("only the manifest author " + rec.manifest.Author.String() + " can claim this engine")
98 }
99 rec.claimed = true
100 chain.Emit("EngineClaimed", "id", id)
101}
102
103// DeprecateEngine stops new collections; existing ones keep rendering.
104func DeprecateEngine(cur realm, id string) {
105 assertNoDeposit(cur)
106 rec := mustEngine(id)
107 if !rec.claimed || caller(cur) != rec.manifest.Author {
108 panic("only the engine's author can deprecate it")
109 }
110 rec.deprecated = true
111 chain.Emit("EngineDeprecated", "id", id)
112}
113
114func mustEngine(id string) *engineRecord {
115 v := engines.Get(id)
116 if v == nil {
117 panic("unknown engine " + id)
118 }
119 return v.(*engineRecord)
120}
121
122// copyManifest gives core its own copy: the engine's slice was allocated in
123// the engine realm.
124func copyManifest(m engine.Manifest) engine.Manifest {
125 out := m
126 out.TraitTypes = append([]string{}, m.TraitTypes...)
127 return out
128}
129
130// --- engine reads (Studio, Verify) -------------------------------------------
131
132// RenderWith renders any registered engine for arbitrary params and seed as
133// a data URI — Studio previews and the Verify button.
134func RenderWith(id, params, seed string) string {
135 rec := mustEngine(id)
136 if err := rec.impl.ValidateParams(params); err != nil {
137 panic("params rejected by engine: " + err.Error())
138 }
139 svg := rec.impl.Render(studioInput(params, seed))
140 if err := engine.CheckSVG(rec.manifest, svg); err != nil {
141 return dataURI(placeholderSVG(rec.manifest.Name, "Engine output rejected"))
142 }
143 return dataURI(svg)
144}
145
146// EngineTraits returns a core-owned copy of the engine's traits for seed.
147func EngineTraits(id, params, seed string) []engine.Trait {
148 rec := mustEngine(id)
149 if err := rec.impl.ValidateParams(params); err != nil {
150 panic("params rejected by engine: " + err.Error())
151 }
152 ts := rec.impl.Traits(studioInput(params, seed))
153 if err := engine.ValidateTraits(rec.manifest, ts); err != nil {
154 panic("engine traits rejected: " + err.Error())
155 }
156 return append([]engine.Trait{}, ts...)
157}
158
159func TraitsWith(id, params, seed string) string {
160 w := &safe.JSONWriter{}
161 writeTraits(w, EngineTraits(id, params, seed))
162 return w.String()
163}
164
165// VerifyEngine re-renders the sample and compares it with the fingerprint
166// stored at registration.
167func VerifyEngine(id string) bool {
168 rec := mustEngine(id)
169 return safe.Sha256Hex(rec.impl.Render(engine.SampleInput())) == rec.fingerprint
170}
171
172// EngineManifest is the typed read for other realms (gallery).
173func EngineManifest(id string) engine.Manifest { return copyManifest(mustEngine(id).manifest) }
174
175// EngineStatus is "unclaimed", "unverified", "reviewed" or "deprecated".
176func EngineStatus(id string) string { return engineStatus(mustEngine(id)) }
177
178func engineStatus(rec *engineRecord) string {
179 switch {
180 case rec.deprecated:
181 return "deprecated"
182 case !rec.claimed:
183 return "unclaimed"
184 case rec.reviewed:
185 return "reviewed"
186 }
187 return "unverified"
188}
189
190// Engine is the record as JSON.
191func Engine(id string) string {
192 w := &safe.JSONWriter{}
193 writeEngine(w, mustEngine(id))
194 return w.String()
195}
196
197func Engines(offset, limit int64) string {
198 w := &safe.JSONWriter{}
199 w.BeginArray()
200 engineOrder.IterateByOffset(int(offset), int(clampLimit(limit)), func(_ string, v any) bool {
201 writeEngine(w, mustEngine(v.(string)))
202 return false
203 })
204 w.EndArray()
205 return w.String()
206}
207
208func writeEngine(w *safe.JSONWriter, rec *engineRecord) {
209 m := rec.manifest
210 w.BeginObject()
211 w.StrField("id", rec.id)
212 w.StrField("pkgPath", rec.id) // the adapter realm; read its source (and imports) with vm/qfile
213 w.StrField("name", m.Name)
214 w.StrField("version", m.Version)
215 w.StrField("description", m.Description)
216 w.StrField("license", m.License)
217 w.StrField("author", m.Author.String())
218 w.StrField("paramsSchema", m.ParamsSchema)
219 w.Key("traitTypes")
220 w.BeginArray()
221 for _, t := range m.TraitTypes {
222 w.Str(t)
223 }
224 w.EndArray()
225 w.BoolField("living", m.Living)
226 w.IntField("maxSvgBytes", m.MaxSVGBytes)
227 w.StrField("fingerprint", rec.fingerprint)
228 w.StrField("status", engineStatus(rec))
229 w.IntField("registeredHeight", rec.registeredHeight)
230 w.EndObject()
231}
232
233func studioInput(params, seed string) engine.Input {
234 in := engine.SampleInput()
235 in.Seed = seed
236 in.Params = params
237 in.Height = height()
238 return in
239}