registry.gno
4.10 Kb · 132 lines
1// Package registry is spike (a) of tasarim-v6 §5.5: can a realm store an
2// interface value handed over by ANOTHER realm and call it later, and what
3// can that foreign code do to the registry while it runs?
4//
5// It is deliberately tiny and is never part of the product. Two exports are
6// intentionally unsafe probes and are marked as such: SharedBoard (returns a
7// pointer into registry state) and UnsafeBump (a non-crossing exported
8// mutator). Production core has neither; this spike measures why.
9package registry
10
11import (
12 "chain"
13 "chain/runtime"
14 "crypto/sha256"
15 "encoding/hex"
16 "strconv"
17 "strings"
18
19 "gno.land/p/nt/avl/v0"
20)
21
22// Engine is the contract a registered engine satisfies. Methods take and
23// return plain values only, so the engine never receives a reference into
24// registry state (tasarim-v6 §5.4 defence (a)).
25type Engine interface {
26 Name() string
27 Render(seed string) string
28}
29
30// SampleSeed is rendered once at registration; its hash is the fingerprint.
31const SampleSeed = "spike-sample"
32
33type record struct {
34 impl Engine
35 fingerprint string
36 height int64
37}
38
39// Board is registry-owned state that a foreign engine can get a pointer to.
40type Board struct {
41 Count int
42}
43
44var (
45 engines avl.Tree // engine pkgpath -> *record
46 board = &Board{}
47 bumps int
48)
49
50// Register stores the caller realm's engine. The id is the caller's pkgpath,
51// so a realm can only ever register itself.
52func Register(cur realm, e Engine) string {
53 id := cur.Previous().PkgPath()
54 if id == "" || strings.Contains(id, "#") {
55 panic("only a code realm can register an engine")
56 }
57 if e == nil {
58 panic("nil engine")
59 }
60 if engines.Has(id) {
61 panic("engine already registered: " + id)
62 }
63 sample := e.Render(SampleSeed)
64 engines.Set(id, &record{impl: e, fingerprint: sha256Hex(sample), height: runtime.ChainHeight()})
65 chain.Emit("SpikeEngineRegistered", "id", id, "name", e.Name())
66 return id
67}
68
69// RenderWith runs a registered engine. Read-only: reached through qeval.
70func RenderWith(id, seed string) string {
71 return mustRecord(id).impl.Render(seed)
72}
73
74// RenderInTx runs an engine inside a transaction. UNSAFE PROBE: a qeval
75// discards writes, so this is how the spike observes on chain what engine
76// code can do from a write path — the thing core never does (I-12).
77func RenderInTx(cur realm, id, seed string) string {
78 return mustRecord(id).impl.Render(seed)
79}
80
81// VerifyFingerprint re-renders the sample and compares it with the hash
82// stored at registration; false means the engine's output changed.
83func VerifyFingerprint(id string) bool {
84 r := mustRecord(id)
85 return sha256Hex(r.impl.Render(SampleSeed)) == r.fingerprint
86}
87
88func Fingerprint(id string) string { return mustRecord(id).fingerprint }
89
90func IsRegistered(id string) bool { return engines.Has(id) }
91
92// SharedBoard returns a pointer into registry state. UNSAFE PROBE: the spike
93// checks that a foreign engine writing through it is refused by the VM
94// (interrealm v2 §3.2 readonly check).
95func SharedBoard() *Board { return board }
96
97// BoardCount reads the probe target.
98func BoardCount() int { return board.Count }
99
100// UnsafeBump is a non-crossing exported function that mutates registry
101// state. UNSAFE PROBE: interrealm v2 §4.1 says a non-crossing call into
102// /r/registry borrows /r/registry's storage, so a foreign engine calling it
103// SHOULD succeed. That is exactly why production core exports no
104// state-changing non-crossing function (invariant I-12).
105func UnsafeBump() { bumps++ }
106
107// Bumps reads the UnsafeBump counter.
108func Bumps() int { return bumps }
109
110func Render(path string) string {
111 out := "# spike/registry\n\n"
112 engines.Iterate("", "", func(key string, value any) bool {
113 r := value.(*record)
114 out += "- `" + key + "` fingerprint `" + r.fingerprint + "` (h=" + strconv.FormatInt(r.height, 10) + ")\n"
115 return false
116 })
117 out += "\nboard.Count=" + strconv.Itoa(board.Count) + " bumps=" + strconv.Itoa(bumps) + "\n"
118 return out
119}
120
121func mustRecord(id string) *record {
122 v := engines.Get(id)
123 if v == nil {
124 panic("unknown engine: " + id)
125 }
126 return v.(*record)
127}
128
129func sha256Hex(s string) string {
130 sum := sha256.Sum256([]byte(s))
131 return hex.EncodeToString(sum[:])
132}