cft

Build A Proof of Stake Blockchain in Go

A blockchain technology that’s on the rise.


user

Israel Miles

3 years ago | 14 min read

The field of blockchain is continuing to explode into 2021. The exponential growth in cryptocurrencies is matched only by the adoption of blockchain solutions in both private and public applications.

Market analysis has predicted that the global blockchain market size will explode from $3.0 billion USD in 2020 to $39.7 billion by 2025, at an effective Compound Annual Growth Rate (CAGR) of 67.3% during 2020–2025.

For your daily basic economics reminder, that growth rate is an averaged value encompassing the entire market. Imagine how the real winners in this space are going to perform.

Furthermore, the 67% CAGR is irrespective of the monumental gains of the cryptocurrency space with a global market cap of $2 trillion and growing.

From CoinMarketCap

Imagine if you had been buying during the trade volume increase between January and July of 2020. Thing is, it’s obviously possible — but you need to understand the market and where it’s headed.

With the revolution that blockchain is bringing to world markets, it’s critical to understand the fundamentals before you make future predictions. In this article, we are going to explore the fundamentals of Proof of Stake, a blockchain protocol that resembles a lottery method for forging new blocks in the blockchain.

The primary goals of this article are as follows:

  1. Understand current performance trends of the blockchain field (done).
  2. Learn Proof of Stake through a working example in GoLang.
  3. Level Up your computer science and Go programming skills.

This will be a fun one for Hodlers and Gophers alike, let’s code!

Understanding Proof of Stake

We will keep this section short, as the fundamentals of Proof of Stake (PoS) are actually fairly simple. Of course, the core component of this system is the blockchain itself. Put simply, a blockchain is an immutable ledger where each individual block is cryptographically built from the blocks previous before it. You can never change any piece of the blockchain, as everyone connected to the network will easily see the changes and refute your version of the blockchain.

The process of creating new blocks is defined by your blockchain protocol.

Bitcoin is built off of a Proof of Work (PoW) protocol where an increasing amount of computational power is required to verify previous transactions through a mathematical process. Each time you verify a list of transactions contained within a block, you are given a reward in the form of bitcoin.

Thus, the proof of your transaction history is in the amount of work you put in — with those completing this work being known as “miners”. A growing issue with PoW is the monumental amounts of computing power required to solve these mathematical puzzles as time goes on.

Not just that, but if any single entity obtains more than 51% of the computing power, they could theoretically diverge the blockchain to be whatever they want it to be (this is known as a 51% attack).

Proof of Stake is fundamentally different. Instead of computational power to verify and expand the blockchain, you “stake” an amount of tokens (not necessarily a cryptocurrency) on the blockchain network. This is normally done by creating your own “node” that facilitates you participating in the blockchain ecosystem. A node could be your own computer, or a network of computers.

PoW versus PoS

The details don’t really matter here, the core idea is that if your node is doing honest work, you will be given a higher chance of forging a new block in the blockchain and given a reward in addition to your original stake back. The probability of being chosen to forge the next block also increases in some proportion to the amount of tokens that you stake on the network.

Made with LucidChart

However, if you perform dishonest actions, your stake could be penalized or even withdrawn completely. This reward and penalization method is meant to promote honest work in the blockchain without the computing scalability bottleneck that is associated with Proof of Work.

Now that we’ve got a conceptual idea of PoS versus PoW, let’s move onto coding a working PoS example in Go.

Proof of Stake in Go

Let’s face it, if I had built a production-grade Proof of Stake blockchain, I don’t think a single blog post would suffice in explaining it. This is going to be a simple but functional example to expand upon, and I hope you’ll enjoy it as much as I have while building it.

That being said, there is still a fair amount of complexity here so we are going to take it piece by piece. If you make it through, I’m sure you’ll be able to create a new iteration of your own PoS blockchain, probably better than mine! :-)

Import “blockchain”

