本文将逐步拆解实现区块链功能的几个步骤
你需要掌握的基本知识:
- 什么是区块链
- 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包,这里不做赘述。
有疑问加站长微信联系(非本文作者)