很多人学了很久区块链,背得动哈希、默克尔树、工作量证明这些名词,真到动手写代码的时候就卡壳。其实区块链的核心逻辑并没有那么玄乎,抛开 P2P 网络、持久化、智能合约这些外围模块,最内核的东西就是「链式存储 + 共识算法 + 交易处理」三件套。
这篇文章我们用 Go 从零实现一个可运行的简易区块链,完整包含 PoW 工作量证明、默克尔树交易校验、内存交易池三个核心组件。代码尽量精简,只保留最本质的逻辑,读完你就能明白比特币、以太坊这些公链最底层到底是怎么跑起来的。
一、整体架构设计
在动手写代码之前,先把整个系统的模块拆清楚。一个最简的区块链系统至少包含五层:
- 交易层:定义交易结构,管理内存交易池,负责接收和暂存待打包交易
- 区块层:定义区块头与区块体结构,承载交易数据与前块哈希指针
- 共识层:实现 PoW 工作量证明算法,通过算力竞争获得出块权
- 校验层:基于默克尔树快速校验区块内交易完整性,支持轻节点验证
- 链管理层:维护主链状态,处理区块追加、合法性校验与链查询
我们这次不涉及持久化和网络同步,所有数据都放在内存里,重点把核心逻辑跑通。理解了这一套,后续再加上 BoltDB 存区块、加 libp2p 做组网,就是一个完整的公链节点原型。
二、基础数据结构定义
先把最底层的数据结构定下来,这是整个系统的骨架。
2.1 交易结构
一笔交易最核心的就是发送方、接收方、金额,再加上时间戳做唯一区分。真实公链里会有签名、nonce、Gas 这些字段,我们先简化掉。
typeTransactionstruct{FromstringTostringAmountint64Timeint64}// 计算交易哈希,用于默克尔树叶子节点func(tx*Transaction)Hash()[]byte{data:=fmt.Sprintf("%s%s%d%d",tx.From,tx.To,tx.Amount,tx.Time)hash:=sha256.Sum256([]byte(data))returnhash[:]}这里直接把交易字段拼接后做 SHA256,生产环境会用更严谨的序列化方式,比如 RLP 或者 Protobuf,原理是一样的。
2.2 区块结构
区块分成区块头和区块体两部分。区块头存元数据,区块体存具体交易列表。这是区块链最经典的设计,也是默克尔树能发挥作用的前提——轻节点只同步区块头就能验证交易。
typeBlockstruct{Header BlockHeader Txs[]*Transaction}typeBlockHeaderstruct{VersionintPrevHash[]byte// 前一个区块的哈希,形成链式结构MerkleRoot[]byte// 交易默克尔树根哈希TimeStampint64Difficultyint// PoW 难度值Nonceint64// 工作量证明随机数Heightint// 区块高度}PrevHash是链式结构的核心:每个区块都指向前一个区块的哈希,从创世块一直串到最新块。只要中间任何一个区块的数据被篡改,后续所有区块的哈希都会对不上,这就是区块链不可篡改的底层原理。
三、交易池(TxPool)实现
交易池是区块链节点的「候车厅」:用户发的交易先全部进池子里排队,矿工出块的时候从池子里挑交易打包。没有交易池的话,每来一笔交易就出一个块,效率会极低。
3.1 交易池核心逻辑
最简交易池就是一个线程安全的列表,支持添加交易、批量取出交易、查询数量。
typeTxPoolstruct{mu sync.RWMutex pool[]*Transaction}funcNewTxPool()*TxPool{return&TxPool{pool:make([]*Transaction,0),}}// 向交易池添加交易func(tp*TxPool)AddTx(tx*Transaction){tp.mu.Lock()defertp.mu.Unlock()tp.pool=append(tp.pool,tx)}// 取出指定数量的交易用于打包func(tp*TxPool)PackTxs(maxint)[]*Transaction{tp.mu.Lock()defertp.mu.Unlock()iflen(tp.pool)==0{returnnil}n:=min(max,len(tp.pool))txs:=tp.pool[:n]tp.pool=tp.pool[n:]// 打包后从池中移除returntxs}真实公链的交易池要复杂得多,会按 Gas 价格排序、处理 nonce 连续性、做双花校验、限制池大小。但核心思想没变:暂存、排序、打包、出块后清理。
3.2 为什么需要交易池
很多初学者会忽略交易池的重要性。它本质上是「异步解耦」的设计:用户提交交易和矿工打包出块是两个完全不同的节奏。交易可以源源不断地进来,矿工按固定时间间隔出块,每次打包一批。这样既保证了用户体验,也提高了区块空间的利用率。
四、默克尔树(Merkle Tree)实现
默克尔树是区块链里被问得最多的知识点,但很多人只背结论,不知道它到底解决了什么问题。
4.1 默克尔树的作用
如果没有默克尔树,要验证某笔交易是否在某个区块里,你必须把整个区块的所有交易都下载下来,逐一比对。这对轻节点、手机钱包来说完全不可行。
有了默克尔树之后,所有交易的哈希两两配对、逐层哈希,最终得到一个根哈希存在区块头里。验证一笔交易只需要下载一条对数级别的哈希路径,不需要全量交易数据。这就是轻节点能工作的底层基础。
4.2 代码实现
typeMerkleNodestruct{Left*MerkleNode Right*MerkleNode Data[]byte}typeMerkleTreestruct{Root*MerkleNode}funcNewMerkleTree(data[][]byte)*MerkleTree{varnodes[]*MerkleNode// 构造叶子节点for_,datum:=rangedata{hash:=sha256.Sum256(datum)nodes=append(nodes,&MerkleNode{Data:hash[:]})}// 叶子节点数量为奇数时,复制最后一个iflen(nodes)%2!=0{nodes=append(nodes,nodes[len(nodes)-1])}// 逐层向上构建forlen(nodes)>1{varnewLevel[]*MerkleNodefori:=0;i<len(nodes);i+=2{left:=nodes[i]right:=nodes[i+1]combined:=append(left.Data,right.Data...)hash:=sha256.Sum256(combined)parent:=&MerkleNode{Left:left,Right:right,Data:hash[:],}newLevel=append(newLevel,parent)}nodes=newLeveliflen(nodes)%2!=0&&len(nodes)!=1{nodes=append(nodes,nodes[len(nodes)-1])}}return&MerkleTree{Root:nodes[0]}}这里有个细节:如果某一层节点数是奇数,就把最后一个节点复制一份凑成偶数。这是比特币里的标准做法,目的是保证树的结构完整,不会出现单边分支。
4.3 区块集成
每个区块打包交易后,把所有交易的哈希拿出来构建默克尔树,根哈希写入区块头。
funcComputeMerkleRoot(txs[]*Transaction)[]byte{varhashes[][]bytefor_,tx:=rangetxs{hashes=append(hashes,tx.Hash())}tree:=NewMerkleTree(hashes)returntree.Root.Data}五、PoW 工作量证明实现
PoW 是中本聪共识的核心,本质上就是一道「不好算但好验证」的数学题:找一个 nonce,使得「区块头 + nonce」的哈希值前面有 N 个零。N 越大,难度越高。
5.1 核心逻辑
typeProofOfWorkstruct{block*Block target*big.Int}funcNewProofOfWork(block*Block)*ProofOfWork{target:=big.NewInt(1)// 左移 256 - difficulty 位,得到目标值target.Lsh(target,uint(256-block.Header.Difficulty))return&ProofOfWork{block:block,target:target,}}// 准备参与哈希计算的数据func(pow*ProofOfWork)prepareData(nonceint64)[]byte{b:=pow.block.Header data:=bytes.Join([][]byte{IntToBytes(b.Version),b.PrevHash,b.MerkleRoot,IntToBytes(b.TimeStamp),IntToBytes(b.Difficulty),IntToBytes(nonce),IntToBytes(b.Height),},[]byte{})returndata}5.2 挖矿过程
挖矿就是从 0 开始递增 nonce,一次次算哈希,直到结果小于目标值。
func(pow*ProofOfWork)Run()(int64,[]byte){varhashInt big.Intvarhash[32]bytenonce:=int64(0)fornonce<math.MaxInt64{data:=pow.prepareData(nonce)hash=sha256.Sum256(data)hashInt.SetBytes(hash[:])ifhashInt.Cmp(pow.target)==-1{break// 找到符合要求的 nonce}else{nonce++}}returnnonce,hash[:]}这段代码就是比特币挖矿的最简原型。真实环境里会用 ASIC 芯片并行计算,难度也会根据全网算力动态调整,但数学原理完全一致。
5.3 验证逻辑
验证比挖矿简单得多,只需要用区块里的 nonce 算一遍哈希,看是否满足难度要求。
func(pow*ProofOfWork)Validate()bool{varhashInt big.Int data:=pow.prepareData(pow.block.Header.Nonce)hash:=sha256.Sum256(data)hashInt.SetBytes(hash[:])returnhashInt.Cmp(pow.target)==-1}这就是 PoW 最精妙的地方:挖矿成本极高,验证成本极低。全网节点一秒钟就能校验一个区块的合法性,但要造出一个合法区块,必须付出对应的算力成本。
六、区块链主逻辑
把上面的模块串起来,就是完整的区块链对象。它维护一条链、一个交易池,提供出块和校验能力。
6.1 区块链结构体与创世块
typeBlockchainstruct{mu sync.RWMutex chain[]*Block txPool*TxPool diffint// 全局难度}// 创建区块链,生成创世块funcNewBlockchain(diffint)*Blockchain{bc:=&Blockchain{txPool:NewTxPool(),diff:diff,}// 生成创世块genesis:=&Block{Header:BlockHeader{Version:1,PrevHash:make([]byte,32),// 创世块前哈希全零TimeStamp:time.Now().Unix(),Difficulty:diff,Height:0,},Txs:nil,}genesis.Header.MerkleRoot=ComputeMerkleRoot(genesis.Txs)pow:=NewProofOfWork(genesis)nonce,_:=pow.Run()genesis.Header.Nonce=nonce bc.chain=append(bc.chain,genesis)returnbc}创世块是整条链的起点,它没有前序区块,PrevHash 全部填 0。
6.2 出块方法
出块流程就是:从交易池打包交易 → 构建区块 → 计算默克尔根 → PoW 挖矿 → 追加到链上。
func(bc*Blockchain)MineBlock()*Block{bc.mu.Lock()deferbc.mu.Unlock()lastBlock:=bc.chain[len(bc.chain)-1]// 从交易池打包最多 10 笔交易txs:=bc.txPool.PackTxs(10)newBlock:=&Block{Header:BlockHeader{Version:1,PrevHash:bc.hashBlock(lastBlock),TimeStamp:time.Now().Unix(),Difficulty:bc.diff,Height:lastBlock.Header.Height+1,},Txs:txs,}newBlock.Header.MerkleRoot=ComputeMerkleRoot(txs)// 执行 PoW 挖矿pow:=NewProofOfWork(newBlock)nonce,_:=pow.Run()newBlock.Header.Nonce=nonce bc.chain=append(bc.chain,newBlock)returnnewBlock}// 计算区块哈希func(bc*Blockchain)hashBlock(block*Block)[]byte{pow:=NewProofOfWork(block)data:=pow.prepareData(block.Header.Nonce)hash:=sha256.Sum256(data)returnhash[:]}6.3 链合法性校验
整条链的校验逻辑很简单:遍历每个区块,检查前哈希是否对上、PoW 是否合法、默克尔根是否匹配。
func(bc*Blockchain)IsValid()bool{fori:=1;i<len(bc.chain);i++{curr:=bc.chain[i]prev:=bc.chain[i-1]// 检查前哈希指针if!bytes.Equal(curr.Header.PrevHash,bc.hashBlock(prev)){returnfalse}// 检查 PoW 合法性pow:=NewProofOfWork(curr)if!pow.Validate(){returnfalse}// 检查默克尔根是否匹配if!bytes.Equal(curr.Header.MerkleRoot,ComputeMerkleRoot(curr.Txs)){returnfalse}}returntrue}七、运行演示
写个 main 函数把整个流程跑一遍,直观感受一下。
funcmain(){// 初始化一条难度为 16 的链bc:=NewBlockchain(16)fmt.Println("创世块生成完成,链高度:",bc.chain[0].Header.Height)// 往交易池里塞几笔交易fori:=0;i<25;i++{tx:=&Transaction{From:fmt.Sprintf("addr_%d",i),To:fmt.Sprintf("addr_%d",i+100),Amount:int64(rand.Intn(1000)),Time:time.Now().Unix()+int64(i),}bc.txPool.AddTx(tx)}fmt.Println("交易池已存入 25 笔交易")// 连续出 3 个块fori:=0;i<3;i++{block:=bc.MineBlock()fmt.Printf("挖出第 %d 块,nonce: %d,交易数: %d\n",block.Header.Height,block.Header.Nonce,len(block.Txs))}// 校验整条链fmt.Println("链合法性校验结果:",bc.IsValid())}运行后你会看到:创世块生成、交易入池、连续出块,每块最多打包 10 笔交易,25 笔交易分三块打完。整个过程和真实公链的出块逻辑完全一致,只是简化了网络和持久化。
八、踩坑点与优化方向
手写这个最简版本的时候,有几个很容易踩的坑,这里一并说清楚。
第一,字节序问题。把整型转字节数组参与哈希计算时,必须统一用大端序。如果发送方和接收方用的字节序不一样,算出来的哈希完全对不上,这是新手最容易犯的错误。
第二,默克尔树奇数节点处理。如果叶子节点是奇数个,一定要复制最后一个节点,不能直接让父节点挂单个子节点。否则不仅和主流公链不兼容,还会引入安全隐患。
第三,交易池并发安全。交易池的添加和打包是高频并发操作,一定要加读写锁。生产环境还会做更细粒度的锁拆分,避免全局锁成为性能瓶颈。
第四,难度值不要设太高。本地测试建议 16~20 位零就够了,难度 24 以上普通电脑就要挖好几秒。真实比特币网络难度是动态调整的,保证平均 10 分钟出一个块。
如果想继续扩展,可以沿着这几个方向做:
- 加入 UTXO 模型和签名校验,实现真正的转账逻辑
- 用 BoltDB 或 LevelDB 做区块持久化,支持节点重启恢复
- 引入 libp2p 实现 P2P 组网,支持多节点同步
- 加上难度调整算法,根据出块速度动态调节难度
- 实现 SPV 轻节点,验证默克尔证明
九、写在最后
很多人觉得区块链高深莫测,其实剥开一层层包装,最内核的就是这么点东西。哈希指针保证不可篡改,默克尔树支持轻量验证,PoW 解决分布式共识,交易池做异步解耦。这四个组件组合起来,就是中本聪当年写下的比特币原型。
当然,真实公链的工程复杂度要高得多,光是一个交易池就能写几万行代码。但万变不离其宗,所有复杂的优化都是在这个最简模型的基础上,为了性能、安全、可扩展性一点点加上去的。
动手跑一遍代码,比看十篇理论文章都管用。