First we need to include some Go packages we will need in the project in addition to defining our custom object types. Here are the packages you’ll need — we’ll be using math/rand, crypto/sha256 and encoding/hex for our cryptographic blockchain methods. Of course, errors is a standard in Go!

package main

import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log"
math "math/rand"
"time"
)

Next comes our custom data types. Go makes this super easy with the use of structs. Here we have three custom types, the first being PoSNetwork where we have a Blockchain field that is an array of references to instances of the Block struct.

We will keep track of the most recently added block via the BlockchainHead field, and we will also have an array of references to the Node struct to act as Validators.

type PoSNetwork struct {
Blockchain []*Block
BlockchainHead *Block
Validators []*Node
}

type Node struct {
Stake int
Address string
}

type Block struct {
Timestamp string
PrevHash string
Hash string
ValidatorAddr string
}

The Node struct will have a Stake field that will represent the amount of tokens it is adding to the network. The Address will be a randomly generated string so that we can keep track of which Node successfully validated the next block.

Finally, the Block struct will contain the information we need to keep track of our blockchain. We will have a Timestamp for when the block was created, a previous hash from the previous block, the Hash representing itself, and the address of the Node that validated this block — all type string.

Build a blockchain brick by block

Here’s our first method to build the blockchain — let’s break it down. First in our function signature, we attach this method to the PoSNetwork struct and reference that struct as n. Then, we take a reference to a Node as the only parameter. We will return a new array of Block references to represent the new Blockchain, a reference to a Block to be the new BlockchainHead, and a possible error if things get hairy.

You’ll notice that right away we call the method ValidateBlockchain() before we even try to add anything. We’ll get to validation later, but just know that there is logic to penalize a Node if we find our Blockchain to be changed.

func (n PoSNetwork) GenerateNewBlock(Validator *Node) ([]*Block, *Block, error) {
if err := n.ValidateBlockchain(); err != nil {
Validator.Stake -= 10
return n.Blockchain, n.BlockchainHead, err
}

currentTime := time.Now().String()

newBlock := &Block {
Timestamp: currentTime,
PrevHash: n.BlockchainHead.Hash,
Hash: NewBlockHash(n.BlockchainHead),
ValidatorAddr: Validator.Address,
}

if err := n.ValidateBlockCandidate(newBlock); err != nil {
Validator.Stake -= 10
return n.Blockchain, n.BlockchainHead, err
} else {
n.Blockchain = append(n.Blockchain, newBlock)
}
return n.Blockchain, newBlock, nil
}

After checking if our Blockchain is in tact, we get the current time of the system store it as Timestamp upon instantiation of a new Block. We also attach the Hash of the previous hash which we have easy access to thanks to BlockchainHead. We will then call the method NewBlockHash() on BlockchainHead, and assign the address of the input Node as our validator address.

Once the fields of the new Block have been filled, we call ValidateBlockCandidate() on the new Block and see if there are any errors. If there are, we penalize the culprit Node and return an error. If everything went fine, we append the new block to our Blockchain. The return statement on line 22 is just a default if we didn’t catch an error.

But how does NewBlockHash() work you ask? Well, thankfully there’s not much going on here. We have two functions to accomplish the task of creating a unique Hash for each Block. The function NewBlockHash() simply takes all of the info of the Block and concatenates it into a single string to be passed to newHash().

func NewBlockHash(block *Block) string {
blockInfo := block.Timestamp + block.PrevHash + block.Hash + block.ValidatorAddr
return newHash(blockInfo)
}

func newHash(s string) string {
h := sha256.New()
h.Write([]byte(s))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
}

Next, newHash() will leverage the crypto/sha256 package to create a new SHA256 object stored as h. We then convert the input string s into a byte array and write that into h. Finally we call h.Sum() to get h into a format where we can call hex.EncodeToString() so that we have a string as our final output.

Still following me? Good!

Validating our blockchain

