dev.gno
6.74 Kb · 181 lines
1// Package dev is the chain as a Plan 9 device tree.
2//
3// In Plan 9 a device is not storage, it is code behind a name: reading
4// /dev/time runs a function. Everything a gno realm normally reaches through
5// an import of chain/runtime is published here as a file instead, so it can be
6// read, listed, bound and unioned like anything else:
7//
8// cat /dev/sysname the chain id
9// cat /dev/height the block height
10// cat /dev/session what a delegated key is allowed to touch
11//
12// The tree is posted to gno.land/r/moul/x/plan9/ns's /srv at deploy time, so
13// any account can bind it into their own namespace, and nobody has to import
14// this realm to use it. That is the cross-realm mount the whole experiment
15// turns on: an interface value published by one realm, stored by another, and
16// called later from a read-only Render.
17//
18// It is READ-ONLY, like every synthetic tree: the files have no Write, so
19// grafting this into a stranger's namespace cannot be turned into a write
20// against this realm.
21//
22// /dev/session is the one to look at. A gno.land account session is a
23// delegated key scoped to a list of path prefixes, which is Plan 9's "the
24// namespace IS the capability" rediscovered thirty-four years later. Printing
25// it as a file makes that visible.
26//
27// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
28// Center at Bell Labs; the name and the marks are theirs, and the copyright is
29// held by the Plan 9 Foundation (https://p9f.org). This realm is not
30// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
31// code: it borrows the vocabulary so that the design reads without a glossary,
32// and it is an homage, asking what that ecosystem's spirit looks like on a
33// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
34package dev
35
36import (
37 "chain/runtime"
38 "chain/runtime/unsafe"
39 "crypto/sha256"
40 "encoding/hex"
41 "strconv"
42 "strings"
43 "time"
44
45 ninep "gno.land/p/moul/x/plan9/ninep/v0"
46 synfs "gno.land/p/moul/x/plan9/synfs/v0"
47
48 nsrealm "gno.land/r/moul/x/plan9/ns/v0"
49)
50
51var tree *synfs.Tree
52
53// docs describes each device, for Render and for /dev/drivers.
54var docs = [][2]string{
55 {"caller", "pkgpath of the realm that crossed into this read"},
56 {"domain", "the chain domain"},
57 {"drivers", "this table"},
58 {"height", "current block height"},
59 {"null", "always empty; the bit bucket"},
60 {"random", "sha256 of chain id and height, hex; block-deterministic, NOT unpredictable"},
61 {"session", "the calling key's session scope, one AllowPath per line"},
62 {"sysname", "the chain id"},
63 {"time", "block time, RFC3339"},
64 {"user", "the origin caller's address"},
65 {"zero", "endless NUL bytes; reads exactly what you ask for"},
66}
67
68func init(cur realm) {
69 tree = build()
70 nsrealm.Post(cross(cur), "dev", tree.Root())
71}
72
73func build() *synfs.Tree {
74 t := synfs.New("dev", "sys", func() int64 { return runtime.ChainHeight() })
75 r := t.Root()
76
77 r.Add("sysname", func() string { return runtime.ChainID() })
78 r.Add("domain", func() string { return runtime.ChainDomain() })
79 r.Add("height", func() string { return strconv.FormatInt(runtime.ChainHeight(), 10) })
80 r.Add("time", func() string { return time.Now().Format(time.RFC3339) })
81 r.Add("user", func() string { return unsafe.OriginCaller().String() })
82 r.Add("caller", func() string { return unsafe.PreviousRealm().PkgPath() })
83 r.Add("null", func() string { return "" })
84 r.Add("random", func() string {
85 sum := sha256.Sum256([]byte(runtime.ChainID() + ":" +
86 strconv.FormatInt(runtime.ChainHeight(), 10)))
87 return hex.EncodeToString(sum[:])
88 })
89 r.Add("session", session)
90 r.Add("drivers", drivers)
91
92 // /dev/zero has no end, so it serves the read window itself rather than
93 // materialising a value. An unbounded read returns nothing, which is what
94 // stops `cat /dev/zero` from being a denial of service.
95 r.AddRange("zero", 0444, func(off, count int64) (string, error) {
96 if count <= 0 {
97 return "", nil
98 }
99 if count > 4096 {
100 count = 4096
101 }
102 return strings.Repeat("\x00", int(count)), nil
103 })
104 return t
105}
106
107// session prints the calling key's authority. A plain key has none, which is
108// itself worth saying out loud.
109func session() string {
110 pubKeyAddr, expiresAt, allowPaths, isSession := runtime.GetSessionInfo()
111 if !isSession {
112 var b strings.Builder
113 b.WriteString("session no\n")
114 b.WriteString("scope full\n")
115 return b.String()
116 }
117 var b strings.Builder
118 b.WriteString("session yes\n")
119 b.WriteString("key " + pubKeyAddr.String() + "\n")
120 b.WriteString("expires " + strconv.FormatInt(expiresAt, 10) + "\n")
121 for _, p := range allowPaths {
122 b.WriteString("allow " + p + "\n")
123 }
124 return b.String()
125}
126
127func drivers() string {
128 var b strings.Builder
129 for _, d := range docs {
130 b.WriteString(d[0] + "\t" + d[1] + "\n")
131 }
132 return b.String()
133}
134
135// Root returns the device tree, for a realm that would rather import it than
136// bind it. Reading it is safe from anywhere; it has no mutating method.
137func Root() ninep.File { return tree.Root() }
138
139// Render lists the devices, or reads one.
140//
141// Render("") the table of devices
142// Render("cat/height") one device's contents
143func Render(path string) string {
144 parts := strings.Split(strings.Trim(path, "/"), "/")
145 if len(parts) >= 2 && parts[0] == "cat" {
146 name := parts[1]
147 f, err := tree.Root().Walk(name)
148 if err != nil {
149 return "# /dev/" + name + "\n\n```\n" + err.Error() + "\n```\n"
150 }
151 data, err := ninep.ReadAll(f)
152 if err != nil {
153 return "# /dev/" + name + "\n\n```\n" + err.Error() + "\n```\n"
154 }
155 return "# /dev/" + name + "\n\n```\n" + data + "\n```\n\n[all devices](:)\n"
156 }
157 return renderIndex()
158}
159
160func renderIndex() string {
161 var b strings.Builder
162 b.WriteString("# /dev\n\n")
163 b.WriteString("The chain as a Plan 9 device tree. Each file is a function: reading it ")
164 b.WriteString("runs code, so `/dev/height` is never stale.\n\n")
165 b.WriteString("Posted to [`r/moul/x/plan9/ns`](/r/moul/x/plan9/ns/v0)'s `/srv` at deploy ")
166 b.WriteString("time, so no realm has to import this one to use it:\n\n")
167 b.WriteString("```\nbind /srv/dev /dev\n```\n\n")
168 b.WriteString("| device | contents |\n|---|---|\n")
169 for _, d := range docs {
170 b.WriteString("| [`" + d[0] + "`](:cat/" + d[0] + ") | " + d[1] + " |\n")
171 }
172 b.WriteString("\nDesign and analysis: ")
173 b.WriteString("[moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136).\n")
174 b.WriteString("\n_Not affiliated with Plan 9. Plan 9 from Bell Labs is the ")
175 b.WriteString("work of the Computing Science Research Center at Bell Labs; the name ")
176 b.WriteString("and the marks are theirs, and the copyright is held by the ")
177 b.WriteString("[Plan 9 Foundation](https://p9f.org). This realm borrows the ")
178 b.WriteString("vocabulary and none of the code: it is an homage, asking what that ")
179 b.WriteString("ecosystem's spirit looks like on a chain._\n")
180 return b.String()
181}