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:
- Special Packages: Gno introduces specialized packages like std for accessing the caller’s address and
avl.Tree
for a deterministic map. - Deterministic Design: Gno execution is verifiable and is capable of running on distributed systems.
- Transpilation Magic: Under the hood, Gno code is transpiled into Go, leveraging the robust Go compiler system.
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:
which you can take the raw output of and convert to audio:
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.
- Examples galore: continue exploring with the dozens of examples of Gno.
- Getting started: more information on getting started with Gno.
- Community creations: checkout what people are building in gno.
- Knoweldge hub: read previous talks about Gno.
- Enter the conversation: join the discord.