gno -> go

gno -> go

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

gno -> go

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:

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

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

gcc -o crowd crowd.c
./crowd | head -c 4M > crowd.raw
sox -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:

package bytebeat

import (
	"std"

	bytebeat "gno.land/p/demo/audio/bytebeat/v1"
	"gno.land/p/demo/ufmt"
)

type Comment struct {
	User    string
	Message string
}

var (
	data     string
	comments []Comment
)

func init() {
	seconds := uint32(10)
	data = bytebeat.ByteBeat(seconds, 8000, func(t int) int {
		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)
	})
}

func AddComment(s string) string {
	caller := std.GetOrigCaller() // main
	comments = append(comments, Comment{
		User:    string(caller),
		Message: s,
	})
	return "ok"
}

func Render(path string) string {
	output := ""
	output += "\n"
	output += "# my bytebeat\n"
	output += `<audio controls="controls" autobuffer="autobuffer" autoplay="autoplay">
<source src="data:audio/wav;base64,`
	output += data
	output += `" />
</audio>`
	output += "\n\n## comments\n"
	for i, comment := range comments {
		output += ufmt.Sprintf("%d. *%s* - %s\n ", i, comment.Message, comment.User)
	}
	return output
}

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:

package microblog

import (
	"std"

	"gno.land/p/demo/microblog"
	"gno.land/r/demo/users"
)

var (
	title  = "gno-based microblog"
	prefix = "/r/demo/microblog:"
	m      *microblog.Microblog
)

func init() {
	m = microblog.NewMicroblog(title, prefix)
}

// Render calls the microblog renderer
func Render(path string) string {
	return m.Render(path)
}

// NewPost takes a single argument (post markdown) and
// adds a post to the address of the caller.
func NewPost(text string) string {
	if err := m.NewPost(text); err != nil {
		return "unable to add new post"
	}
	return "added new post"
}

func Register(name, profile string) string {
	caller := std.GetOrigCaller() // main
	users.Register(caller, name, profile)
	return "OK"
}

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:

package microblog

import (
	"errors"
	"sort"
	"std"
	"strings"
	"time"

	"gno.land/p/demo/avl"
	"gno.land/p/demo/ufmt"
	"gno.land/r/demo/users"
)

var (
	ErrNotFound    = errors.New("not found")
	StatusNotFound = "404"
)

type Microblog struct {
	Title  string
	Prefix string   // i.e. r/gnoland/blog:
	Pages  avl.Tree // author (string) -> Page
}

func NewMicroblog(title string, prefix string) (m *Microblog) {
	return &Microblog{
		Title:  title,
		Prefix: prefix,
		Pages:  avl.Tree{},
	}
}

func (m *Microblog) GetPages() []*Page {
	var (
		pages = make([]*Page, m.Pages.Size())
		index = 0
	)

	m.Pages.Iterate("", "", func(key string, value interface{}) bool {
		pages[index] = value.(*Page)
		index++
		return false
	})

	sort.Sort(byLastPosted(pages))

	return pages
}

func (m *Microblog) RenderHome() string {
	output := ufmt.Sprintf("# %s\n\n", m.Title)
	output += "# pages\n\n"

	for _, page := range m.GetPages() {
		if u := users.GetUserByAddress(page.Author); u != nil {
			output += ufmt.Sprintf("- [%s (%s)](%s%s)\n", u.Name(), page.Author.String(), m.Prefix, page.Author.String())
		} else {
			output += ufmt.Sprintf("- [%s](%s%s)\n", page.Author.String(), m.Prefix, page.Author.String())
		}
	}

	return output
}

func (m *Microblog) RenderUser(user string) string {
	silo, found := m.Pages.Get(user)
	if !found {
		return StatusNotFound
	}

	return (silo.(*Page)).String()
}

func (m *Microblog) Render(path string) string {
	parts := strings.Split(path, "/")

	isHome := path == ""
	isUser := len(parts) == 1

	switch {
	case isHome:
		return m.RenderHome()

	case isUser:
		return m.RenderUser(parts[0])
	}

	return StatusNotFound
}

func (m *Microblog) NewPost(text string) error {
	author := std.GetOrigCaller()
	_, found := m.Pages.Get(author.String())
	if !found {
		// make a new page for the new author
		m.Pages.Set(author.String(), &Page{
			Author:    author,
			CreatedAt: time.Now(),
		})
	}

	page, err := m.GetPage(author.String())
	if err != nil {
		return err
	}
	return page.NewPost(text)
}

func (m *Microblog) GetPage(author string) (*Page, error) {
	silo, found := m.Pages.Get(author)
	if !found {
		return nil, ErrNotFound
	}
	return silo.(*Page), nil
}

type Page struct {
	ID         int
	Author     std.Address
	CreatedAt  time.Time
	LastPosted time.Time
	Posts      avl.Tree // time -> Post
}

// byLastPosted implements sort.Interface for []Page based on
// the LastPosted field.
type byLastPosted []*Page

func (a byLastPosted) Len() int           { return len(a) }
func (a byLastPosted) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a byLastPosted) Less(i, j int) bool { return a[i].LastPosted.After(a[j].LastPosted) }

func (p *Page) String() string {
	o := ""
	if u := users.GetUserByAddress(p.Author); u != nil {
		o += ufmt.Sprintf("# [%s](/r/demo/users:%s)\n\n", u.Name(), u.Name())
		o += ufmt.Sprintf("%s\n\n", u.Profile())
	}
	o += ufmt.Sprintf("## [%s](/r/demo/microblog:%s)\n\n", p.Author, p.Author)

	o += ufmt.Sprintf("joined %s, last updated %s\n\n", p.CreatedAt.Format("2006-02-01"), p.LastPosted.Format("2006-02-01"))
	o += "## feed\n\n"
	for _, u := range p.GetPosts() {
		o += u.String() + "\n\n"
	}
	return o
}

func (p *Page) NewPost(text string) error {
	now := time.Now()
	p.LastPosted = now
	p.Posts.Set(ufmt.Sprintf("%s%d", now.Format(time.RFC3339), p.Posts.Size()), &Post{
		ID:        p.Posts.Size(),
		Text:      text,
		CreatedAt: now,
	})
	return nil
}

func (p *Page) GetPosts() []*Post {
	posts := make([]*Post, p.Posts.Size())
	i := 0
	p.Posts.ReverseIterate("", "", func(key string, value interface{}) bool {
		postParsed := value.(*Post)
		posts[i] = postParsed
		i++
		return false
	})
	return posts
}

// Post lists the specific update
type Post struct {
	ID        int
	CreatedAt time.Time
	Text      string
}

func (p *Post) String() string {
	return "> " + strings.ReplaceAll(p.Text, "\n", "\n>\n>") + "\n>\n> *" + p.CreatedAt.Format(time.RFC1123) + "*"
}

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.