Transaction
DD24EF909B662A…161D43A4E4E7
Block 272,412 · index 0 · indexed
Summary
- Hash
- DD24EF909B662AABFA6FDE4DBF3B9637A3591154D19C93158D66161D43A4E4E7
- Block
- 272,412
- Size
- 80078 bytes
- Gas used
- 94,116,519 / 254,060,400
- Fee
- 762181ugnot
- Status
- success
Messages
- Attached funds
- 11000000ugnot
Arguments · 9
- #1rc
- #2README.md
- #3# `gno.land/p/moul/x/plan9/rc/v0` **A small shell over a Plan 9 [namespace](../../ns/v0)**: `ls`, `cat`, `stat`, `ns`, `bind`, `mount`, `unmount`, `mkdir`, `rm`, `echo`, `cd`, `pwd`, `walk`, `help`. ```go import rc "gno.land/p/moul/x/plan9/rc/v0" sh, fs := rc.NewMemShell("g1...", rc.ReadWrite, runtime.ChainHeight) out, err := sh.Run("bind -ac /srv/dev /dev; echo hello > /tmp/greeting; ls -l /") ``` A namespace you cannot inspect is a namespace you cannot trust. Everything here operates on an `ns.Ns` and returns text, so one realm's `Render` becomes a file browser and one transaction becomes a shell command. **The mode split is the security model.** A shell in `ReadOnly` mode refuses every mutating command, which is what lets a realm expose it through `Render`, where mutating anything would be a bug, while the same code backs a crossing `Exec` that may write. **On the first error the run stops and returns it.** A realm should let that error panic, so a half-applied command line reverts with its transaction rather than leaving a namespace nobody asked for. Two commands exist to make a namespace legible rather than to do work: - **`ns`** prints the mount table in `ns(1)` format. - **`walk /bin/rc`** prints how each element resolves, with the union width per step. It is where a bind stops being magic: the width column says exactly where one took effect, and that a union is top level only. Quoting follows rc: single quotes, with `''` inside a quoted string standing for one literal quote. Commands are separated by `;` or newlines, and quoting is respected when splitting them. `MaxCommands` (32) bounds one run. **Live demo:** [`r/moul/x/plan9/ns`](../../../../../r/moul/x/plan9/ns/v0) renders it. Design and analysis: [moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136). --- **Not affiliated with Plan 9.** Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the [Plan 9 Foundation](https://p9f.org). This package borrows the vocabulary and none of the code: it is an independent homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: [NOTICE](../../../../../NOTICE.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/p/moul/x/plan9/rc/v0" gno = "0.9"
- #6rc.gno
- #7// Package rc is a small shell over a Plan 9 namespace. // // It exists because a namespace you cannot inspect is a namespace you cannot // trust. Everything here operates on a gno.land/p/moul/x/plan9/ns namespace // and returns text, so one realm's Render becomes a file browser and one // transaction becomes a shell command: // // Exec("bind -a /srv/dev /dev; echo hello > /tmp/greeting") // // The mode split is the security model. A Shell in ReadOnly mode refuses every // mutating command, which is what lets a realm expose the shell through // Render, where mutating anything would be a bug, while the same code backs a // crossing Exec that may write. // // Quoting follows rc: single quotes, with ” inside a quoted string standing // for one literal quote. Commands are separated by ';' or newlines, and // quoting is respected when splitting them. // // On the first error the run STOPS and returns it. A realm should let that // error panic, so a half-applied command line reverts with the transaction // rather than leaving the namespace in a state nobody asked for. // // NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research // Center at Bell Labs; the name and the marks are theirs, and the copyright is // held by the Plan 9 Foundation (https://p9f.org). This package is not // affiliated with, endorsed by, or sponsored by them, and contains no Plan 9 // code: it borrows the vocabulary so that the design reads without a glossary, // and it is an homage, asking what that ecosystem's spirit looks like on a // chain. Full attribution: NOTICE.md at the root of moul/gno-contracts. package rc import ( "errors" "strconv" "strings" memfs "gno.land/p/moul/x/plan9/memfs/v0" ninep "gno.land/p/moul/x/plan9/ninep/v0" ns "gno.land/p/moul/x/plan9/ns/v0" ) // Mode decides whether mutating commands are allowed. type Mode uint8 // Shell modes. const ( ReadOnly Mode = 0 ReadWrite Mode = 1 ) var ( // ErrReadOnly is returned when a mutating command runs in ReadOnly mode. ErrReadOnly = errors.New("read-only shell") // ErrUsage reports a malformed command line. ErrUsage = errors.New("usage") // ErrUnknown reports a command this shell does not have. ErrUnknown = errors.New("command not found") // ErrQuote reports an unterminated quoted string. ErrQuote = errors.New("unterminated '") ) // MaxCommands bounds one run, so a single call cannot loop the VM. const MaxCommands = 32 // Shell runs commands against one namespace. type Shell struct { ns *ns.Ns mode Mode now func() int64 } // New returns a shell over n. now supplies the block height stamped on writes // and may be nil in ReadOnly mode. func New(n *ns.Ns, mode Mode, now func() int64) *Shell { return &Shell{ns: n, mode: mode, now: now} } // Ns returns the namespace the shell operates on. func (s *Shell) Ns() *ns.Ns { return s.ns } func (s *Shell) clock() int64 { if s.now == nil { return 0 } return s.now() } func (s *Shell) writable() error { if s.mode != ReadWrite { return ErrReadOnly } return nil } // Run executes a command line and returns its output. Execution stops at the // first error. func (s *Shell) Run(line string) (string, error) { cmds, err := splitCommands(line) if err != nil { return "", err } if len(cmds) > MaxCommands { return "", errors.New("too many commands in one line") } var out strings.Builder for _, c := range cmds { toks, err := tokenize(c) if err != nil { return out.String(), err } if len(toks) == 0 { continue } text, err := s.run1(toks) out.WriteString(text) if err != nil { return out.String(), errors.New(toks[0] + ": " + err.Error()) } } return out.String(), nil } func (s *Shell) run1(toks []string) (string, error) { cmd, args := toks[0], toks[1:] switch cmd { case "help": return help, nil case "pwd": return s.ns.Cwd() + "\n", nil case "cd": return "", s.cd(args) case "ns": return s.ns.String(), nil case "ls": return s.ls(args) case "cat": return s.cat(args) case "stat": return s.stat(args) case "walk": return s.walk(args) case "bind", "mount": return "", s.bind(cmd, args) case "unmount", "unbind": return "", s.unmount(args) case "mkdir": return "", s.mkdir(args) case "rm": return "", s.rm(args) case "echo": return s.echo(args) } return "", ErrUnknown } const help = `bind [-a|-b|-c] new old make new visible at old (-b before, -a after, -c creates) cat file... print files cd [dir] change directory echo [-n] word... [>|>> file] ls [-l] [-u] [path...] list; -l long, -u collapse union duplicates mkdir path... create directories mount synonym for bind ns print the namespace pwd print the working directory rm path... remove files and empty directories stat path... print the 9P stat of each path unmount [new] old undo a binding walk path show how each element of path resolves ` func (s *Shell) cd(args []string) error { if len(args) == 0 { return s.ns.Cd("/") } if len(args) > 1 { return ErrUsage } return s.ns.Cd(args[0]) } func (s *Shell) ls(args []string) (string, error) { long, unique, paths := false, false, []string{} for _, a := range args { if strings.HasPrefix(a, "-") && len(a) > 1 { for _, r := range a[1:] { switch r { case 'l': long = true case 'u': unique = true default: return "", ErrUsage } } continue } paths = append(paths, a) } if len(paths) == 0 { paths = []string{s.ns.Cwd()} } var out strings.Builder for i, p := range paths { st, err := s.ns.Stat(p) if err != nil { return out.String(), err } if len(paths) > 1 { if i > 0 { out.WriteString("\n") } out.WriteString(s.ns.Abs(p) + ":\n") } if !st.IsDir() { out.WriteString(entryLine(st, long)) continue } ents, err := s.ns.ReadDir(p, unique) if err != nil { return out.String(), err } for _, e := range ents { out.WriteString(entryLine(e, long)) } } return out.String(), nil } func entryLine(st ninep.Stat, long bool) string { if long { return st.Line() + "\n" } name := st.Name if st.IsDir() && name != "/" { name += "/" } return name + "\n" } func (s *Shell) cat(args []string) (string, error) { if len(args) == 0 { return "", ErrUsage } var out strings.Builder for _, p := range args { data, err := s.ns.ReadFile(p) if err != nil { return out.String(), err } out.WriteString(data) } return out.String(), nil } func (s *Shell) stat(args []string) (string, error) { if len(args) == 0 { args = []string{s.ns.Cwd()} } var out strings.Builder for _, p := range args { st, err := s.ns.Stat(p) if err != nil { return out.String(), err } members, err := s.ns.Resolve(p) if err != nil { return out.String(), err } out.WriteString(s.ns.Abs(p) + " " + st.Qid.String() + " " + st.Mode.String() + " uid=" + st.Uid + " length=" + strconv.FormatInt(st.Length, 10) + " mtime=" + strconv.FormatInt(st.Mtime, 10) + " union=" + strconv.Itoa(len(members)) + "\n") } return out.String(), nil } // walk shows the resolution of each element, which is where a namespace stops // being magic: the union width column says exactly when a bind took effect. func (s *Shell) walk(args []string) (string, error) { if len(args) != 1 { return "", ErrUsage } abs := s.ns.Abs(args[0]) var out strings.Builder prefix := "/" report := func(p string) error { st, err := s.ns.Stat(p) if err != nil { return err } members, err := s.ns.Resolve(p) if err != nil { return err } out.WriteString(pad(p, 24) + " " + st.Qid.String() + " " + st.Mode.String() + " union=" + strconv.Itoa(len(members)) + "\n") return nil } if err := report("/"); err != nil { return out.String(), err } for _, e := range ninep.Elems(abs) { prefix = ninep.Join(prefix, e) if err := report(prefix); err != nil { return out.String(), err } } return out.String(), nil } func pad(s string, n int) string { for len(s) < n { s += " " } return s } func (s *Shell) bind(verb string, args []string) error { if err := s.writable(); err != nil { return err } flag := ns.MREPL rest := []string{} for _, a := range args { if strings.HasPrefix(a, "-") && len(a) > 1 { for _, r := range a[1:] { switch r { case 'b': flag = flag&^0x3 | ns.MBEFORE case 'a': flag = flag&^0x3 | ns.MAFTER case 'c': flag |= ns.MCREATE default: return ErrUsage } } continue } rest = append(rest, a) } if len(rest) != 2 { return ErrUsage } return s.ns.BindVerb(verb, rest[0], rest[1], flag) } func (s *Shell) unmount(args []string) error { if err := s.writable(); err != nil { return err } switch len(args) { case 1: return s.ns.Unmount("", args[0]) case 2: return s.ns.Unmount(args[0], args[1]) } return ErrUsage } func (s *Shell) mkdir(args []string) error { if err := s.writable(); err != nil { return err } if len(args) == 0 { return ErrUsage } for _, p := range args { abs := s.ns.Abs(p) target, err := s.ns.CreateTarget(ninep.Dir(abs)) if err != nil { return err } if _, err := target.Create(ninep.Base(abs), ninep.DirPerm, s.clock()); err != nil { return err } } return nil } func (s *Shell) rm(args []string) error { if err := s.writable(); err != nil { return err } if len(args) == 0 { return ErrUsage } for _, p := range args { abs := s.ns.Abs(p) name := ninep.Base(abs) members, err := s.ns.Resolve(ninep.Dir(abs)) if err != nil { return err } found, removed := false, false for _, m := range members { if _, err := m.Walk(name); err != nil { continue } found = true mu, ok := m.(ninep.Mutable) if !ok { continue // a read-only server: try the next union member } if err := mu.Remove(name); err != nil { return err } removed = true break } switch { case removed: case found: return ninep.ErrReadOnly default: return ninep.ErrNotExist } } return nil } func (s *Shell) echo(args []string) (string, error) { newline := true words := []string{} redirect, appendTo, dest := false, false, "" i := 0 for i < len(args) { a := args[i] switch { case a == "-n" && len(words) == 0 && !redirect: newline = false case a == ">" || a == ">>": if i+1 >= len(args) { return "", ErrUsage } redirect, appendTo, dest = true, a == ">>", args[i+1] i++ default: words = append(words, a) } i++ } text := strings.Join(words, " ") if newline { text += "\n" } if !redirect { return text, nil } if err := s.writable(); err != nil { return "", err } return "", s.writeTo(dest, text, appendTo) } // writeTo creates or opens dest and writes text, honouring the namespace's // MCREATE rules when the file has to be created. func (s *Shell) writeTo(dest, text string, appendTo bool) error { abs := s.ns.Abs(dest) f, err := s.ns.Open(abs) if err == ninep.ErrNotExist { target, terr := s.ns.CreateTarget(ninep.Dir(abs)) if terr != nil { return terr } created, cerr := target.Create(ninep.Base(abs), ninep.FilePerm, s.clock()) if cerr != nil { return cerr } f = created } else if err != nil { return err } mu, ok := f.(ninep.Mutable) if !ok { return ninep.ErrReadOnly } off := int64(0) if appendTo { off = mu.Stat().Length } else if err := mu.Truncate(0, s.clock()); err != nil { return err } _, err = mu.Write(off, text, s.clock()) return err } // splitCommands splits on ';' and newlines, respecting rc quoting. func splitCommands(line string) ([]string, error) { out := []string{} var b strings.Builder quoted := false for i := 0; i < len(line); i++ { c := line[i] if c == '\'' { quoted = !quoted } if !quoted && (c == ';' || c == '\n') { out = append(out, b.String()) b.Reset() continue } b.WriteByte(c) } if quoted { return nil, ErrQuote } out = append(out, b.String()) return out, nil } // tokenize splits one command into words using rc's quoting rules. func tokenize(s string) ([]string, error) { toks := []string{} i := 0 for i < len(s) { for i < len(s) && (s[i] == ' ' || s[i] == '\t') { i++ } if i >= len(s) { break } var b strings.Builder quoted := false for i < len(s) { c := s[i] if c == '\'' { if quoted && i+1 < len(s) && s[i+1] == '\'' { b.WriteByte('\'') i += 2 continue } quoted = !quoted i++ continue } if !quoted && (c == ' ' || c == '\t') { break } b.WriteByte(c) i++ } if quoted { return nil, ErrQuote } toks = append(toks, b.String()) } return toks, nil } // NewMemShell is the convenience a realm or a test wants: a fresh ram root, // a namespace over it, and a shell. It exists here rather than in ns so that // ns keeps no dependency on a particular file server. func NewMemShell(uid string, mode Mode, now func() int64) (*Shell, *memfs.FS) { h := int64(0) if now != nil { h = now() } fs := memfs.New(uid, h) return New(ns.New(fs.Root()), mode, now), fs }
- #8rc_test.gno
- #9package rc import ( "strings" "testing" memfs "gno.land/p/moul/x/plan9/memfs/v0" ninep "gno.land/p/moul/x/plan9/ninep/v0" ns "gno.land/p/moul/x/plan9/ns/v0" synfs "gno.land/p/moul/x/plan9/synfs/v0" ) func clock(h int64) func() int64 { return func() int64 { return h } } func shell(t *testing.T, files ...string) (*Shell, *memfs.FS) { t.Helper() sh, fs := NewMemShell("glenda", ReadWrite, clock(100)) for _, f := range files { if err := fs.WriteFile(f, "contents of "+f+"\n", 100); err != nil { t.Fatalf("seed %s: %v", f, err) } } return sh, fs } func run(t *testing.T, sh *Shell, line string) string { t.Helper() out, err := sh.Run(line) if err != nil { t.Fatalf("%s: %v", line, err) } return out } func TestTokenize(t *testing.T) { tests := []struct { name string in string want []string }{ {"plain", "ls -l /dev", []string{"ls", "-l", "/dev"}}, {"extra spaces", " ls /dev ", []string{"ls", "/dev"}}, {"quoted", "echo 'hello world'", []string{"echo", "hello world"}}, {"quote inside quote", "echo 'it''s'", []string{"echo", "it's"}}, {"empty quoted word", "echo ''", []string{"echo", ""}}, {"adjacent", "echo a'b c'd", []string{"echo", "ab cd"}}, {"empty line", "", []string{}}, } for _, tt := range tests { got, err := tokenize(tt.in) if err != nil { t.Errorf("%s: %v", tt.name, err) continue } if len(got) != len(tt.want) { t.Errorf("%s: got %v, want %v", tt.name, got, tt.want) continue } for i := range got { if got[i] != tt.want[i] { t.Errorf("%s: token %d got %q, want %q", tt.name, i, got[i], tt.want[i]) } } } if _, err := tokenize("echo 'unterminated"); err != ErrQuote { t.Errorf("unterminated quote: got %v", err) } } func TestSplitCommands(t *testing.T) { got, err := splitCommands("pwd; ls /\necho 'a;b'") if err != nil { t.Fatalf("%v", err) } want := []string{"pwd", " ls /", "echo 'a;b'"} if len(got) != len(want) { t.Fatalf("got %v", got) } for i := range want { if got[i] != want[i] { t.Errorf("%d: got %q, want %q", i, got[i], want[i]) } } } func TestPwdAndCd(t *testing.T) { sh, _ := shell(t, "/usr/glenda/lib/profile") if got := run(t, sh, "pwd"); got != "/\n" { t.Errorf("pwd: %q", got) } run(t, sh, "cd /usr/glenda") if got := run(t, sh, "pwd"); got != "/usr/glenda\n" { t.Errorf("pwd after cd: %q", got) } if got := run(t, sh, "cat lib/profile"); got != "contents of /usr/glenda/lib/profile\n" { t.Errorf("relative cat: %q", got) } run(t, sh, "cd") if got := run(t, sh, "pwd"); got != "/\n" { t.Errorf("bare cd should go to the root: %q", got) } } func TestLs(t *testing.T) { sh, _ := shell(t, "/bin/ls", "/bin/rc", "/tmp/x") if got := run(t, sh, "ls /"); got != "bin/\ntmp/\n" { t.Errorf("ls /: %q", got) } if got := run(t, sh, "ls /bin"); got != "ls\nrc\n" { t.Errorf("ls /bin: %q", got) } long := run(t, sh, "ls -l /bin") if !strings.Contains(long, "-rw-r--r-- glenda") { t.Errorf("ls -l: %q", long) } if got := run(t, sh, "ls /bin/ls"); got != "ls\n" { t.Errorf("ls of a file: %q", got) } if _, err := sh.Run("ls -Z"); err == nil { t.Error("an unknown flag should fail") } } func TestLsUnionAndUnique(t *testing.T) { sh, fs := shell(t) fs.WriteFile("/bin/ls", "system\n", 100) fs.WriteFile("/usr/glenda/bin/ls", "mine\n", 100) run(t, sh, "bind -b /usr/glenda/bin /bin") if got := run(t, sh, "ls /bin"); got != "ls\nls\n" { t.Errorf("a union lists both, as Plan 9 does: %q", got) } if got := run(t, sh, "ls -u /bin"); got != "ls\n" { t.Errorf("ls -u: %q", got) } if got := run(t, sh, "cat /bin/ls"); got != "mine\n" { t.Errorf("the member bound before wins the walk: %q", got) } } func TestCatErrors(t *testing.T) { sh, _ := shell(t, "/bin/ls") if _, err := sh.Run("cat /bin"); err == nil { t.Error("cat of a directory should fail") } if _, err := sh.Run("cat /absent"); err == nil { t.Error("cat of a missing file should fail") } if _, err := sh.Run("cat"); err == nil { t.Error("cat with no argument should fail") } } func TestEchoAndRedirect(t *testing.T) { sh, _ := shell(t) if got := run(t, sh, "echo hello world"); got != "hello world\n" { t.Errorf("echo: %q", got) } if got := run(t, sh, "echo -n hello"); got != "hello" { t.Errorf("echo -n: %q", got) } run(t, sh, "echo hello > /greeting") if got := run(t, sh, "cat /greeting"); got != "hello\n" { t.Errorf("after redirect: %q", got) } run(t, sh, "echo again >> /greeting") if got := run(t, sh, "cat /greeting"); got != "hello\nagain\n" { t.Errorf("after append: %q", got) } run(t, sh, "echo replaced > /greeting") if got := run(t, sh, "cat /greeting"); got != "replaced\n" { t.Errorf("a plain redirect truncates: %q", got) } if _, err := sh.Run("echo hi >"); err == nil { t.Error("a redirect with no target should fail") } } func TestMkdirAndRm(t *testing.T) { sh, _ := shell(t) run(t, sh, "mkdir /tmp") run(t, sh, "echo x > /tmp/f") if got := run(t, sh, "ls /tmp"); got != "f\n" { t.Errorf("ls /tmp: %q", got) } if _, err := sh.Run("rm /tmp"); err == nil || err.Error() != "rm: "+ninep.ErrNotEmpty.Error() { t.Errorf("rm of a non-empty directory: got %v", err) } run(t, sh, "rm /tmp/f") run(t, sh, "rm /tmp") if got := run(t, sh, "ls /"); got != "" { t.Errorf("root should be empty: %q", got) } if _, err := sh.Run("rm /absent"); err == nil { t.Error("rm of a missing file should fail") } } func TestBindAndNs(t *testing.T) { sh, _ := shell(t, "/bin/ls", "/usr/glenda/bin/rc") run(t, sh, "bind -a /usr/glenda/bin /bin") if got := run(t, sh, "ls /bin"); got != "ls\nrc\n" { t.Errorf("ls after bind: %q", got) } want := "bind -a /usr/glenda/bin /bin\ncd /\n" if got := run(t, sh, "ns"); got != want { t.Errorf("ns:\ngot %q\nwant %q", got, want) } run(t, sh, "unmount /usr/glenda/bin /bin") if got := run(t, sh, "ls /bin"); got != "ls\n" { t.Errorf("after unmount: %q", got) } if got := run(t, sh, "ns"); got != "cd /\n" { t.Errorf("ns after unmount: %q", got) } } func TestMountIsASynonym(t *testing.T) { sh, _ := shell(t, "/bin/ls", "/srv/dev/height") run(t, sh, "mkdir /dev") run(t, sh, "mount /srv/dev /dev") if got := run(t, sh, "ls /dev"); got != "height\n" { t.Errorf("ls /dev: %q", got) } if !strings.HasPrefix(run(t, sh, "ns"), "mount /srv/dev /dev\n") { t.Errorf("ns should record the verb used: %q", run(t, sh, "ns")) } } func TestCreateInAUnionNeedsTheFlag(t *testing.T) { sh, _ := shell(t, "/bin/ls", "/usr/glenda/bin/rc") run(t, sh, "bind -a /usr/glenda/bin /bin") if _, err := sh.Run("echo x > /bin/new"); err == nil { t.Error("creating in a union with no -c should be refused") } run(t, sh, "bind -ac /usr/glenda/bin /bin") run(t, sh, "echo x > /bin/new") if got := run(t, sh, "cat /usr/glenda/bin/new"); got != "x\n" { t.Errorf("the create should land in the -c member: %q", got) } } func TestReadOnlyModeRefusesWrites(t *testing.T) { sh, fs := NewMemShell("glenda", ReadOnly, clock(1)) fs.WriteFile("/bin/ls", "x\n", 1) // Reads still work. if got := run(t, sh, "ls /bin"); got != "ls\n" { t.Errorf("ls: %q", got) } if got := run(t, sh, "cat /bin/ls"); got != "x\n" { t.Errorf("cat: %q", got) } if got := run(t, sh, "echo hi"); got != "hi\n" { t.Errorf("echo without a redirect is not a write: %q", got) } for _, line := range []string{ "mkdir /tmp", "rm /bin/ls", "bind /bin /bin", "unmount /bin", "echo hi > /f", } { _, err := sh.Run(line) if err == nil { t.Errorf("%q should be refused in ReadOnly mode", line) continue } if !strings.Contains(err.Error(), ErrReadOnly.Error()) { t.Errorf("%q: got %v, want a read-only error", line, err) } } } func TestStopsAtTheFirstError(t *testing.T) { sh, _ := shell(t) out, err := sh.Run("echo one; cat /absent; echo three") if err == nil { t.Fatal("expected an error") } if out != "one\n" { t.Errorf("output should stop at the failure: %q", out) } if !strings.HasPrefix(err.Error(), "cat: ") { t.Errorf("the error should name the command: %v", err) } } func TestUnknownCommand(t *testing.T) { sh, _ := shell(t) if _, err := sh.Run("nosuchthing"); err == nil { t.Error("expected an error") } } func TestStatAndWalkShowTheUnion(t *testing.T) { sh, fs := shell(t) fs.WriteFile("/bin/ls", "a\n", 100) fs.WriteFile("/usr/glenda/bin/rc", "b\n", 100) run(t, sh, "bind -a /usr/glenda/bin /bin") st := run(t, sh, "stat /bin") if !strings.Contains(st, "union=2") { t.Errorf("stat should report the union width: %q", st) } w := run(t, sh, "walk /bin/rc") lines := strings.Split(strings.TrimSpace(w), "\n") if len(lines) != 3 { t.Fatalf("walk should report /, /bin and /bin/rc: %q", w) } if !strings.Contains(lines[1], "union=2") { t.Errorf("the union should appear at /bin: %q", lines[1]) } if !strings.Contains(lines[2], "union=1") { t.Errorf("a union is top level only, /bin/rc is not one: %q", lines[2]) } } func TestReadOnlyServerMountedInAWritableNamespace(t *testing.T) { // This is the cross-realm shape: a synthetic, read-only tree grafted into // a namespace whose root is writable. Reads work; writes are refused // where the server is read-only and still allowed elsewhere. sh, fs := shell(t) fs.MkdirAll("/dev", 100) tr := synfs.New("dev", "sys", clock(100)) tr.Root().Add("height", func() string { return "100" }) if err := sh.Ns().Mount(tr.Root(), "#dev", "/dev", ns.MREPL); err != nil { t.Fatalf("mount: %v", err) } if got := run(t, sh, "cat /dev/height"); got != "100" { t.Errorf("read through the mount: %q", got) } if got := run(t, sh, "ls /dev"); got != "height\n" { t.Errorf("ls /dev: %q", got) } if _, err := sh.Run("echo x > /dev/height"); err == nil { t.Error("writing to a read-only server should be refused") } if _, err := sh.Run("rm /dev/height"); err == nil || err.Error() != "rm: "+ninep.ErrReadOnly.Error() { t.Errorf("rm on a read-only server: got %v", err) } // The rest of the namespace is unaffected. run(t, sh, "echo fine > /ok") if got := run(t, sh, "cat /ok"); got != "fine\n" { t.Errorf("got %q", got) } } func TestHelpListsEveryCommand(t *testing.T) { sh, _ := shell(t) out := run(t, sh, "help") for _, cmd := range []string{"bind", "cat", "cd", "echo", "ls", "mkdir", "mount", "ns", "pwd", "rm", "stat", "unmount", "walk"} { if !strings.Contains(out, cmd) { t.Errorf("help does not mention %q", cmd) } } }
- Attached funds
- 5000000ugnot
Arguments · 9
- #1aaabbb
- #2README.md
- #3# `gno.land/r/moul/x/pairs/aaabbb/v0` One AMM pair, AAA/BBB, and nothing else: the worked example of an instance realm. Read the file, it is the point. Two constants, an `init` that builds the pair and registers it, and one-line re-exports forwarding `cur` into [`p/moul/x/pair/v0`](../../../../../p/moul/x/pair/v0). It is `tools/pairgen` output, differing only in its doc comment and in two blank imports that force the in-repo test tokens to initialise first (an instance deployed against already-deployed tokens needs neither). ```sh go -C tools run ./pairgen gno.land/r/moul/x/pairs/aaa/v0.AAA \ gno.land/r/moul/x/pairs/bbb/v0.BBB -o /tmp/out -namespace moul/x/pairs diff /tmp/out/aaabbb/aaabbb.gno r/moul/x/pairs/aaabbb/aaabbb.gno ``` Trades against the two throwaway faucet tokens `r/moul/x/pairs/aaa/v0` and `r/moul/x/pairs/bbb/v0`. Neither has any value. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4aaabbb.gno
- #5// Realm aaabbb is ONE AMM pair, AAA/BBB, and nothing else. // // This is the whole instance half of the instance-per-realm pattern, and it is // meant to be read in full in one screen: two configuration constants, an init // that builds the pair and announces it to the registry, and one-line // re-exports that forward the realm's own `cur` into the shared logic in // p/moul/x/pair/v0. Every instance in the ecosystem is byte-identical to this // file except for the two constants, which is what makes verifying an // unknown instance a two-line diff. // // The re-exports cannot be factored away: MsgCall addresses a realm path and a // function name, so anything a user calls has to be declared here. // // State and funds belong to THIS realm: the pair struct is its variable, the // tokens sit at its own package address, and its creator paid its storage // deposit. A bug in one instance cannot touch another. // // Pattern, and why an AMM is the contested case for it: see the README of // gno.land/p/moul/x/pair/v0. package aaabbb import ( "gno.land/p/moul/x/pair/v0" "gno.land/r/moul/x/pairreg/v0" "gno.land/r/nt/grc20reg/v0" // Only because both tokens live in this same repository: the blank imports // force their init (and therefore their grc20reg registration) to run // before this one in a test binary. An instance deployed on chain against // already-deployed tokens does not need them. _ "gno.land/r/moul/x/pairs/aaa/v0" _ "gno.land/r/moul/x/pairs/bbb/v0" ) // The only two lines that differ between two instances. const ( keyA = "gno.land/r/moul/x/pairs/aaa/v0.AAA" keyB = "gno.land/r/moul/x/pairs/bbb/v0.BBB" ) var p *pair.Pair func init(cur realm) { p = pair.New(keyA, keyB, grc20reg.MustGet(keyA), grc20reg.MustGet(keyB)) pairreg.Register(cross(cur), p) } func AddLiquidity(cur realm, maxA, maxB int64) int64 { return p.AddLiquidity(0, cur, maxA, maxB) } func RemoveLiquidity(cur realm, shares int64) (int64, int64) { return p.RemoveLiquidity(0, cur, shares) } func Swap(cur realm, keyIn string, amountIn, minOut int64) int64 { return p.Swap(0, cur, keyIn, amountIn, minOut) } func Quote(keyIn string, amountIn int64) int64 { return p.Quote(keyIn, amountIn) } func Reserves() (int64, int64) { return p.Reserves() } func SharesOf(owner address) int64 { return p.SharesOf(owner) } func TotalShares() int64 { return p.TotalShares() } func Keys() (string, string) { return p.Keys() } func Render(path string) string { return p.Render(path) }
- #6aaabbb_test.gno
- #7package aaabbb import ( "chain" "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/moul/x/pairreg/v0" "gno.land/r/moul/x/pairs/aaa/v0" "gno.land/r/moul/x/pairs/bbb/v0" ) const selfPath = "gno.land/r/moul/x/pairs/aaabbb/v0" func self() address { return chain.PackageAddress(selfPath) } // fund gives addr both tokens and approves this instance to spend them. func fund(cur realm, addr address) { testing.SetRealm(testing.NewUserRealm(addr)) aaa.Faucet(cross(cur)) bbb.Faucet(cross(cur)) aaa.Approve(cross(cur), self(), 1_000_000_000) bbb.Approve(cross(cur), self(), 1_000_000_000) } // Deploying the instance is the whole "factory call": its init builds the pair // and announces it, so one addpkg both creates and lists it. func TestSelfRegisteredAtInit(t *testing.T) { e := pairreg.Get(selfPath) uassert.True(t, e != nil, "instance must register itself in init") uassert.Equal(t, selfPath, e.Path) uassert.Equal(t, 1, pairreg.Size()) keyA, keyB := Keys() uassert.Equal(t, "gno.land/r/moul/x/pairs/aaa/v0.AAA", keyA) uassert.Equal(t, "gno.land/r/moul/x/pairs/bbb/v0.BBB", keyB) } // The registry holds a live pointer into this realm's storage, so its own // Render shows state it never copied. This is the property that lets one // qrender describe a thousand instances. func TestRegistrySeesLiveState(cur realm, t *testing.T) { alice := testutils.TestAddress("alice-live") fund(cur, alice) AddLiquidity(cross(cur), 1_000_000, 4_000_000) e := pairreg.Get(selfPath) resA, resB := e.Pair.Reserves() uassert.Equal(t, int64(1_000_000), resA) uassert.Equal(t, int64(4_000_000), resB) index := pairreg.Render("") uassert.True(t, strings.Contains(index, "AAA/BBB"), "index lists the couple") uassert.True(t, strings.Contains(index, "1000000 AAA"), "index shows live reserves") uassert.True(t, strings.Contains(index, "moul/x/pairs/aaabbb/v0"), "index links the instance") // Wind the pair back down so the next test starts from an empty pool. RemoveLiquidity(cross(cur), TotalShares()) } // Full lifecycle through the instance's own entry points, which is also the // proof that funds move to and from THIS realm's address while every line of // logic runs in the shared pure package. func TestLifecycle(cur realm, t *testing.T) { alice := testutils.TestAddress("alice-cycle") fund(cur, alice) minted := AddLiquidity(cross(cur), 1_000_000, 4_000_000) uassert.Equal(t, int64(1_000_000), minted, "first deposit mints amountA, no sqrt") uassert.Equal(t, int64(1_000_000), aaa.BalanceOf(self()), "tokens land on the instance") uassert.Equal(t, int64(4_000_000), bbb.BalanceOf(self())) uassert.Equal(t, minted, SharesOf(alice)) quoted := Quote("gno.land/r/moul/x/pairs/aaa/v0.AAA", 100_000) out := Swap(cross(cur), "gno.land/r/moul/x/pairs/aaa/v0.AAA", 100_000, 362_000) uassert.Equal(t, int64(362_644), out, "same pricing as the single-realm design") uassert.Equal(t, quoted, out, "Quote matches what Swap pays") resA, resB := Reserves() uassert.Equal(t, int64(1_100_000), resA) uassert.Equal(t, int64(3_637_356), resB) gotA, gotB := RemoveLiquidity(cross(cur), 500_000) uassert.Equal(t, int64(550_000), gotA) uassert.Equal(t, int64(1_818_678), gotB) // The last provider out is paid the whole reserve, so nothing is stranded. gotA, gotB = RemoveLiquidity(cross(cur), TotalShares()) uassert.Equal(t, int64(550_000), gotA) uassert.Equal(t, int64(1_818_678), gotB) uassert.Equal(t, int64(0), TotalShares()) uassert.Equal(t, int64(0), aaa.BalanceOf(self()), "instance holds nothing afterwards") uassert.Equal(t, int64(0), bbb.BalanceOf(self())) } // A token this pair does not hold must be refused by name, not silently // treated as one of the two sides. Asserted through Quote, which reaches the // same guard without a crossing call: a panic raised inside a crossing call // unwinds the whole transaction and is not recoverable by the caller. func TestRejectsForeignToken(cur realm, t *testing.T) { alice := testutils.TestAddress("alice-foreign") fund(cur, alice) AddLiquidity(cross(cur), 1_000_000, 4_000_000) uassert.PanicsWithMessage(t, cur, "pair: gno.land/r/nope/v0.NOPE is not in this pair", func() { Quote("gno.land/r/nope/v0.NOPE", 1_000) }) RemoveLiquidity(cross(cur), TotalShares()) } func TestRender(cur realm, t *testing.T) { alice := testutils.TestAddress("alice-render") fund(cur, alice) empty := Render("") uassert.True(t, strings.Contains(empty, "# AAA / BBB")) uassert.True(t, strings.Contains(empty, "Empty"), "empty pair says so") AddLiquidity(cross(cur), 1_000_000, 4_000_000) full := Render("") uassert.True(t, strings.Contains(full, "| AAA | 1000000 |"), "reserves row") uassert.True(t, strings.Contains(full, "LP shares: **1000000**")) uassert.True(t, strings.Contains(full, "1 AAA buys ~4 BBB"), "spot line") RemoveLiquidity(cross(cur), TotalShares()) }
- #8gnomod.toml
- #9module = "gno.land/r/moul/x/pairs/aaabbb/v0" gno = "0.9" # public: its init registers the *pair.Pair it owns with r/moul/x/pairreg, # so this realm's own objects have to be reachable from that one. That # hand-off is the whole point of the instance-per-realm shape.
- Attached funds
- 10000000ugnot
Arguments · 13
- #1provable
- #2README.md
- #3# provable What gno.land can and cannot prove about itself, demonstrated rather than asserted. Demo realm for [`p/moul/x/merkle/v0`](../../../../../p/moul/x/merkle/v0) and [`p/moul/x/mmr/v0`](../../../../../p/moul/x/mmr/v0). ## The working half The realm keeps a small append-only log in a Merkle mountain range and hands out inclusion proofs against its root. Appending costs O(log n) hashes and never rebuilds. Verification is a single native `crypto/merkle` call. ``` Append("something") → index Root() → the current root, hex ProofOf(index) → path, before, after Verify(root, index, total, entry, path, before, after) → bool ``` `Verify` takes the root as an **argument** rather than reading it from state. That is the honest signature: the realm is a verifier, not an oracle. Passing `Root()` checks against the live log; passing an older root checks against that older log. `VerifyFixed` accepts the other proof encoding, the flat Tendermint sibling list, against the same root. Both work because the log's root **is** the Tendermint simple-tree root over the same entries. `TestVerifyFixedAcceptsTendermintProof` pins that rather than asserting it. ## The half that is the point A proof from this realm says *this entry is consistent with the root this realm published*. It cannot say *this root is the chain's own*. Two independent reasons, both verifiable in the monorepo source: **1. Realm state is not merkleized.** `gno.land/pkg/gnoland/app.go` mounts exactly two stores: ```go baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) // merkleized baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // not ``` and `tm2/pkg/store/dbadapter/store.go` says it in a comment: *"Always returns a zero commitID, as dbadapter store doesn't merkleize"*. The VM keeper takes both, and `gnovm/pkg/gnolang/store.go` puts realm objects, types and realm metadata in the **base** store. | Data | Store | Provable against the app hash | |---|---|---| | Package source | iavl | yes | | Account balances | iavl | yes | | Escaped object hashes (cross-realm, refcount > 1) | iavl | yes, the hash only | | Realm objects, types, realm metadata | base | **no** | | This log | base | **no** | So you can prove to a light client that a package has a given source, and that an address holds a given balance. You cannot prove that any realm's AVL tree contains any key. Every GRC20 balance and every DAO vote on the chain is, today, unprovable. **2. A realm cannot see a header.** `chain/runtime` exposes `ChainID`, `ChainDomain`, `ChainHeight` and `GetSessionInfo`, and nothing else. No app hash, no block hash, no header. Even handed a valid Tendermint proof, a realm has no trusted root to check it against. The one crack in the wall: an **escaped** object, one referenced across realm boundaries, does get its hash written into the IAVL store. Cross-realm objects are partially provable already. Nothing else is. ## Why say so out loud Merkle proofs are the part of a chain that most invites overclaiming. "Verifiable on chain" is true here in a narrow sense and false in the sense a reader assumes. Anything built on Merkle proofs in a gno realm is trust-minimised relative to a committed root, never trustless. A demo that showed only the working half would be the interesting-sounding part of a true story. ## Bounds `MaxEntries` is 512 and `MaxEntryLen` is 256 bytes. `Render` lists the last 10 entries, so the page stays a fixed size: the chain caps a query at `maxGasQuery` and a reader cannot raise it, so an unbounded `Render` is a permanently unreadable page. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/x/provable/v0" gno = "0.9" private = true
- #6provable.gno
- #7// Package provable is an honest demonstration of what gno.land can and cannot // prove about itself. // // It keeps a small append-only log in a Merkle mountain range and hands out // inclusion proofs against the log's root. That part works, and it is the // useful half: a realm can commit to a set and let anyone verify membership // cheaply, with one native call. // // The other half is the point of the realm. A proof here says "this entry is // consistent with the root this realm published". It cannot say "this root is // the chain's own", because: // // 1. gno.land mounts two stores (gno.land/pkg/gnoland/app.go). The IAVL one // is merkleized and holds package source and account balances. The other // is a plain dbadapter whose Commit is documented as // "Always returns a zero commitID, as dbadapter store doesn't merkleize", // and THAT is where every realm's objects live. So no realm's state, // including this log, contributes anything to the app hash. There is no // state proof to produce. // 2. chain/runtime exposes ChainID, ChainDomain, ChainHeight and // GetSessionInfo, and nothing else. No app hash, no block hash, no header. // So even given a Tendermint proof, a realm has no trusted root to check // it against. // // The one crack in the wall: an ESCAPED object, one referenced across realm // boundaries, does get its hash written into the IAVL store // (gnovm/pkg/gnolang/store.go). Cross-realm objects are therefore partially // provable already. Nothing else is. // // Demo of gno.land/p/moul/x/merkle/v0 and gno.land/p/moul/x/mmr/v0. package provable import ( "strconv" "strings" "gno.land/p/moul/x/merkle/v0" "gno.land/p/moul/x/mmr/v0" ) const ( // MaxEntries bounds the log. Unbounded append from an untrusted caller is // a storage-growth hazard even when the caller pays the deposit, and an // unbounded Render is a permanently unreadable page: the chain caps a // query at maxGasQuery, and a reader cannot raise it. MaxEntries = 512 // MaxEntryLen bounds one entry. MaxEntryLen = 256 // renderEntries is how many of the most recent entries Render lists, so // the page stays a fixed size no matter how full the log is. renderEntries = 10 ) var ( log = mmr.New() entries []string ) func init() { seed() } // Append adds an entry to the log and returns its index. The root moves, so // every previously issued proof stops verifying against the new root: that is // the nature of an append-only commitment, not a bug. func Append(cur realm, entry string) int { return appendEntry(entry) } func appendEntry(entry string) int { entry = strings.TrimSpace(entry) if entry == "" { panic("provable: empty entry") } if len(entry) > MaxEntryLen { panic("provable: entry longer than " + strconv.Itoa(MaxEntryLen) + " bytes") } if len(entries) >= MaxEntries { panic("provable: log is full at " + strconv.Itoa(MaxEntries) + " entries") } entries = append(entries, entry) return log.Append([]byte(entry)) } // Size returns the number of entries. func Size() int { return log.Size() } // Root returns the current log root, hex-encoded. It is also the Tendermint // simple-tree root over the same entries, so any Tendermint verifier accepts // it. func Root() string { return log.RootHex() } // Entry returns the entry at index. func Entry(index int) string { mustRange(index) return entries[index] } // ProofOf returns the inclusion proof for index, as the three comma-separated // hex lists Verify expects. func ProofOf(index int) (path, before, after string) { mustRange(index) p, err := log.Proof(index) if err != nil { panic("provable: " + err.Error()) } return p.Hex() } // Verify checks a proof against a root the caller supplies, which is the // honest signature: the realm is a verifier, not an oracle. Pass Root() to // check against the live log. func Verify(root string, index, total int, entry, path, before, after string) bool { rb, err := hexHash(root) if err != nil { return false } p, err := mmr.ParseProof(index, total, path, before, after) if err != nil { return false } return mmr.Verify(rb, []byte(entry), p) } // VerifyFixed checks a Tendermint simple-tree proof, the fixed-leaf-set // encoding, against a root the caller supplies. Same tree as the log, a // different proof shape: siblings are one flat list, leaf first. // // Both encodings verify against the same root, because the Tendermint tree and // a right-bagged mountain range are the same structure. func VerifyFixed(root string, index, total int, leaf, siblings string) bool { rb, err := hexHash(root) if err != nil { return false } p, err := merkle.ParseProof(index, total, siblings) if err != nil { return false } return p.Verify(rb, []byte(leaf)) } func mustRange(index int) { if index < 0 || index >= len(entries) { panic("provable: index out of range") } } func hexHash(s string) ([]byte, error) { p, err := merkle.ParseProof(0, 1, strings.TrimSpace(s)) if err != nil { return nil, err } if len(p.Siblings) != 1 { return nil, merkle.ErrBadHashSize } return p.Siblings[0], nil } // seed returns the realm to its deployed state: an empty log plus the three // demo entries, so a fresh deployment renders something and so the example // tests have a fixed starting point. // // Realm globals persist across a whole test binary and examples run after // every Test, so a pinned Render must call this first. func seed() { log = mmr.New() entries = nil appendEntry("genesis of this log") appendEntry("a second commitment") appendEntry("a third commitment") }
- #8provable_test.gno
- #9package provable import ( "strconv" "strings" "testing" "gno.land/p/moul/x/merkle/v0" ) func TestAppendAndVerify(cur realm, t *testing.T) { seed() i := Append(cross(cur), "a fourth commitment") if i != 3 || Size() != 4 { t.Fatalf("Append returned %d with Size %d, want 3 and 4", i, Size()) } if Entry(i) != "a fourth commitment" { t.Errorf("Entry(%d) = %q", i, Entry(i)) } root := Root() for j := 0; j < Size(); j++ { path, before, after := ProofOf(j) if !Verify(root, j, Size(), Entry(j), path, before, after) { t.Errorf("entry %d: its own proof does not verify", j) } } } // Append trims, so the proof must be over the trimmed entry. func TestAppendTrims(cur realm, t *testing.T) { seed() i := Append(cross(cur), " padded ") if Entry(i) != "padded" { t.Fatalf("Entry = %q, want %q", Entry(i), "padded") } path, before, after := ProofOf(i) if !Verify(Root(), i, Size(), "padded", path, before, after) { t.Error("trimmed entry does not verify") } if Verify(Root(), i, Size(), " padded ", path, before, after) { t.Error("the untrimmed string verified, so the leaf is not what is stored") } } func TestVerifyRejects(cur realm, t *testing.T) { seed() root := Root() path, before, after := ProofOf(1) other := "0000000000000000000000000000000000000000000000000000000000000000" tests := []struct { name string root string index, total int entry, path, before, after string }{ {"wrong entry", root, 1, 3, "not in the log", path, before, after}, {"another real entry", root, 1, 3, Entry(0), path, before, after}, {"wrong index", root, 0, 3, Entry(1), path, before, after}, {"wrong total", root, 1, 4, Entry(1), path, before, after}, {"wrong root", other, 1, 3, Entry(1), path, before, after}, {"root not hex", "zz", 1, 3, Entry(1), path, before, after}, {"root wrong length", "abcd", 1, 3, Entry(1), path, before, after}, {"empty path", root, 1, 3, Entry(1), "", before, after}, {"peaks swapped", root, 1, 3, Entry(1), path, after, before}, {"garbage path", root, 1, 3, Entry(1), "zz", before, after}, } for _, tc := range tests { if Verify(tc.root, tc.index, tc.total, tc.entry, tc.path, tc.before, tc.after) { t.Errorf("%s: accepted, must be rejected", tc.name) } } } // The log root is also the Tendermint simple-tree root over the same entries, // so the fixed-leaf-set proof encoding verifies against it too. This is the // claim the realm makes in Render; it is tested, not asserted. func TestVerifyFixedAcceptsTendermintProof(cur realm, t *testing.T) { seed() Append(cross(cur), "a fourth commitment") Append(cross(cur), "a fifth commitment") leaves := make([][]byte, Size()) for i := range leaves { leaves[i] = []byte(Entry(i)) } tree := merkle.New(leaves) if tree.RootHex() != Root() { t.Fatalf("tree root %s, log root %s", tree.RootHex(), Root()) } for i := 0; i < Size(); i++ { p, err := tree.Proof(i) if err != nil { t.Fatalf("i=%d: %v", i, err) } if !VerifyFixed(Root(), i, Size(), Entry(i), p.Hex()) { t.Errorf("i=%d: Tendermint proof rejected by the realm", i) } if VerifyFixed(Root(), i, Size(), "forged", p.Hex()) { t.Errorf("i=%d: forged leaf accepted", i) } } } func TestCaps(t *testing.T) { seed() tests := []struct { name string entry string }{ {"empty", ""}, {"whitespace only", " "}, {"too long", strings.Repeat("x", MaxEntryLen+1)}, } for _, tc := range tests { if got := panicked(func() { appendEntry(tc.entry) }); got == "" { t.Errorf("%s: accepted, must panic", tc.name) } } // A note of exactly MaxEntryLen is fine. if got := panicked(func() { appendEntry(strings.Repeat("x", MaxEntryLen)) }); got != "" { t.Errorf("entry of exactly MaxEntryLen panicked: %s", got) } } func TestLogIsBounded(t *testing.T) { seed() for Size() < MaxEntries { appendEntry("filler-" + strconv.Itoa(Size())) } if got := panicked(func() { appendEntry("one too many") }); got == "" { t.Error("appending past MaxEntries was accepted") } seed() } func TestEntryRangeChecked(t *testing.T) { seed() for _, i := range []int{-1, 3, 999} { if got := panicked(func() { Entry(i) }); got == "" { t.Errorf("Entry(%d) did not panic", i) } if got := panicked(func() { ProofOf(i) }); got == "" { t.Errorf("ProofOf(%d) did not panic", i) } } } // panicked runs f and returns the panic message, or "" if it returned // normally. Safe here because these calls are in-package and do not cross a // realm boundary, where a panic would be an unrecoverable abort instead. func panicked(f func()) (msg string) { defer func() { if r := recover(); r != nil { if s, ok := r.(string); ok { msg = s } else { msg = "panic" } } }() f() return "" }
- #10render.gno
- #11package provable import ( "strconv" "strings" ) // Render serves the overview at "" and one entry with its proof at // "entry/<index>". // // Output is a fixed size: the entry list is capped at renderEntries, so a full // log still renders. The chain caps a query at maxGasQuery and a reader cannot // raise it, so an unbounded Render is a permanently unreadable page. func Render(path string) string { path = strings.TrimSpace(path) if strings.HasPrefix(path, "entry/") { return renderEntry(strings.TrimPrefix(path, "entry/")) } return renderIndex() } func renderIndex() string { var b strings.Builder b.WriteString("# provable\n\n") b.WriteString("What gno.land can and cannot prove about itself, demonstrated rather than asserted.\n\n") b.WriteString("## The log\n\n") b.WriteString("An append-only Merkle mountain range. Appending costs O(log n) hashes and never rebuilds.\n\n") b.WriteString("| Field | Value |\n|---|---|\n") b.WriteString("| Entries | " + strconv.Itoa(log.Size()) + " / " + strconv.Itoa(MaxEntries) + " |\n") b.WriteString("| Stored hashes | " + strconv.Itoa(log.Nodes()) + " |\n") b.WriteString("| Peaks | " + strconv.Itoa(len(log.PeakHashes())) + " |\n") b.WriteString("| Root | `" + rootOrDash() + "` |\n\n") b.WriteString("This root is also the Tendermint simple-tree root over the same entries, so any\n") b.WriteString("Tendermint verifier accepts it. `Verify` and `VerifyFixed` check the two proof\n") b.WriteString("encodings against it.\n\n") b.WriteString("## What the chain can prove\n\n") b.WriteString("`gno.land/pkg/gnoland/app.go` mounts exactly two stores. One is IAVL and merkleized,\n") b.WriteString("the other is a plain dbadapter whose `Commit` is documented as *\"Always returns a zero\n") b.WriteString("commitID, as dbadapter store doesn't merkleize\"*.\n\n") b.WriteString("| Data | Store | Provable against the app hash |\n|---|---|---|\n") b.WriteString("| Package source | iavl | yes |\n") b.WriteString("| Account balances | iavl | yes |\n") b.WriteString("| Escaped object hashes (cross-realm) | iavl | yes, the hash only |\n") b.WriteString("| Realm objects, types, realm metadata | base | **no** |\n") b.WriteString("| **This log** | base | **no** |\n\n") b.WriteString("## What that means here\n\n") b.WriteString("A proof from this realm says *this entry is consistent with the root this realm\n") b.WriteString("published*. It cannot say *this root is the chain's own*, for two independent reasons:\n\n") b.WriteString("1. Realm state is not merkleized, so there is no state proof to produce.\n") b.WriteString("2. `chain/runtime` exposes `ChainID`, `ChainDomain`, `ChainHeight` and `GetSessionInfo`\n") b.WriteString(" and nothing else. No app hash, no block hash, no header. A realm has no trusted\n") b.WriteString(" root to check anything against.\n\n") b.WriteString("Anything built on Merkle proofs in a gno realm is trust-minimised relative to a\n") b.WriteString("committed root, never trustless. Saying otherwise would be the interesting-sounding\n") b.WriteString("half of a true story.\n\n") b.WriteString("## Entries\n\n") if log.Size() == 0 { b.WriteString("_Empty. Call `Append(\"something\")`._\n") return b.String() } start := 0 if len(entries) > renderEntries { start = len(entries) - renderEntries b.WriteString("Showing the last " + strconv.Itoa(renderEntries) + " of " + strconv.Itoa(len(entries)) + ".\n\n") } b.WriteString("| # | Entry |\n|---|---|\n") for i := start; i < len(entries); i++ { b.WriteString("| [" + strconv.Itoa(i) + "](/r/moul/x/provable/v0:entry/" + strconv.Itoa(i) + ") | " + entries[i] + " |\n") } return b.String() } func renderEntry(raw string) string { i, err := strconv.Atoi(raw) if err != nil || i < 0 || i >= len(entries) { return "# provable\n\nNo entry `" + raw + "`.\n" } path, before, after := ProofOf(i) var b strings.Builder b.WriteString("# provable: entry " + strconv.Itoa(i) + "\n\n") b.WriteString("```\n" + entries[i] + "\n```\n\n") b.WriteString("## Inclusion proof\n\n") b.WriteString("| Field | Value |\n|---|---|\n") b.WriteString("| Index | " + strconv.Itoa(i) + " |\n") b.WriteString("| Total | " + strconv.Itoa(log.Size()) + " |\n") b.WriteString("| Path | `" + orDash(path) + "` |\n") b.WriteString("| Before | `" + orDash(before) + "` |\n") b.WriteString("| After | `" + orDash(after) + "` |\n") b.WriteString("| Root | `" + rootOrDash() + "` |\n\n") b.WriteString("Valid against that root only. The next `Append` moves it.\n") return b.String() } func rootOrDash() string { return orDash(Root()) } func orDash(s string) string { if s == "" { return "-" } return s }
- #12render_example_test.gno
- #13package provable // ExampleRender pins the realm's Render output as a testable example. func ExampleRender() { seed() print(Render("")) // Output: // # provable // // What gno.land can and cannot prove about itself, demonstrated rather than asserted. // // ## The log // // An append-only Merkle mountain range. Appending costs O(log n) hashes and never rebuilds. // // | Field | Value | // |---|---| // | Entries | 3 / 512 | // | Stored hashes | 4 | // | Peaks | 2 | // | Root | `c9d22dd237c668944fde619edb56cc1bee0e988b03ac08694f6d3aba440c0776` | // // This root is also the Tendermint simple-tree root over the same entries, so any // Tendermint verifier accepts it. `Verify` and `VerifyFixed` check the two proof // encodings against it. // // ## What the chain can prove // // `gno.land/pkg/gnoland/app.go` mounts exactly two stores. One is IAVL and merkleized, // the other is a plain dbadapter whose `Commit` is documented as *"Always returns a zero // commitID, as dbadapter store doesn't merkleize"*. // // | Data | Store | Provable against the app hash | // |---|---|---| // | Package source | iavl | yes | // | Account balances | iavl | yes | // | Escaped object hashes (cross-realm) | iavl | yes, the hash only | // | Realm objects, types, realm metadata | base | **no** | // | **This log** | base | **no** | // // ## What that means here // // A proof from this realm says *this entry is consistent with the root this realm // published*. It cannot say *this root is the chain's own*, for two independent reasons: // // 1. Realm state is not merkleized, so there is no state proof to produce. // 2. `chain/runtime` exposes `ChainID`, `ChainDomain`, `ChainHeight` and `GetSessionInfo` // and nothing else. No app hash, no block hash, no header. A realm has no trusted // root to check anything against. // // Anything built on Merkle proofs in a gno realm is trust-minimised relative to a // committed root, never trustless. Saying otherwise would be the interesting-sounding // half of a true story. // // ## Entries // // | # | Entry | // |---|---| // | [0](/r/moul/x/provable/v0:entry/0) | genesis of this log | // | [1](/r/moul/x/provable/v0:entry/1) | a second commitment | // | [2](/r/moul/x/provable/v0:entry/2) | a third commitment | } // ExampleRenderEntry pins the per-entry page, including a live proof. func ExampleRenderEntry() { seed() print(Render("entry/1")) // Output: // # provable: entry 1 // // ``` // a second commitment // ``` // // ## Inclusion proof // // | Field | Value | // |---|---| // | Index | 1 | // | Total | 3 | // | Path | `8f88aaa49b43fc28f266ea6b4f18d865933cad3584b59f162b8ce55edd8263fc` | // | Before | `-` | // | After | `495f0f3042890e970ad8344ae78ff490806c2b8403282d518e8112044bc43827` | // | Root | `c9d22dd237c668944fde619edb56cc1bee0e988b03ac08694f6d3aba440c0776` | // // Valid against that root only. The next `Append` moves it. } // ExampleRenderMissingEntry pins the not-found page. func ExampleRenderMissingEntry() { seed() print(Render("entry/99")) // Output: // # provable // // No entry `99`. }
- Attached funds
- 9000000ugnot
Arguments · 11
- #1bfdemo
- #2README.md
- #3# r/moul/x/vm/bfdemo A realm that runs Brainfuck programs on chain, a slice at a time. It is a demo of [`p/moul/x/vm/bf`](/p/moul/x/vm/bf/v0) (the machine) and [`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0) (the host ABI, the fuel meter, the instance store), and carries no logic of its own. What it exists to show is the thing gno realm code cannot do for itself: a program that runs out of fuel does not fail, it pauses. The realm keeps the snapshot, and the next caller pays for the next slice. Upload a program, call `Step` a few times, and watch one computation finish across several transactions. ## Calls | function | what | |---|---| | `Upload(src, input, budget)` | compile and store a program, returns its id. Compilation happens here, so a malformed program is rejected by the transaction that submitted it | | `Step(id, fuel)` | run one slice and keep the snapshot. Anyone may pay for a slice, not only the owner | | `Remove(id)` | delete an instance. Owner only | `Render("/")` lists the instances and offers three sample programs. `Render("/<id>")` shows one instance: its compiled program, status, fuel, and output. ## Limits Everything a caller can grow is bounded, because all of it is storage somebody pays a deposit on: 64 instances, 64 KiB of source, 1 KiB of input, 4 KiB of output, 5,000,000 fuel per slice, 30,000 tape cells. A guest that writes past the output cap is trapped rather than truncated, so the rendered output is never a lie. ## Untrusted input A program's source is caller-supplied, and everything outside the eight operators is a comment that may hold anything at all. The page never renders it: it shows the program the machine actually compiled, which cannot contain a backtick or a newline and so cannot break out of its code fence. Guest output is arbitrary bytes and is escaped the same way, backtick included. No instance is funded, so `Host.Send` always returns `ErrNotGranted`. That is the capability rule doing its job, not a missing feature. <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4bfdemo.gno
- #5// Package bfdemo is the playground for the guest-VM work: a realm that runs // Brainfuck programs on chain, a slice at a time. // // It is a demo of two libraries and carries no logic of its own: // [p/moul/x/vm/bf](/p/moul/x/vm/bf/v0) is the machine, and // [p/moul/x/vm/vmkit](/p/moul/x/vm/vmkit/v0) is the host ABI, the fuel meter // and the instance store. // // What it exists to show is the thing gno realm code cannot do for itself: a // program that runs out of fuel does not fail, it pauses. The realm keeps the // snapshot, and the next caller pays for the next slice. Upload a program, // call Step a few times, and watch one computation finish across several // transactions. package bfdemo import ( "chain" "chain/runtime" "chain/runtime/unsafe" "time" "gno.land/p/moul/x/vm/bf/v0" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/ufmt/v0" ) // Caps. Everything a caller can grow is bounded, because all of it is storage // somebody pays a deposit on. const ( // MaxInstances is how many programs the realm keeps at once. Past it, // an upload has to wait for someone to remove one. MaxInstances = 64 // MaxOutput caps the bytes one program may write. Past it the guest is // trapped rather than truncated, so rendered output is never a lie. MaxOutput = 4096 // MaxInput caps the call input a program can be given. MaxInput = 1024 // DefaultFuel is the budget an upload gets when it asks for none, and // the slice size Step uses when asked for none. DefaultFuel = 100000 // MaxSliceFuel bounds one transaction's work regardless of what the // caller asked for. MaxSliceFuel = 5000000 ) var ( // store holds the instances: program, snapshot, status, accounting. store = vmkit.NewStore() // inputs holds each instance's call input, keyed by id. It is separate // from the instance so vmkit.Instance stays the VM-agnostic record it // is meant to be. inputs = avl.NewTree() idgen seqid.ID count int ) // host is the realm-backed [vmkit.Host]. One is built per call, wrapping the // instance being stepped, so a guest's output and authority are scoped to it // and nothing ambient leaks in. type host struct { inst *vmkit.Instance in []byte kv *avl.Tree overrun bool // the guest wrote past MaxOutput } func (h *host) Caller() address { return h.inst.Owner } func (h *host) Origin() address { return h.inst.Owner } func (h *host) Now() int64 { return time.Now().Unix() } func (h *host) Height() int64 { return runtime.ChainHeight() } func (h *host) Input() []byte { return h.in } // Get and Set are scoped to the instance by construction: the tree belongs to // the instance being stepped, so one program cannot reach another's storage // even though both live in this one realm. bf never calls them; they are here // because the ABI is the point. func (h *host) Get(key []byte) []byte { v := h.kv.Get(string(key)) if v == nil { return nil } return v.([]byte) } func (h *host) Set(key, val []byte) { h.kv.Set(string(key), val) } func (h *host) Output(p []byte) { if len(h.inst.Output)+len(p) > MaxOutput { h.overrun = true return } h.inst.Output = append(h.inst.Output, p...) } func (h *host) Emit(typ string, kv ...string) { chain.Emit(typ, kv...) } // Send is never granted here. The demo funds no instance, so a guest that // tries to move coins is refused. That is the capability rule doing its job, // not a missing feature. func (h *host) Send(to address, amount int64) error { return vmkit.ErrNotGranted } func (h *host) Log(msg string) {} // Upload compiles src and stores it as a new instance, returning its id. // // Compilation happens here rather than at the first Step, so an unbalanced // program is rejected by the transaction that submitted it instead of costing // somebody else the gas later. func Upload(cur realm, src, input string, budget int64) string { if count >= MaxInstances { panic("bfdemo: too many instances, remove one first") } if len(input) > MaxInput { panic("bfdemo: input too long") } if _, err := bf.CompileDefault(src); err != nil { panic("bfdemo: " + err.Error()) } if budget <= 0 { budget = DefaultFuel } id := idgen.Next().String() owner := unsafe.PreviousRealm().Address() store.Set(vmkit.NewInstance(id, owner, bf.VMName, []byte(src), budget)) if input != "" { inputs.Set(id, input) } count++ chain.Emit("bf_upload", "id", id, "bytes", ufmt.Sprintf("%d", len(src))) return id } // Step runs one slice of the instance: up to `fuel` guest ops, then stop and // keep the snapshot. Anyone may pay for a slice, not only the owner: a paused // program that only its owner can advance is a worse demo and no safer, since // the program and its budget were both fixed at upload. func Step(cur realm, id string, fuel int64) string { inst := store.Get(id) if inst == nil { panic("bfdemo: no such instance") } if inst.Status != vmkit.Running { panic("bfdemo: instance is " + inst.Status.String()) } if fuel <= 0 { fuel = DefaultFuel } if fuel > MaxSliceFuel { fuel = MaxSliceFuel } prog, err := bf.CompileDefault(string(inst.Program)) if err != nil { panic("bfdemo: " + err.Error()) } h := &host{inst: inst, in: []byte(inputOf(id)), kv: avl.NewTree()} if err := inst.Run(bf.NewMachine(prog), h, fuel); err != nil { panic("bfdemo: " + err.Error()) } if h.overrun { inst.Status = vmkit.Trapped inst.Trap = "output limit reached" } chain.Emit("bf_step", "id", id, "status", inst.Status.String(), "fuel", ufmt.Sprintf("%d", inst.FuelUsed), ) return inst.Status.String() } // Remove deletes an instance. Owner only. func Remove(cur realm, id string) { inst := store.Get(id) if inst == nil { panic("bfdemo: no such instance") } if inst.Owner != unsafe.PreviousRealm().Address() { panic("bfdemo: not your instance") } store.Remove(id) inputs.Remove(id) count-- } func inputOf(id string) string { v := inputs.Get(id) if v == nil { return "" } return v.(string) }
- #6bfdemo_test.gno
- #7package bfdemo import ( "strings" "testing" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/avl/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) const hello = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------." // resetState puts the realm globals back where init() left them. Realm state // persists for the whole test binary, so every test that asserts on ids or on // the rendered instance list has to start from here. func resetState() { store = vmkit.NewStore() inputs = avl.NewTree() idgen = seqid.ID(0) count = 0 } func TestUploadAndRunToCompletion(cur realm, t *testing.T) { resetState() alice := testutils.TestAddress("alice") testing.SetRealm(testing.NewUserRealm(alice)) id := Upload(cross(cur), hello, "", 0) uassert.Equal(t, "halted", Step(cross(cur), id, 0)) inst := store.Get(id) uassert.Equal(t, "Hello World", string(inst.Output)) uassert.Equal(t, int64(1), inst.Slices) uassert.Equal(t, alice, inst.Owner) } // TestOneProgramAcrossManyTransactions is what the realm exists to // demonstrate: the same computation, finished over several calls, each paying // for its own slice. func TestOneProgramAcrossManyTransactions(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) id := Upload(cross(cur), hello, "", 0) status := "" slices := 0 for slices < 500 { status = Step(cross(cur), id, 7) slices++ if status != "running" { break } } inst := store.Get(id) uassert.Equal(t, "halted", status) uassert.Equal(t, "Hello World", string(inst.Output)) uassert.True(t, inst.Slices > 1, "took more than one slice") } func TestInputIsReadThroughTheHost(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) // ",[.,]" echoes its input back. id := Upload(cross(cur), ",[.,]", "gno.land", 0) uassert.Equal(t, "halted", Step(cross(cur), id, 0)) uassert.Equal(t, "gno.land", string(store.Get(id).Output)) } func TestBudgetStopsAProgram(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) id := Upload(cross(cur), hello, "", 12) uassert.Equal(t, "out of fuel", Step(cross(cur), id, 0)) uassert.Equal(t, int64(12), store.Get(id).FuelUsed) // And it stays stopped rather than quietly continuing. uassert.AbortsContains(t, cur, "instance is out of fuel", func() { Step(cross(cur), id, 0) }) } func TestUploadRejectsAMalformedProgram(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) // Rejected by the transaction that submitted it, not by whoever pays // for the first slice later. uassert.AbortsContains(t, cur, "unmatched", func() { Upload(cross(cur), "+++[", "", 0) }) uassert.Equal(t, 0, count) } func TestRemoveIsOwnerOnly(cur realm, t *testing.T) { resetState() alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") testing.SetRealm(testing.NewUserRealm(alice)) id := Upload(cross(cur), hello, "", 0) testing.SetRealm(testing.NewUserRealm(bob)) uassert.AbortsContains(t, cur, "not your instance", func() { Remove(cross(cur), id) }) // Anyone may pay for a slice, though: the program and its budget were // both fixed at upload. uassert.Equal(t, "halted", Step(cross(cur), id, 0)) testing.SetRealm(testing.NewUserRealm(alice)) Remove(cross(cur), id) uassert.Equal(t, 0, count) uassert.True(t, store.Get(id) == nil) } func TestOutputIsCappedAndTrapped(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) // A cell set to 1 and printed forever: an infinite writer. id := Upload(cross(cur), "+[.]", "", 0) status := Step(cross(cur), id, 0) inst := store.Get(id) uassert.Equal(t, "trapped", status) uassert.Equal(t, "output limit reached", inst.Trap) uassert.Equal(t, MaxOutput, len(inst.Output)) } func TestRenderUnknownInstance(t *testing.T) { resetState() uassert.True(t, strings.Contains(Render("/nope"), "No such instance")) } // TestRenderEscapesGuestOutput pins the rule that a realm cannot be fixed in // place once it is live: everything a caller controls is escaped before it // reaches the page. func TestRenderEscapesGuestOutput(cur realm, t *testing.T) { resetState() testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("alice"))) // Print a backtick (96), which would otherwise close the code fence. src := strings.Repeat("+", 96) + "." id := Upload(cross(cur), src, "", 0) Step(cross(cur), id, 0) page := Render("/" + id) uassert.Equal(t, "`", string(store.Get(id).Output)) uassert.True(t, strings.Contains(page, "\\x60"), "the backtick is escaped") // The comment text in a program never reaches the page at all: only // the eight operators do. uassert.Equal(t, "+++[-]<>", operatorsOnly("+++ this is a comment [-] </script>")) }
- #8gnomod.toml
- #9module = "gno.land/r/moul/x/vm/bfdemo/v0" gno = "0.9" private = true
- #10render.gno
- #11package bfdemo import ( "strings" "gno.land/p/moul/kit/ui/v0" "gno.land/p/moul/x/vm/bf/v0" "gno.land/p/moul/x/vm/vmkit/v0" "gno.land/p/nt/ufmt/v0" ) // samples are the programs the home page offers, so the realm is usable // without writing Brainfuck by hand. var samples = []struct { name string src string input string note string }{ { "Hello World", "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.", "", "385 guest ops, five tape cells. Finishes in one slice.", }, { "Echo the input", ",[.,]", "gno.land", "Reads through Host.Input, which is what the naive interpreter could not do at all: it panicked on `,`.", }, { "Heavy loop", "++++++++++[->++++++++++[->++++++++++[-]<]<]", "", "About 111k guest ops. Give it 20000 fuel and it takes six slices, which is the point.", }, } // Render is the realm's gnoweb view. // // - "/" the samples, the instance list, and how the thing works. // - "/<id>" one instance: its program, status, fuel and output. func Render(path string) string { id := strings.TrimPrefix(path, "/") id = strings.TrimSpace(id) if id == "" { return renderHome() } return renderInstance(id) } func renderHome() string { var sb strings.Builder sb.WriteString("# Brainfuck, on chain, a slice at a time\n\n") sb.WriteString("Demo of [`p/moul/x/vm/bf`](/p/moul/x/vm/bf/v0) (the machine) and ") sb.WriteString("[`p/moul/x/vm/vmkit`](/p/moul/x/vm/vmkit/v0) (the host ABI, the fuel meter, ") sb.WriteString("the instance store). This realm holds no logic of its own.\n\n") sb.WriteString("A program here does not run to completion. It runs until its fuel slice is spent, ") sb.WriteString("then pauses: the realm keeps the snapshot and the next caller pays for the next slice. ") sb.WriteString("Realm code cannot pause itself, and a guest can.\n\n") sb.WriteString("## Samples\n\n") for _, s := range samples { sb.WriteString("**" + ui.Inline(s.name) + "**: " + s.note + "\n\n") sb.WriteString("```\n" + s.src + "\n```\n\n") sb.WriteString(ui.Action("Upload "+s.name, "Upload", "src", s.src, "input", s.input, "budget", "0") + "\n\n") } sb.WriteString("## Instances\n\n") t := ui.NewTable("id", "status", "fuel used", "slices", "output") store.ReverseIterate(func(i *vmkit.Instance) bool { t.Row( "["+i.ID+"](/r/moul/x/vm/bfdemo/v0:"+i.ID+")", i.Status.String(), ufmt.Sprintf("%d", i.FuelUsed), ufmt.Sprintf("%d", i.Slices), "`"+ui.Cell(printable(i.Output, 24))+"`", ) return false }) sb.WriteString(t.OrEmpty("No instances yet. Upload one of the samples above.")) sb.WriteString("\n\n") sb.WriteString("## Limits\n\n") lt := ui.NewTable("limit", "value") lt.Row("instances", ufmt.Sprintf("%d", MaxInstances)) lt.Row("source bytes", ufmt.Sprintf("%d", bf.MaxSource)) lt.Row("input bytes", ufmt.Sprintf("%d", MaxInput)) lt.Row("output bytes", ufmt.Sprintf("%d", MaxOutput)) lt.Row("fuel per slice", ufmt.Sprintf("%d", MaxSliceFuel)) lt.Row("tape cells", ufmt.Sprintf("%d", bf.TapeSize)) sb.WriteString(lt.String()) return sb.String() } func renderInstance(id string) string { var sb strings.Builder sb.WriteString("# Instance " + ui.Inline(id) + "\n\n") sb.WriteString("[← all instances](/r/moul/x/vm/bfdemo/v0)\n\n") inst := store.Get(id) if inst == nil { sb.WriteString("**No such instance.** It was never uploaded, or its owner removed it.\n") return sb.String() } t := ui.NewTable("field", "value") t.Row("owner", ui.Addr(inst.Owner)) t.Row("vm", ui.Cell(inst.VM)) t.Row("status", inst.Status.String()) if inst.Trap != "" { t.Row("trap", ui.Cell(inst.Trap)) } t.Row("fuel used", ufmt.Sprintf("%d", inst.FuelUsed)) t.Row("fuel budget", fuelText(inst.FuelBudget)) t.Row("slices", ufmt.Sprintf("%d", inst.Slices)) t.Row("source bytes", ufmt.Sprintf("%d", len(inst.Program))) t.Row("snapshot bytes", ufmt.Sprintf("%d", len(inst.Snapshot))) sb.WriteString(t.String()) sb.WriteString("\n") // The source is caller-supplied, and everything outside the eight // operators is a comment that may hold anything at all. Show the // program the machine actually compiled, which cannot contain a // backtick or a newline and so cannot break out of the fence. sb.WriteString("## Program\n\n```\n" + operatorsOnly(string(inst.Program)) + "\n```\n\n") sb.WriteString("## Output\n\n") if len(inst.Output) == 0 { sb.WriteString(ui.Empty("Nothing written yet.")) } else { sb.WriteString("```\n" + printable(inst.Output, MaxOutput) + "\n```\n") } sb.WriteString("\n") if inst.Status == vmkit.Running { sb.WriteString(ui.Action("Run 10000 more fuel", "Step", "id", id, "fuel", "10000")) sb.WriteString(" · ") sb.WriteString(ui.Action("Run 100000 more fuel", "Step", "id", id, "fuel", "100000")) sb.WriteString("\n") } else { sb.WriteString("This instance is **" + inst.Status.String() + "** and cannot be stepped again.\n") } return sb.String() } func fuelText(n int64) string { if n == vmkit.Unmetered { return "unmetered" } return ufmt.Sprintf("%d", n) } // operatorsOnly strips a program down to the eight characters the language // defines. Everything else is a comment by definition, and a comment is // attacker-controlled text that has no business reaching a renderer. func operatorsOnly(src string) string { var sb strings.Builder for i := 0; i < len(src); i++ { switch src[i] { case '+', '-', '<', '>', '[', ']', '.', ',': sb.WriteByte(src[i]) } } if sb.Len() == 0 { return "(no operators: this program does nothing)" } return sb.String() } // printable renders guest output for a code fence: printable ASCII as itself, // everything else as an escape. A guest writes arbitrary bytes, so this is // the only form of it that is safe to show. func printable(b []byte, max int) string { if len(b) == 0 { return "" } truncated := false if len(b) > max { b, truncated = b[:max], true } const hexDigits = "0123456789abcdef" var sb strings.Builder for _, c := range b { switch { case c == '\n': sb.WriteString("\\n") case c == '\t': sb.WriteString("\\t") case c == '\\': sb.WriteString("\\\\") case c == '`': sb.WriteString("\\x60") case c >= 0x20 && c < 0x7f: sb.WriteByte(c) default: sb.WriteString("\\x") sb.WriteByte(hexDigits[c>>4]) sb.WriteByte(hexDigits[c&0x0f]) } } if truncated { sb.WriteString("…") } return sb.String() }
Result log
msg:0,success:true,log:,events:[] msg:1,success:true,log:,events:[] msg:2,success:true,log:,events:[] msg:3,success:true,log:,events:[]