engine.gno
6.41 Kb · 208 lines
1// Package engine is the "art as code" contract (design §5.2): what a
2// generative engine implements, what core hands it, and the conformance
3// rules both core and engine authors check against.
4//
5// An engine is registered from its own /r/ adapter realm. Because the
6// adapter's methods are declared in that realm, core calling them runs the
7// engine with the adapter's authority, never core's (interrealm v2 borrow
8// rule #1). Methods receive and return plain values only: an engine never
9// gets a reference into core state.
10package engine
11
12import (
13 "errors"
14 "strconv"
15
16 "gno.land/p/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/safe/v1"
17)
18
19// Input is everything an engine may depend on. p/ packages are frozen once
20// deployed, so the fields "living" engines will need later (phase 2) are
21// here from day one.
22type Input struct {
23 Seed string // 64-hex final seed (sample seeds in Studio)
24 Params string // collection params, in the engine's own format
25 CollectionID string
26 TokenID string
27 MintHeight int64
28 LastMoveHeight int64 // patina: holding time = Height - LastMoveHeight
29 Moves int64 // transfers so far
30 Height int64 // height of the read (fixed by Verify)
31}
32
33type Trait struct {
34 Type, Value string
35}
36
37type Manifest struct {
38 Name, Version, Description, License string
39 Author address // proven by core.ClaimEngine
40 ParamsSchema string // JSON Schema subset; Studio builds its form from it
41 TraitTypes []string
42 Living bool // Render depends on LastMoveHeight/Moves/Height
43 MaxSVGBytes int64 // <= MaxSVGBytes
44}
45
46// Engine is implemented by a stateless value declared in the adapter realm.
47//
48// Contract: deterministic in Input; ValidateParams("") must accept the
49// engine's defaults; Render returns raw SVG (core does the base64); Traits
50// only uses types listed in the manifest, each at most once.
51type Engine interface {
52 Manifest() Manifest
53 ValidateParams(params string) error
54 Traits(in Input) []Trait
55 Render(in Input) string
56}
57
58// Limits core enforces on every engine.
59const (
60 MaxSVGBytes = 65536
61 MaxTraits = 32
62 MaxTraitLen = 64
63 MaxNameLen = 64
64 MaxVersionLen = 32
65 MaxDescLen = 1024
66 MaxLicenseLen = 64
67 MaxParamsSchema = 4096
68)
69
70// SampleInput is the fixed input of an engine's fingerprint: core renders it
71// at registration and stores sha256(svg); anyone can recompute it later.
72func SampleInput() Input {
73 return Input{
74 Seed: safe.Sha256Hex(safe.Domain + "|engine-sample|v1"),
75 CollectionID: "0",
76 TokenID: "0",
77 MintHeight: 1,
78 LastMoveHeight: 1,
79 Height: 1,
80 }
81}
82
83// SampleSeed returns the i-th Studio/conformance sample seed.
84func SampleSeed(i int) string {
85 return safe.Sha256Hex(safe.Domain + "|sample|v1|" + strconv.Itoa(i))
86}
87
88// ValidateManifest checks sizes, text hygiene and the author address.
89func ValidateManifest(m Manifest) error {
90 fields := []struct {
91 name, value string
92 max int
93 required bool
94 }{
95 {"name", m.Name, MaxNameLen, true},
96 {"version", m.Version, MaxVersionLen, true},
97 {"description", m.Description, MaxDescLen, false},
98 {"license", m.License, MaxLicenseLen, false},
99 {"paramsSchema", m.ParamsSchema, MaxParamsSchema, false},
100 }
101 for _, f := range fields {
102 if f.required && f.value == "" {
103 return errors.New("manifest " + f.name + ": must not be empty")
104 }
105 if err := safe.CheckText(f.value, f.max); err != nil {
106 return errors.New("manifest " + f.name + ": " + err.Error())
107 }
108 }
109 if !m.Author.IsValid() {
110 return errors.New("manifest author: invalid address")
111 }
112 if len(m.TraitTypes) > MaxTraits {
113 return errors.New("manifest: too many trait types")
114 }
115 for i, tt := range m.TraitTypes {
116 if tt == "" {
117 return errors.New("manifest: empty trait type")
118 }
119 if err := safe.CheckText(tt, MaxTraitLen); err != nil {
120 return errors.New("manifest trait type: " + err.Error())
121 }
122 for _, prev := range m.TraitTypes[:i] {
123 if prev == tt {
124 return errors.New("manifest: duplicate trait type " + tt)
125 }
126 }
127 }
128 if m.MaxSVGBytes <= 0 || m.MaxSVGBytes > MaxSVGBytes {
129 return errors.New("manifest maxSVGBytes: must be 1.." + strconv.Itoa(MaxSVGBytes))
130 }
131 return nil
132}
133
134// ValidateTraits checks one Traits() result against the manifest.
135func ValidateTraits(m Manifest, ts []Trait) error {
136 if len(ts) > len(m.TraitTypes) {
137 return errors.New("traits: more traits than manifest trait types")
138 }
139 for i, t := range ts {
140 if !contains(m.TraitTypes, t.Type) {
141 return errors.New("traits: type not in manifest: " + t.Type)
142 }
143 for _, prev := range ts[:i] {
144 if prev.Type == t.Type {
145 return errors.New("traits: duplicate type " + t.Type)
146 }
147 }
148 if err := safe.CheckText(t.Value, MaxTraitLen); err != nil {
149 return errors.New("traits " + t.Type + ": " + err.Error())
150 }
151 }
152 return nil
153}
154
155// CheckSVG is the output rule core applies to every render.
156func CheckSVG(m Manifest, svg string) error {
157 return safe.AssertSVG(svg, int(m.MaxSVGBytes))
158}
159
160// Conformance is the self-test engine authors run in their _test.gno
161// before registering: manifest, default params, determinism, size, SVG
162// hygiene, traits, and — for non-living engines — independence from the
163// living fields.
164func Conformance(e Engine, samples int) error {
165 m := e.Manifest()
166 if err := ValidateManifest(m); err != nil {
167 return err
168 }
169 if err := e.ValidateParams(""); err != nil {
170 return errors.New("default params rejected: " + err.Error())
171 }
172 inputs := []Input{SampleInput()}
173 for i := 0; i < samples; i++ {
174 in := SampleInput()
175 in.Seed = SampleSeed(i)
176 in.TokenID = strconv.Itoa(i + 1)
177 inputs = append(inputs, in)
178 }
179 for _, in := range inputs {
180 svg := e.Render(in)
181 if svg != e.Render(in) {
182 return errors.New("render is not deterministic for seed " + in.Seed)
183 }
184 if err := CheckSVG(m, svg); err != nil {
185 return errors.New("seed " + in.Seed + ": " + err.Error())
186 }
187 if err := ValidateTraits(m, e.Traits(in)); err != nil {
188 return errors.New("seed " + in.Seed + ": " + err.Error())
189 }
190 if !m.Living {
191 moved := in
192 moved.Height, moved.Moves, moved.LastMoveHeight = in.Height+1000, in.Moves+3, in.Height+500
193 if e.Render(moved) != svg {
194 return errors.New("non-living engine depends on Height/Moves/LastMoveHeight")
195 }
196 }
197 }
198 return nil
199}
200
201func contains(list []string, s string) bool {
202 for _, x := range list {
203 if x == s {
204 return true
205 }
206 }
207 return false
208}