Zack Scholl

zack.scholl@gmail.com

gno -> go

 / #software 

Making things in web3 with a new programming language, gno.

I’ve developed dozens of websites using Go and recently I’ve been drawn by my curiosity into how website/app development would be done with “web3” technologies. Luckily, there is a new language called “Gno” that is lowering the barrier to entry for developing in web3.

what is gno?

Gno isn’t just another programming language; it’s an interpreted version of the Go language, optimized for the intricacies of blockchain and deterministic execution on distributed systems. If you’re familiar with Go, transitioning to Gno is a breeze.

Here are some key differentiators between Go and Gno:

The differences may seem minute, but they pave the way for powerful blockchain applications with unparalleled precision.

There is a lot more information on their main website at gno.land. Also you can just easily play with Gno code online too, at the play.gno.land website.

What is a Smart Contract?

“Smart contracts” are are immutable, self-executing programs living on the blockchain.

Beyond automating transactions, smart contracts can be used for various applications, such as creating incentivized social networks and reshaping interactions with the web (i.e., “web3”). Smart contracts written in Gno run within the Gno.land ecosystem.

What is Gno.land?

Gno.land is a platform for writing smart contracts in Gno, the first of a series of Gno Layer 1 chains. Built on Tendermint2, Cosmos/IBC, and secured by Proof of Contribution, Gno.land prioritizes simplicity, security, scalability, and transparency.

web3 Bytebeat Symphony

For my first attempt into web3 I wanted to make a simple, shareable system of music. Music could be encoded into the blockchain and it could be easily recorded (verifiably) that some piece of music was generated first by some identity on the chain. I went for bytebeat music because its very straightforward to code. bytebeat was discovered by viznut in 2011 as a way to type a very short computer programs that generate chiptune music, for example this is a bytebeat program:

1main(t){
2  for(;;t++) putchar(((t<<1)^((t<<1)+(t>>7)&t>>12)));
3}

which you can take the raw output of and convert to audio:

1gcc -o crowd crowd.c
2./crowd | head -c 4M > crowd.raw
3sox -r 8000 -c 1 -t u8 crowd.raw crowd.wav

simplicity, built-in

The thing that amazed me about the Gno language is how fast it is to get started. First of all, the language is basically Go, plus a few addons. Also its mostly “batteries included” (persistent globals = database, getting callers = identification). Here is, essentially, my entire “smart contract” for generating bytebeat music, in just 50 lines of code:

 1package bytebeat
 2
 3import (
 4	"std"
 5
 6	bytebeat "gno.land/p/demo/audio/bytebeat/v1"
 7	"gno.land/p/demo/ufmt"
 8)
 9
10type Comment struct {
11	User    string
12	Message string
13}
14
15var (
16	data     string
17	comments []Comment
18)
19
20func init() {
21	seconds := uint32(10)
22	data = bytebeat.ByteBeat(seconds, 8000, func(t int) int {
23		return (t>>10^t>>11)%5*((t>>14&3^t>>15&1)+1)*t%99 + ((3 + (t >> 14 & 3) - (t >> 16 & 1)) / 3 * t % 99 & 64)
24	})
25}
26
27func AddComment(s string) string {
28	caller := std.GetOrigCaller() // main
29	comments = append(comments, Comment{
30		User:    string(caller),
31		Message: s,
32	})
33	return "ok"
34}
35
36func Render(path string) string {
37	output := ""
38	output += "\n"
39	output += "# my bytebeat\n"
40	output += `<audio controls="controls" autobuffer="autobuffer" autoplay="autoplay">
41<source src="data:audio/wav;base64,`
42	output += data
43	output += `" />
44</audio>`
45	output += "\n\n## comments\n"
46	for i, comment := range comments {
47		output += ufmt.Sprintf("%d. *%s* - %s\n ", i, comment.Message, comment.User)
48	}
49	return output
50}

