共计 7793 个字符,预计需要花费 20 分钟才能阅读完成。
本文将逐步拆解实现区块链功能的几个步骤
你需要掌握的基本知识:
- 什么是区块链
- 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 包,这里不做赘述。
正文完