A blockchain is of no use if you can’t verify it. Here we attach the ValidateBlockchain() method to our PoSNetwork struct and return a possible error. If the blockchain is empty or just has a single block, we have no way to make sure it’s correct so we just return nil for the error.

Next we evaluate three separate conditions between every pair of blocks in our blockchain. The first check is if the Hash of the previous Block is equal to what the current Block has stored for it’s previous Hash.

func (n PoSNetwork) ValidateBlockchain() error {
if len(n.Blockchain) <= 1 {
return nil
}

currBlockIdx := len(n.Blockchain)-1
prevBlockIdx := len(n.Blockchain)-2

for prevBlockIdx >= 0 {
currBlock := n.Blockchain[currBlockIdx]
prevBlock := n.Blockchain[prevBlockIdx]
if currBlock.PrevHash != prevBlock.Hash {
return errors.New("blockchain has inconsistent hashes")
}

if currBlock.Timestamp <= prevBlock.Timestamp {
return errors.New("blockchain has inconsistent timestamps")
}

if NewBlockHash(prevBlock) != currBlock.Hash {
return errors.New("blockchain has inconsistent hash generation")
}
currBlockIdx--
prevBlockIdx--
}
return nil
}

We also check to see if at any point a previous Block’s Timestamp is newer than the current Block. If the current Block was made in 2020, but the previous Block was made in 2021, you know something is wrong.

Lastly we check that if we directly calculate the Hash of the previous Block, that we still get back the Hash of the current Block. If any of these conditions hold, then we return an error since our blockchain is in a tampered state.

Now that we’ve validated the entire blockchain, we need to make sure that the next Block to add is also valid. This follows the same condition checking as above, only for a single new block to be added.

func (n PoSNetwork) ValidateBlockCandidate(newBlock *Block) error {
if n.BlockchainHead.Hash != newBlock.PrevHash {
return errors.New("blockchain HEAD hash is not equal to new block previous hash")
}

if n.BlockchainHead.Timestamp >= newBlock.Timestamp {
return errors.New("blockchain HEAD timestamp is greater than or equal to new block timestamp")
}

if NewBlockHash(n.BlockchainHead) != newBlock.Hash {
return errors.New("new block hash of blockchain HEAD does not equal new block hash")
}
return nil
}

Winner winner, token dinner

Alright, now we’ve gone through the steps of adding new blocks to our blockchain as well as validating it’s correctness. So how do we decide when to add new blocks? Well, this is where our validators come into play. For each Node that has a stake in the network,

we will go through a lottery method to randomly choose one of the nodes to forge the next block in our blockchain and receive a reward.

But first, we need a Node in the first place. To add a new Node to our PoSNetwork, we call NewNode() which takes in the initial stake of the Node and returns a new array of Node references. Nothing fancy going on here, we just append to our n.Validators array and call randAddress() to generate a unique address for the new Node.

func (n PoSNetwork) NewNode(stake int) []*Node {
newNode := &Node{
Stake: stake,
Address: randAddress(),
}
n.Validators = append(n.Validators, newNode)
return n.Validators
}

func randAddress() string {
b := make([]byte, 16)
_, _ = math.Read(b)
return fmt.Sprintf("%x", b)
}

So how do we actually pick a winner? With a little probability and statistics! In the SelectWinner() method, we first find the total stake that is held within the network by ranging over n.Validators. We also add any nodes with a stake greater than zero to the array winnerPool for possible selection.

If we find winnerPool to be empty, we return an error. Nobody likes a broke gambler. Then we select a winning number using the Intn() method which will pick a random number between 0 and our total stake.

func (n PoSNetwork) SelectWinner() (*Node, error) {
var winnerPool []*Node
totalStake := 0
for _, node := range n.Validators {
if node.Stake > 0 {
winnerPool = append(winnerPool, node)
totalStake += node.Stake
}
}
if winnerPool == nil {
return nil, errors.New("there are no nodes with stake in the network")
}
winnerNumber := math.Intn(totalStake)
tmp := 0
for _, node := range n.Validators {
tmp += node.Stake
if winnerNumber < tmp {
return node, nil
}
}
return nil, errors.New("a winner should have been picked but wasn't")
}

