This article's mirror link: Building a UTXO-based Blockchain from Scratch with Golang (Part 2: Project Refactoring + PoW)
Introduction#
In the previous chapter, we learned what a block is and the relationship between blocks and blockchains. In this chapter, we will refactor this project, expand the header information of the block, and explain how blocks are legitimately added to the blockchain through consensus mechanisms.
Project Refactoring#
In the previous chapter, all our code was written in main.go, which is clearly not conducive to continuing to build the project. We hope that main.go will only be used to finally start our blockchain system, for which we need to transplant the designed blocks and blockchain to other folders. We won't elaborate too much on the refactoring part.
Structure:
The utils package is responsible for placing some utility methods for easy access; constcoe stores global constants; blockchain contains the main implementation of the blockchain.
We moved the previous int64 to byte function into utils
package utils
import (
"encoding/binary"
)
func Int64ToByte(num int64) []byte {
var buf = make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(num))
return buf
}
In constcoe, we first store the difficulty coefficient, which will be explained in detail later.
package constcoe
const (
Difficulty = 12
)
The structures and functions we previously wrote are placed in block.go and blockchain.go.
//block.go
package blockchain
import (
"bytes"
"crypto/sha256"
"lighteningchain/utils"
"time"
)
type Block struct {
Timestamp int64
Hash []byte
PrevHash []byte
Data []byte
}
func (b *Block) SetHash() {
information := bytes.Join([][]byte{utils.Int64ToByte(b.Timestamp), b.PrevHash, b.Data}, []byte{})
hash := sha256.Sum256(information)
b.Hash = hash[:]
}
func CreateBlock(prevhash, data []byte) *Block {
block := Block{time.Now().Unix(), []byte{}, prevhash, data}
block.SetHash()
return &block
}
func GenesisBlock() *Block {
genesisWords := "HelloWorld"
return CreateBlock([]byte{}, []byte(genesisWords))
}
//blockchain.go
package blockchain
type BlockChain struct {
Blocks []*Block
}
func (bc *BlockChain) AddBlock(data string) {
newBlock := CreateBlock(bc.Blocks[len(bc.Blocks)-1].Hash, []byte(data))
bc.Blocks = append(bc.Blocks, newBlock)
}
func CreateBlockChain() *BlockChain {
blockchain := BlockChain{}
blockchain.Blocks = append(blockchain.Blocks, GenesisBlock())
return &blockchain
}
In main.go, only these remain.
package main
import (
"fmt"
"lighteningchain/blockchain"
"time"
)
func main() {
blockchain := blockchain.CreateBlockChain()
time.Sleep(time.Second)
blockchain.AddBlock("This is first Block after Genesis")
time.Sleep(time.Second)
blockchain.AddBlock("This is second!")
time.Sleep(time.Second)
blockchain.AddBlock("Awesome!")
time.Sleep(time.Second)
for num, block := range blockchain.Blocks {
fmt.Printf("number:%d Timestamp: %d\n", num, block.Timestamp)
fmt.Printf("number:%d hash: %x\n", num, block.Hash)
fmt.Printf("number:%d Previous hash: %x\n", num, block.PrevHash)
fmt.Printf("number:%d data: %s\n", num, block.Data)
}
}
Try running it, and it runs successfully! The project refactoring is complete, making future development much easier.
Blockchain Consensus Mechanism#
The so-called "consensus mechanism" is the process of completing transaction verification and confirmation through voting by special nodes in a very short time. For a transaction, if several nodes with no conflicting interests can reach a consensus, we can consider that the entire network can also reach a consensus. To put it more simply, if a Weibo influencer in China, a cryptocurrency player in the United States, an African student, and a European traveler who do not know each other all agree that you are a good person, then it can basically be concluded that you are not a bad person.
The PoW consensus mechanism is shown in the figure below.
Next, we will use our Lightning Chain to implement this consensus mechanism.
Adding Nonce#
Nonce is the random number we need to find in the above figure, which is the most critical part that proves your workload. First, add header information to the block.
type Block struct {
Timestamp int64
Hash []byte // The block hash value is its ID
PrevHash []byte
Data []byte
Nonce int64
Target []byte
}
Next, there will be some function errors that we will fix later.
POW Implementation#
In proofofwork.go, we first implement a function to get the target, which will make it easier for us to repeatedly obtain the target in a distributed system.
func (b *Block) GetTarget() []byte {
target := big.NewInt(1)
target.Lsh(target, uint(256-constcoe.Difficulty))
return target.Bytes()
}
The Lsh function shifts left, the smaller the difficulty, the more it shifts, the larger the target difficulty value, and the more space the hash value falls into, making it easier to find a matching nonce.
Next, we will calculate the nonce.
func (b *Block) GetDataBaseNonce(nonce int64) []byte {
data := bytes.Join([][]byte{
utils.Int64ToByte(b.Timestamp),
b.PrevHash,
utils.Int64ToByte(nonce),
b.Target,
b.Data,
},
[]byte{},
)
return data
}
func (b *Block) FindNonce() int64 {
var intHash big.Int
var intTarget big.Int
intTarget.SetBytes(b.Target)
var hash [32]byte
var nonce int64
nonce = 0
for nonce < math.MaxInt64 {
data := b.GetDataBaseNonce(nonce)
hash = sha256.Sum256(data)
intHash.SetBytes(hash[:])
if intHash.Cmp(&intTarget) == -1 {
break
} else {
nonce++
}
}
return nonce
}
As you can see, the mysterious nonce is just an integer starting from 0, and with each attempt, the nonce increases by 1 until the block hash derived from the current nonce is less than the target difficulty value.
So how does the blockchain know that the result computed by your system in this distributed system is correct? Next, we need to write a validation function.
func (b *Block) ValidatePoW() bool {
var intHash big.Int
var intTarget big.Int
var hash [32]byte
intTarget.SetBytes(b.Target)
data := b.GetDataBaseNonce(b.Nonce)
hash = sha256.Sum256(data)
intHash.SetBytes(hash[:])
if intHash.Cmp(&intTarget) == -1 {
return true
}
return false
}
We have completed the PoW implementation, now let's make some small modifications in block.go.
func (b *Block) SetHash() {
information := bytes.Join([][]byte{utils.Int64ToByte(b.Timestamp),
b.PrevHash, b.Target, utils.Int64ToByte(b.Nonce), b.Data}, []byte{})
hash := sha256.Sum256(information) // The sha256 package implements the SHA224 and SHA256 hash algorithms defined in FIPS 180-4.
b.Hash = hash[:]
}
func CreateBlock(prevhash []byte, data []byte) *Block {
block := Block{time.Now().Unix(), []byte{},
prevhash, data, 0, []byte{}}
block.Target = block.GetTarget()
block.Nonce = block.FindNonce()
block.SetHash() // Calculate hash after all data is added
return &block
}
Everything is complete!
Debugging#
Open main.go, and let's add a line to output to verify if PoW is successful.
package main
import (
"fmt"
"lighteningchain/blockchain"
"time"
)
func main() {
blockchain := blockchain.CreateBlockChain()
time.Sleep(time.Second)
blockchain.AddBlock("This is first Block after Genesis")
time.Sleep(time.Second)
blockchain.AddBlock("This is second!")
time.Sleep(time.Second)
blockchain.AddBlock("Awesome!")
time.Sleep(time.Second)
for num, block := range blockchain.Blocks {
fmt.Printf("number:%d Timestamp: %d\n", num, block.Timestamp)
fmt.Printf("number:%d hash: %x\n", num, block.Hash)
fmt.Printf("number:%d Previous hash: %x\n", num, block.PrevHash)
fmt.Printf("number:%d data: %s\n", num, block.Data)
fmt.Printf("number:%d nonce:%d\n", num, block.Nonce)
fmt.Println("POW validation:", block.ValidatePoW())
}
}
Click run, and the output is:
number:0 Timestamp: 1677654426
number:0 hash: 51c810ee37b56f26baaf27ad8c8c271c1e383dcf75c6b8baaca059a9e621ac67
number:0 Previous hash:
number:0 data: HelloWorld!
number:0 nonce:14014
POW validation: true
number:1 Timestamp: 1677654427
number:1 hash: 059131a889810a8484bc072d0bcd7ecba3011a509ab6bc460c7a892357621f82
number:1 Previous hash: 51c810ee37b56f26baaf27ad8c8c271c1e383dcf75c6b8baaca059a9e621ac67
number:1 data: This is first Block after Genesis
number:1 nonce:1143
POW validation: true
number:2 Timestamp: 1677654428
number:2 hash: 055263bd8eea37b526e45b097b1f837c108ab2fc88f26bbf567a4fa9598cadb9
number:2 Previous hash: 059131a889810a8484bc072d0bcd7ecba3011a509ab6bc460c7a892357621f82
number:2 data: This is second!
number:2 nonce:10091
POW validation: true
number:3 Timestamp: 1677654429
number:3 hash: d0b5a049c2780c01e2e66cc23934267c528df80a3bcc69180a3f2231cf08d87f
number:3 Previous hash: 055263bd8eea37b526e45b097b1f837c108ab2fc88f26bbf567a4fa9598cadb9
number:3 data: Awesome!
number:3 nonce:592
POW validation: true
Success!
Summary#
This chapter explained the PoW consensus mechanism, focusing on understanding the nonce and target difficulty value, as well as the implementation of PoW. In the next chapter, we will implement the data storage method in the block and the UTXO model.
Additionally, the mainstream consensus in blockchain has now shifted from PoW to PoS, and I will improve it when I have time in the future.