There are some clever built-in functions here, like the Render function is automatically used by gno.land to do web rendering - so in addition to a smart contract you get a web server for free.

This smart contract is not yet totally “in action” yet because there is no main net to deliver it. But it is really straightforward and easy to setup your own Gno.land server to run smart contracts. I wrote a whole presentation on the topic which you can read more about here.

Microblogging in the GnoVerse

For another attempt into web3 I was interested in making a Twitter-like thing - essentially a way to “microblog”. Again, my smart contract for this package is extremely concise:

 1package microblog
 2
 3import (
 4	"std"
 5
 6	"gno.land/p/demo/microblog"
 7	"gno.land/r/demo/users"
 8)
 9
10var (
11	title  = "gno-based microblog"
12	prefix = "/r/demo/microblog:"
13	m      *microblog.Microblog
14)
15
16func init() {
17	m = microblog.NewMicroblog(title, prefix)
18}
19
20// Render calls the microblog renderer
21func Render(path string) string {
22	return m.Render(path)
23}
24
25// NewPost takes a single argument (post markdown) and
26// adds a post to the address of the caller.
27func NewPost(text string) string {
28	if err := m.NewPost(text); err != nil {
29		return "unable to add new post"
30	}
31	return "added new post"
32}
33
34func Register(name, profile string) string {
35	caller := std.GetOrigCaller() // main
36	users.Register(caller, name, profile)
37	return "OK"
38}

A lot of work is being done behind the scenes here with some “packages”. There is a difference between “Packages” and “Realms” in Gno - where both are like Go packages in that they are modular and importable, but “Realms” have the special ability that they persist globals, as they are meant to be run on the block chain.

