【项目实践】基于区块链的记账系统&Go语言学习
前言
本学期的区块链课程需要利用区块链的原理,自己实现一个电子帐本。反复查阅资料,我发现除了C++语言以外,Go语言在区块链系统和交易平台里相当常见——它以简洁语法、高效并发(goroutine)和卓越性能著称,适合构建高并发分布式系统,因此在这些应用中被广泛使用。
恰逢我最近明显感到“被Python惯坏了”,并因此有些焦虑,想要学习一些新的东西来打破舒适圈。于是灵机一动,决定用Go语言来完成区块链实验的项目。尽管我并不知道Go语言有些什么特性,甚至从未接触过这门语言,但我坚信掌握一门编程语言的最好方法是在实践中使用它。边做边学,我总能收获一些东西。
而对于区块链记账系统的实现,老师给出了实验步骤和大致思路(虽然又是Python版本的)。因此我只需分步骤完成每个模块的设计,再将它们整合起来就可以了。
因此你将在这篇文章中看到:
- 我从0基础开始,逐步掌握一门编程语言的探索路径。
- 构建一个基于区块链的记账项目的详细过程。
- 区块链相关技术的Go实现。相关的原理并不会详细介绍,但我会分享一些指路链接。
环境配置与Hello World!
Go环境的配置并不复杂,从官网下载对应版本的安装包,直接安装就行了。接着简单配置环境变量,就能在命令行编译运行Go程序。我选择在VS Code作为IDE,因此还需要安装相关插件和配置。此处不再赘述。
我参考了这篇文章配置 VS Code 的Go开发环境。当然在配置完成之后也遇到了一些问题(提示go.mod file not found),最终在CSDN上找到了问题的解决方案,有类似问题的同学也可以参考一下。感谢这些分享经验的作者❤。
接下来就是喜闻乐见的 Hello World 时间。编译运行,看到对应字符串就说明环境配置成功了。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
我不喜欢跟着视频或者网课学习,因此接下来几乎所有关于Go的语法和特性,我都是从菜鸟教程上学习的,偶遇一些困难再针对性地用搜索引擎。除此之外,我尽量避免使用AI工具直接生成代码,不仅形成路径依赖,自己也什么都学不到。
Merkle树的生成
Merkle树(Merkle Tree)是一种基于哈希的二叉树结构,用于高效验证大规模数据的完整性与一致性。其核心思想是将数据块逐层哈希,最终生成一个根哈希值(Merkle Root),任何数据的篡改都会导致根哈希变化。Merkle树是区块链的核心结构,区块头中的Merkle树根保证了区块内数据不被轻易篡改。
然而初来乍到,对Go语言的各种特性、程序设计思想都不清楚,就算知道Merkle树的结构和实现,我还是感到无从下手。甚至查了资料之后,我才知道Go中没有“类”的概念,只有结构体,更不用谈按以前的老路子,用面向对象的思想设计一个结构清晰的程序了。
不知道从什么时候开始养成了不写类就不会写程序的坏习惯……非得从头把整体架构设计好,把程序好好封装起来不可。
既然如此,那就不妨把事情做得简单粗暴一点——直接像C程序那样从头到尾地写,不管代码易读性和可扩展性啥的,先实现功能再说。毕竟基础的声明、循环、条件判断语句都是一样的,只是语法略有不同罢了。说干就干,先把程序逻辑用文字表述出来。
数据块的结构体定义 可包括交易内容,先用一个简单字符串代替;
Merkle树节点的定义 {
指向数据块的指针
节点的哈希
左右子节点的指针
}
函数:构建Merkle树
传入参数:当前待处理数据列表
返回参数:根节点,指向处理完成的Merkle树
函数过程:
由于Merkle树自下而上生长,根据数据列表创建叶节点。
最终得到一个完全由叶节点构成的节点列表。
调用递归函数,传入节点列表,使节点两两合并,计算哈希。
再递归函数中递归向上构建树,递归完成后,得到根节点指针
函数:递归构建过程
传入参数:Merkle结点指针列表
返回参数:Merkle结点指针列表,是合并后的产生的父节点列表,层数-1
递归边界:节点列表中只有一个元素,说明已经生成了根节点
函数过程:
判断递归边界
创建一个用于下一步递归的父结点列表
遍历传入的节点列表,若传入参数中还有未处理的节点:
创建一个新的结点表示父节点
从传入参数中取2个结点(若只剩下一个则只取最后一个)
用子结点地址更新父结点的左右指针,计算Hash值更新父节点Hash
将父节点指针放入列表中
递归调用函数,将新构造的父结点列表传入,并将递归函数的返回值返回
函数:计算哈希
传入参数:1个Merkle树节点的指针
返回参数:SHA256哈希值
函数过程:
若传入的节点具有两个子节点,则取它们的哈希值相连接,再计算Hash
若只有左子节点,则取它的哈希值,再计算一次Hash
若不具备子节点(有数据块),根据数据块的内容计算Hash
一边查资料一边写,花了小半个晚上终于写出来且运行成功了。
package main
import (
"crypto/sha256"
"fmt"
)
type DataBlock struct {
content string
}
// Merkle树节点定义
type MerkleNode struct {
data *DataBlock
hash string
left_child *MerkleNode
right_child *MerkleNode
}
// 从数据块构建Merkle树,返回根
func create_tree(datas []DataBlock) MerkleNode {
var nodes []MerkleNode
// 遍历所有数据块,创建叶节点
for _, data := range datas {
var newNode MerkleNode
newNode.data = &data
update_hash(&newNode)
nodes = append(nodes, newNode)
}
return build_tree(nodes)[0]
}
// 递归逐层构建Merkle树
func build_tree(sons []MerkleNode) []MerkleNode {
var fathers []MerkleNode
// 相邻节点配对,创建它们的父节点
for i := 0; i < len(sons); i += 2 {
var newNode MerkleNode
newNode.left_child = &sons[i]
if i+1 < len(sons) {
newNode.right_child = &sons[i+1]
}
update_hash(&newNode)
fathers = append(fathers, newNode)
}
if len(fathers) == 1 {
return fathers
} else {
return build_tree(fathers)
}
}
// 根据子节点信息,计算当前节点Hash
func update_hash(node *MerkleNode) {
hash := sha256.New()
// 存在data,说明是叶节点
// 否则根据左右子节点的哈希计算
if node.data != nil {
// 若指向数据块,根据content属性计算哈希值
hash.Write([]byte(node.data.content))
} else {
hash.Write([]byte(node.left_child.hash))
if node.right_child != nil {
hash.Write([]byte(node.right_child.hash))
}
}
node.hash = string(hash.Sum(nil))
}
func main() {
datas := [5]DataBlock{{"ha"}, {"hello"}, {"world"}, {"aaa"}, {"hello~"}}
root := create_tree(datas[:])
fmt.Printf("Root Hash of Merkle Tree:%x\n", root.hash)
}
/*
参考链接:
- https://blog.csdn.net/qq756684177/article/details/81518823
- https://www.cnblogs.com/X-knight/p/9142622.html
- https://zhuanlan.zhihu.com/p/666478154
- https://www.cnblogs.com/wanghui-garcia/p/10452431.html
*/
(为了节省篇幅,之后实现的代码不再全部放出)
在写的过程中,我通常是需要用什么就去找什么。例如,想知道如何定义一个变量,我就把变量声明相关的章节看了一遍,然后自己选取合适的方法往下写。因为各个编程语言都是相通的,因此我只要把语法习惯改过来,很快就能上手。
(小声吐槽,从来没有把类型写在变量名/形参后面过,真的好不习惯……)
但是这样会遇到一个问题——有一些Go语言特有的特性是我事先不知道的,于是我就会用一些比较绕的思路去解决原本很简单的问题。
例如在第22行声明MerkleNode数组时,我一开始的思路是先获取输入数据块的数量,再申请相应大小的数组,然后用循环变量逐个填入叶节点。但后来我发现除了固定长度的数组之外,Go提供了一种可变长度的数据结构,切片——我可以方便地使用append方法在切片后追加元素,这样就避免了上述的复杂写法,但也产生了一些返工。
var nodes []MerkleNode // 声明一个切片,存储父节点
for _, data := range datas {
var newNode MerkleNode
nodes = append(nodes, newNode) // 将新节点加入到切片中
}
面对这种情况,我采取的方案是从别的高级语言中“迁移”过来一些思考模式。例如,在写上面的for循环时,我就想到Python中,可以对列表简单地使用类似 for element in list
的形式快速取出元素。于是我以“迭代”作为关键词搜索,发现果然在Go中也存在类似的特性——range关键字。
当然这种方法的前提是学习过Python、Java等主流高级语言,知道哪些时候能够有更简略的写法。
除此之外,第一次写Go,难免有些磕磕绊绊,代码中也有很多用法不一致的地方。于是请到了手下的外籍员工帮我检查一下代码规范(误)。
总而言之,第一次尝试还是很成功的,学会了Go的基本语法(虽然还很不熟练,远远谈不上掌握),也了解了range关键字、切片等特性,算是成功入了门。
目前完成的进度就到这里,接下来需要完成的工作:
- [ ] 比特币的工作量证明PoW算法
- [ ] 椭圆曲线密码的公钥和私钥生成,并通过编码生成比特币的公钥地址
- [ ] 连接数据库,并将比特币公钥和公钥地址存入数据库