The last part is where probability comes into play. In order to keep each node having a chance of winning that is proportional to it’s total Stake in the network, we incrementally add the Stake of the current Node to the tmp variable. If at any point the winning number is less than tmp, that Node is selected as our winner.

How does that work out? Well, it’s not too complicated. Let’s say the winning number is 45, and we have four nodes with 10, 30, 20, and 40 tokens staked. If we keep track of the accumulated value of the nodes and compare if the winning number is less than that accumulation, we will have distributed the odds of winning into proportional “buckets”.

Made with LucidChart

Look at the image above, what probability is there that the random winning number will land in that first 10 token bucket? Well, 10/(10+30+20+40) = 10/100 = 10%.

It’s proportional! The for loop with the accumulated sum tmp just allows each node’s stake to be spread out into buckets. The larger your stake, the larger your bucket and thus the larger your odds of winning and forging a new block.

Bringing it all together

All we need now is to tie together our functions and data types. We do all the good stuff in our main() function. The first step is to set a random seed with the current time as our input. Do NOT use time as an input to a random seed as it can actually introduce a security vulnerability in decoding your hash output.

In this example, we need to instantiate a new Proof of Stake network with what’s known as the Genesis block, a.k.a. the first block in the blockchain. Once we do so, we also set the network’s BlockchainHead equal to that first Block.

func main() {
// set random seed
math.Seed(time.Now().UnixNano())

// generate an initial PoS network including a blockchain with a genesis block.
genesisTime := time.Now().String()
pos := &PoSNetwork{
Blockchain: []*Block{
{
Timestamp: genesisTime,
PrevHash: "",
Hash: newHash(genesisTime),
ValidatorAddr: "",
},
},
}
pos.BlockchainHead = pos.Blockchain[0]

// instantiate nodes to act as validators in our network
pos.Validators = pos.NewNode(60)
pos.Validators = pos.NewNode(40)

// build 5 additions to the blockchain
for i := 0; i < 5; i++ {
winner, err := pos.SelectWinner()
if err != nil {
log.Fatal(err)
}
winner.Stake += 10
pos.Blockchain, pos.BlockchainHead, err = pos.GenerateNewBlock(winner)
if err != nil {
log.Fatal(err)
}
fmt.Println("Round ", i)
fmt.Println("\tAddress:", pos.Validators[0].Address, "-Stake:", pos.Validators[0].Stake)
fmt.Println("\tAddress:", pos.Validators[1].Address, "-Stake:", pos.Validators[1].Stake)
}

pos.PrintBlockchainInfo()
}

Then, we add two Nodes to the network to act as validators with 60 and 40 tokens as their initial Stake. For five iterations we will select a new winner for our Blockchain and crash our program if there’s any errors — because prototyping. We pass the newly selected winner to generate a new Block, and print out the total Stake of each Node for each round.

Wonder why I have return types for all of these functions and don’t just add them directly to the PoSNetwork struct? Well, Go doesn’t like you doing that — so you actually have to return the newly constructed arrays or objects back into the initial function call. A headache I would love a better solution to.

At the end of all this, we will print out our newly crafted Blockchain. Here’s an example run:

