Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}