The package for the microblog is the most important and uses a lot of the cool Gno features. There is a AVL tree for managing data and some user management properties built into the language:

  1package microblog
  2
  3import (
  4	"errors"
  5	"sort"
  6	"std"
  7	"strings"
  8	"time"
  9
 10	"gno.land/p/demo/avl"
 11	"gno.land/p/demo/ufmt"
 12	"gno.land/r/demo/users"
 13)
 14
 15var (
 16	ErrNotFound    = errors.New("not found")
 17	StatusNotFound = "404"
 18)
 19
 20type Microblog struct {
 21	Title  string
 22	Prefix string   // i.e. r/gnoland/blog:
 23	Pages  avl.Tree // author (string) -> Page
 24}
 25
 26func NewMicroblog(title string, prefix string) (m *Microblog) {
 27	return &Microblog{
 28		Title:  title,
 29		Prefix: prefix,
 30		Pages:  avl.Tree{},
 31	}
 32}
 33
 34func (m *Microblog) GetPages() []*Page {
 35	var (
 36		pages = make([]*Page, m.Pages.Size())
 37		index = 0
 38	)
 39
 40	m.Pages.Iterate("", "", func(key string, value interface{}) bool {
 41		pages[index] = value.(*Page)
 42		index++
 43		return false
 44	})
 45
 46	sort.Sort(byLastPosted(pages))
 47
 48	return pages
 49}
 50
 51func (m *Microblog) RenderHome() string {
 52	output := ufmt.Sprintf("# %s\n\n", m.Title)
 53	output += "# pages\n\n"
 54
 55	for _, page := range m.GetPages() {
 56		if u := users.GetUserByAddress(page.Author); u != nil {
 57			output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name(), page.Author.String(), m.Prefix, page.Author.String())
 58		} else {
 59			output += ufmt.Sprintf("- [%s](%s%s)\n", page.Author.String(), m.Prefix, page.Author.String())
 60		}
 61	}
 62
 63	return output
 64}
 65
 66func (m *Microblog) RenderUser(user string) string {
 67	silo, found := m.Pages.Get(user)
 68	if !found {
 69		return StatusNotFound
 70	}
 71
 72	return (silo.(*Page)).String()
 73}
 74
 75func (m *Microblog) Render(path string) string {
 76	parts := strings.Split(path, "/")
 77
 78	isHome := path == ""
 79	isUser := len(parts) == 1
 80
 81	switch {
 82	case isHome:
 83		return m.RenderHome()
 84
 85	case isUser:
 86		return m.RenderUser(parts[0])
 87	}
 88
 89	return StatusNotFound
 90}
 91
 92func (m *Microblog) NewPost(text string) error {
 93	author := std.GetOrigCaller()
 94	_, found := m.Pages.Get(author.String())
 95	if !found {
 96		// make a new page for the new author
 97		m.Pages.Set(author.String(), &Page{
 98			Author:    author,
 99			CreatedAt: time.Now(),
100		})
101	}
102
103	page, err := m.GetPage(author.String())
104	if err != nil {
105		return err
106	}
107	return page.NewPost(text)
108}
109
110func (m *Microblog) GetPage(author string) (*Page, error) {
111	silo, found := m.Pages.Get(author)
112	if !found {
113		return nil, ErrNotFound
114	}
115	return silo.(*Page), nil
116}
117
118type Page struct {
119	ID         int
120	Author     std.Address
121	CreatedAt  time.Time
122	LastPosted time.Time
123	Posts      avl.Tree // time -> Post
124}
125
126// byLastPosted implements sort.Interface for []Page based on
127// the LastPosted field.
128type byLastPosted []*Page
129
130func (a byLastPosted) Len() int           { return len(a) }
131func (a byLastPosted) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
132func (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }
133
134func (p *Page) String() string {
135	o := ""
136	if u := users.GetUserByAddress(p.Author); u != nil {
137		o += ufmt.Sprintf("# [%s](/r/demo/users:%s)\n\n", u.Name(), u.Name())
138		o += ufmt.Sprintf("%s\n\n", u.Profile())
139	}
140	o += ufmt.Sprintf("## [%s](/r/demo/microblog:%s)\n\n", p.Author, p.Author)
141
142	o += ufmt.Sprintf("joined %s, last updated %s\n\n", p.CreatedAt.Format("2006-02-01"), p.LastPosted.Format("2006-02-01"))
143	o += "## feed\n\n"
144	for _, u := range p.GetPosts() {
145		o += u.String() + "\n\n"
146	}
147	return o
148}
149
150func (p *Page) NewPost(text string) error {
151	now := time.Now()
152	p.LastPosted = now
153	p.Posts.Set(ufmt.Sprintf("%s%d", now.Format(time.RFC3339), p.Posts.Size()), &Post{
154		ID:        p.Posts.Size(),
155		Text:      text,
156		CreatedAt: now,
157	})
158	return nil
159}
160
161func (p *Page) GetPosts() []*Post {
162	posts := make([]*Post, p.Posts.Size())
163	i := 0
164	p.Posts.ReverseIterate("", "", func(key string, value interface{}) bool {
165		postParsed := value.(*Post)
166		posts[i] = postParsed
167		i++
168		return false
169	})
170	return posts
171}
172
173// Post lists the specific update
174type Post struct {
175	ID        int
176	CreatedAt time.Time
177	Text      string
178}
179
180func (p *Post) String() string {
181	return "> " + strings.ReplaceAll(p.Text, "\n", "\n>\n>") + "\n>\n> *" + p.CreatedAt.Format(time.RFC1123) + "*"
182}

Its a bit more code, but having a full-featured microblog with users and posts and persistent data, with only a Gno standard library is pretty amazing to me.

There is a lot more information about this project that you can dive into here where you can also read aobut how to spin up your own server.

more about gno

Gno provides Go developers with a smooth transition into the world of smart contracts. With a syntax close to Go and a platform like Gno.land, developing decentralized applications becomes accessible. While Gno introduces some unique features and limitations, its potential for web3 development is promising.

tinker / #software 

habitus      sd cards