$ go run main.go
Round 0
Address: f8d44bb083078de97b8428f4f9548130 -Stake: 70
Address: de6ae18584f02b3388569191a04a4b4a -Stake: 40
Round 1
Address: f8d44bb083078de97b8428f4f9548130 -Stake: 70
Address: de6ae18584f02b3388569191a04a4b4a -Stake: 50
Round 2
Address: f8d44bb083078de97b8428f4f9548130 -Stake: 80
Address: de6ae18584f02b3388569191a04a4b4a -Stake: 50
Round 3
Address: f8d44bb083078de97b8428f4f9548130 -Stake: 90
Address: de6ae18584f02b3388569191a04a4b4a -Stake: 50
Round 4
Address: f8d44bb083078de97b8428f4f9548130 -Stake: 100
Address: de6ae18584f02b3388569191a04a4b4a -Stake: 50
Block 0 Info:
Timestamp: 2021-04-12 MDT m=+0.000120025
Previous Hash:
Hash: c5d04de14efed52ce84889c6382f9d307d5b98093d93a84b419478
Validator Address:
Block 1 Info:
Timestamp: 2021-04-12 MDT m=+0.000277288
Previous Hash: c5d04de14efed52ce84889c6382f9d307d5b98093d93a
Hash: d58e90a75b71ac62ef938fc0148314a7f864ad50bd702f959e2d27
Validator Address: f8d44bb083078de97b8428f4f9548130
Block 2 Info:
Timestamp: 2021-04-12 MDT m=+0.000306562
Previous Hash: d58e90a75b71ac62ef938fc0148314a7f864ad50bd702
Hash: e6bfdd6c2c869607e2d9a81b84ddf4478756fedff78a03746cde11
Validator Address: de6ae18584f02b3388569191a04a4b4a
Block 3 Info:
Timestamp: 2021-04-12 MDT m=+0.000321755
Previous Hash: e6bfdd6c2c869607e2d9a81b84ddf4478756fedff78a0
Hash: 8e3dbacc4a610b1665658bc9e7238963eda0d5bbbf3ce809e8fa6e
Validator Address: f8d44bb083078de97b8428f4f9548130
Block 4 Info:
Timestamp: 2021-04-12 MDT m=+0.000333024
Previous Hash: 8e3dbacc4a610b1665658bc9e7238963eda0d5bbbf3ce
Hash: 22760f8deb96c354a4050a3c48741be062bccfa9c51571c170065a
Validator Address: f8d44bb083078de97b8428f4f9548130
Block 5 Info:
Timestamp: 2021-04-12 MDT m=+0.000347521
Previous Hash: 22760f8deb96c354a4050a3c48741be062bccfa9c5157
Hash: d2a5047f7d8a7696c1d0fb9ec49b56d2e71bbcedaaebc83a18b7a5
Validator Address: f8d44bb083078de97b8428f4f9548130

What’s Next

I can already hear it, your example isn’t decentralized! This is weak verification! You have to do everything yourself! Blah blah blah.

Look, if you see something in this example that you think can be done better — do it. And when you build a better and cooler blockchain than this one, write an article and tell me about it! If you can find ways to improve upon this example while sharing that knowledge to community, I see that as nothing but a good thing.

If you’re wondering what can be done next to build upon this example, do I have news for you. Here’s a short list for a few ideas:

  • Testing — You’ll notice I didn’t include any tests, and that’s sacrilege in modern software engineering! Write some basic unit tests and make sure my example passes at least a *few* of them.
  • Concurrency — This is all ran in a single thread, which is never gonna happen in the real world. Try hosting a single TCP server for the main thread, and have connecting nodes concurrently try to add to the blockchain.
  • Decentralization — Can you make a PoS that doesn’t run on a single computer, and promotes being owned by a network?
  • Refactoring — This code could simply use some cleanup to make it more efficient, elegant and concise. Try to make your own PoS example that’s more easily understood than mine!
  • Weighting — What if a single node starts to control the network with a massive stake? Could you exponentially average the weights, or is there a better alternative?
  • Tokenization — How can you make transactions run on this network? How can token rewards for new blocks diminish over time so as to prevent inflation?

Whatever you do, I hope this article taught you something new whether it be about Blockchain, Go, or just some programming fun. Whatever your thoughts in this article, I’d love to hear them in the comments below! Thanks for reading.

Originally published here.

Upvote


user
Created by

Israel Miles

I like to write about science, math, technology and how to be a more productive professional.


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles