本文将逐步拆解实现区块链功能的几个步骤
你需要掌握的基本知识:
- 什么是区块链
- sha256 哈希加密算法
- go 语言基础,包括 goroutine 和 channel 的理解
准备工作
- go get github.com/davecgh/go-spew/spew spew 是一个非常好的打印输出工具,可以在终端输出 struct 和 slice 数据
- go get github.com/gorilla/mux mux 可以用来处理 http 请求,帮助我们快速搭建一个 go 服务器
- go get github.com/joho/godotenv 这个包可以读取.env 文件中的变量
.env 文件需要在项目的根目录下,一般在 main.go 所在位置
- 一款给力的 IDE,比如 Goland
几个概念
- 挖矿,挖矿其实就是通过解决一类数学难题,得到在现有区块链上创建一个区块的权利,并获得一些奖励,比如比特币,以太币等。
- PoW(Proof of work),简单来说 PoW 就是:有一个 Nonce 值(值随意),这个 Nonce 值和区块的数据结合在一起通过 SHA256 得到一个哈希值,如果这个哈希值的前 N(difficulty) 位字符都是 0,那么就算解决了这个数学难题,可以创建一个区块。
- 区块链,区块链是前后紧密连接的,每一个 Block 都会记录上一个区块的哈希,如果当前生成的区块的所记录的 PrevHash 与上一区块不同的话,那么此次生成就无效,同理,如果任何一个人想在区块链的某一个区块上修改数据,那么就会造成整个链无效。
创建项目
- 在 $GOPATH 的 src 下创建项目 blockChain
- 在 blockChain 下创建文件.env 和 main.go
在.env 文件中写入 PORT=8088
需要引入的包
import (
"crypto/sha256"
"encoding/hex"
"time"
"os"
"log"
"net/http"
"github.com/gorilla/mux"
"encoding/json"
"io"
"github.com/davecgh/go-spew/spew"
"sync"
"strconv"
"strings"
"fmt"
"github.com/joho/godotenv"
"net"
"bufio"
)
区块逻辑
定义 Block 区块结构体
type Block struct {
Index int // 表示区块所在区块链的位置
Timestamp string // 生成区块的时间戳
Data int // 写入区块的数据
Hash string // 整个区块数据 SHA256 的哈希
PrevHash string // 上一个区块的哈希值
Difficulty int // 定义难度
Nonce string // 定义一个 Nonce
}
定义常量和一些变量
const difficulty = 1 // 定义难度,也就是哈希包含多少个 0 的前缀
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是 Block
var bcServer chan []Block // 定义一个 channel,处理各个节点之间的同步问题
计算区块哈希
/**
计算区块哈希值
*/
func calculateHash(block Block) string {record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前 block 区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce 值一并加入
h := sha256.New() // 得到 sha256 哈希算法
h.Write([]byte(record)) // 得到对应哈希
hashed := h.Sum(nil)
return hex.EncodeToString(hashed) // 转化为字符串返回
}
生成新的区块
/**
生成一个区块,根据上一个区块
*/
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1 // 索引自增
newBlock.Timestamp = t.String() // 时间戳
newBlock.Data = Data // 数据
newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
newBlock.Difficulty = difficulty // 难度
//newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
for i := 0; ; i++ {hex := fmt.Sprintf("%x", i) // 16 进制展示
newBlock.Nonce = hex
newHash := calculateHash(newBlock) // 计算哈希
if !isHashValid(newHash, newBlock.Difficulty) {fmt.Println(newHash, "继续努力!????")
time.Sleep(time.Second) // 每隔 1s 执行一次
continue
} else {fmt.Println(newHash, "已经成功!")
newBlock.Hash = newHash
break
}
}
return newBlock, nil
}
验证区块是否合法
/**
验证区块是否合法
*/
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
return false
}
if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
return false
}
if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
return false
}
return true
}
验证哈希是否符合 PoW
/**
验证哈希的前缀是否包含 difficulty 个 0
*/
func isHashValid(hash string, difficulty int) bool {prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}
选择长链
因为在实际场景中,区块链可能会产生分叉,造成 A 和 B 长短不一的情况,故而选择长的作为新链
/**
选择长链作为正确的链
*/
func replaceChain(newBlocks []Block) {if len(newBlocks) > len(BlockChain) { // 计算数组长度
BlockChain = newBlocks
}
}
同步节点逻辑
如图所示,节点数据同步就是通过新节点中生成一个区块后,先通过 channel 传递给主线程,然后主线程广播给各个节点来完成的。
监听连接逻辑
/**
处理连接
*/
func handleConn(conn net.Conn) {defer conn.Close() // 完成后关闭
spew.Dump(conn)
io.WriteString(conn, "输入数字:")
scanner := bufio.NewScanner(conn)
go func() {for scanner.Scan() { // 轮询扫描所有 tcp 连接
data, err := strconv.Atoi(scanner.Text())
if err != nil {log.Printf("%v 非数字", scanner.Text(), err)
}
newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)
if err != nil {log.Println(err)
continue
}
if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {newBlockChain := append(BlockChain, newBlock)
replaceChain(newBlockChain)
}
bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
io.WriteString(conn, "\n 输入数字:")
}
}()
go func() {
for { // 每隔 10s 同步一次
time.Sleep(10 * time.Second)
output, err := json.MarshalIndent(BlockChain, ""," ")
if err != nil {log.Fatal(err)
}
io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
}
}()
for _= range bcServer {spew.Dump(BlockChain)
}
}
主函数
func main () {err := godotenv.Load()
if err != nil {log.Fatal(err)
}
bcServer = make(chan []Block) // 创建通道
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "","", difficulty, ""}
spew.Dump(genesisBlock)
BlockChain = append(BlockChain, genesisBlock) // 创世区块
server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听 TCP 端口
if err != nil {log.Fatal(err)
}
defer server.Close() // 完成后关闭 server
for {conn, err := server.Accept()
if err != nil {log.Fatal()
}
go handleConn(conn) // 协程处理连接
}
}
全量代码
package main
import (
"crypto/sha256"
"encoding/hex"
"time"
"os"
"log"
"net/http"
"github.com/gorilla/mux"
"encoding/json"
"io"
"github.com/davecgh/go-spew/spew"
"sync"
"strconv"
"strings"
"fmt"
"github.com/joho/godotenv"
"net"
"bufio"
)
//////////////////// 处理区块链 ////////////////////
const difficulty = 1 // 定义难度,也就是哈希包含多少个 0 的前缀
type Block struct {
Index int // 表示区块所在区块链的位置
Timestamp string // 生成区块的时间戳
Data int // 写入区块的数据
Hash string // 整个区块数据 SHA256 的哈希
PrevHash string // 上一个区块的哈希值
Difficulty int // 定义难度
Nonce string // 定义一个 Nonce
}
var mutex = &sync.Mutex{} // 防止并发写入请求造成的错误,加入互斥锁
var BlockChain []Block // 定义一个区块链,数据元素要全部都是 Block
var bcServer chan []Block // 定义一个 channel,处理各个节点之间的同步问题
/**
计算区块哈希值
*/
func calculateHash(block Block) string {record := strconv.Itoa(block.Index) + block.Timestamp + strconv.Itoa(block.Data) + block.PrevHash + block.Nonce // 得到当前 block 区块的字符串拼接,按照索引、时间戳、所含数据、上一个区块哈希来进行记录,Nonce 值一并加入
h := sha256.New() // 得到 sha256 哈希算法
h.Write([]byte(record)) // 得到对应哈希
hashed := h.Sum(nil)
return hex.EncodeToString(hashed) // 转化为字符串返回
}
/**
生成一个区块,根据上一个区块
*/
func generateBlock(oldBlock Block, Data int) (Block, error) {
var newBlock Block
t := time.Now()
newBlock.Index = oldBlock.Index + 1 // 索引自增
newBlock.Timestamp = t.String() // 时间戳
newBlock.Data = Data // 数据
newBlock.PrevHash = oldBlock.Hash // 上一个区块的哈希
newBlock.Difficulty = difficulty // 难度
//newBlock.Hash = calculateHash(newBlock) // 计算本区块的哈希
for i := 0; ; i++ {hex := fmt.Sprintf("%x", i) // 16 进制展示
newBlock.Nonce = hex
newHash := calculateHash(newBlock) // 计算哈希
if !isHashValid(newHash, newBlock.Difficulty) {fmt.Println(newHash, "继续努力!????")
time.Sleep(time.Second) // 每隔 1s 执行一次
continue
} else {fmt.Println(newHash, "已经成功!")
newBlock.Hash = newHash
break
}
}
return newBlock, nil
}
/**
验证区块是否合法
*/
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index + 1 != newBlock.Index { // 如果索引不继承自上一个,验证不通过
return false
}
if oldBlock.Hash != newBlock.PrevHash { // 如果哈希不继承上一个区块,验证不通过
return false
}
if calculateHash(newBlock) != newBlock.Hash { // 如果计算出来的哈希不一致,验证不通过
return false
}
return true
}
/**
验证哈希的前缀是否包含 difficulty 个 0
*/
func isHashValid(hash string, difficulty int) bool {prefix := strings.Repeat("0", difficulty)
return strings.HasPrefix(hash, prefix)
}
/**
选择长链作为正确的链
*/
func replaceChain(newBlocks []Block) {if len(newBlocks) > len(BlockChain) { // 计算数组长度
BlockChain = newBlocks
}
}
////////////////// 主函数 /////////////////
func main () {err := godotenv.Load()
if err != nil {log.Fatal(err)
}
bcServer = make(chan []Block) // 创建通道
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "","", difficulty, ""}
spew.Dump(genesisBlock)
BlockChain = append(BlockChain, genesisBlock) // 创世区块
server, err := net.Listen("tcp", ":" + os.Getenv("PORT")) // 监听 TCP 端口
if err != nil {log.Fatal(err)
}
defer server.Close() // 完成后关闭 server
for {conn, err := server.Accept()
if err != nil {log.Fatal()
}
go handleConn(conn) // 协程处理连接
}
}
/**
处理连接
*/
func handleConn(conn net.Conn) {defer conn.Close() // 完成后关闭
spew.Dump(conn)
io.WriteString(conn, "输入数字:")
scanner := bufio.NewScanner(conn)
go func() {for scanner.Scan() { // 轮询扫描所有 tcp 连接
data, err := strconv.Atoi(scanner.Text())
if err != nil {log.Printf("%v 非数字", scanner.Text(), err)
}
newBlock, err := generateBlock(BlockChain[len(BlockChain) - 1], data)
if err != nil {log.Println(err)
continue
}
if isBlockValid(newBlock, BlockChain[len(BlockChain) - 1]) {newBlockChain := append(BlockChain, newBlock)
replaceChain(newBlockChain)
}
bcServer <- BlockChain // 将生成的区块数据交给通道,单向传递
io.WriteString(conn, "\n 输入数字:")
}
}()
go func() {
for { // 每隔 10s 同步一次
time.Sleep(10 * time.Second)
output, err := json.MarshalIndent(BlockChain, ""," ")
if err != nil {log.Fatal(err)
}
io.WriteString(conn, "\n↓↓↓↓↓↓↓↓↓↓↓↓↓ 同步区块链:↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n"+ string(output) + "\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑\n")
}
}()
for _= range bcServer {spew.Dump(BlockChain)
}
}
运行
- 打开终端,运行 go run main.go,作为主线程终端
- 新开两个终端作为节点,运行 nc localhost 8088 或 telnet localhost 8088,输入相应的数字
- 等待生成区块,主线程显示如下
- 各个节点每过 10s 会接收主线程的同步区块链数据
- 你可以更换 difficulty 常量的值为 2 或 3,计算时间会成倍增加。
以上节点间的广播同步是通过 tcp 连接来实现的,但更好的方案应该是 p2p 网络,需要安装 libp2p 包,这里不做赘述。