
动手写一个区块链真的没有想象中那么难。很多人一听到区块链三个字第一反应就是比特币、以太坊、密码学、分布式共识这些高大上的词觉得这玩意儿离自己很远。但如果你会一点Python完全可以自己动手从零写一个迷你区块链出来。我当初就是靠这个项目彻底搞懂了区块到底是怎么链起来的挖矿到底在挖什么而且整个过程不到两百行代码。这篇文章我会带你完整走一遍实战流程用纯Python实现一个支持工作量证明PoW、交易记录、链上数据校验的简易区块链。它不是一个能跑生产环境的系统但足够你把区块链的核心骨架看个清清楚楚。适合刚学完Python基础、想找一个有含金量的练手项目的人也适合那些被各种区块链科普文章绕晕、想用代码直接掀开盖子看原理的人。1. 项目概述与整体设计思路1.1 用Python写区块链为什么是一个绝佳的练手项目先说一个很多人误解的地方区块链不是一个应用而是一种数据结构加协议规范的组合。说人话就是它首先是一条特殊的链表这条链表上的每个节点区块都保存了前一节点的指纹任何一环被篡改整条链立刻就能被发现。然后再加上一套谁有权利往链上追加数据的规则就构成了区块链。为什么用Python练手特别好核心原因是Python能让你把注意力全部放在逻辑上而不是内存管理和指针上。比如你在C语言里要手动处理指针、考虑内存释放在Python里只需要定义类、放列表、算哈希几十行代码就能把核心逻辑跑起来。我见过不少人用Go写区块链代码行数翻了一倍但核心算法反而不容易看懂。初学者或者想快速验证想法的人Python绝对是首选。1.2 核心功能需求拆解拿我们这次的迷你区块链来说它至少要具备以下几个能力区块结构每个区块包含索引index、时间戳timestamp、交易数据transactions实际项目里一般是交易列表、前一区块哈希prev_hash、自己的哈希hash以及一个用于挖矿的随机数nonce。哈希计算对区块的所有信息做SHA-256运算生成一个独一无二的指纹。工作量证明PoW要求计算出来的区块哈希满足一定条件比如前几位必须是0不满足就不断改变nonce重算这就是挖矿的本质。链的完整性校验遍历整条链逐个验证哈希是否连续、数据是否被篡改。交易记录为了让区块里装的不是空数据我们加入简单的转账交易让链上承载真实业务。为什么不用现成的库比如hashlib直接封装好的函数就够了吗对hashlib只负责算哈希区块链的链关系、验证逻辑、PoW规则都得你自己写这正是项目的价值所在。1.3 技术选型背后的考量这里有一个关键设计决策为什么要用前一区块哈希作为连接点。如果你做过普通链表会知道节点之间靠的是下一个节点的地址但区块链里没有地址这个概念用的是哈希指纹。好处是显而易见的地址是逻辑关联容易被伪造而哈希是内容关联只要你改了区块里任何一丁点数据它的哈希就变了跟后面所有区块的上一区块哈希都对不上篡改立刻暴露。另一个决策是PoW难度怎么定。我们通过设置哈希结果必须以前N个0开头来控制挖矿难度。N越大平均要算的次数越多耗时越长。这个参数在实际项目中是动态调整的但在教学项目里我们固定为4位甚至3位都可以太大会让你等到怀疑人生。2. 环境准备与基础代码实现2.1 环境准备装好Python就够了这个项目对第三方库的依赖几乎为零——你只需要Python 3.6以上版本标准库里的hashlib算哈希和json序列化就完全够用了。如果你连Python都没装好去官网下载安装包安装时记得勾选Add Python to PATH避免后面在命令行里找不到python命令。为了验证环境没问题可以在命令行输入python --version能正常输出版本号就行。我个人建议顺手装一个Visual Studio Code装好Python插件写起代码来有语法高亮和自动补全比记事本舒服太多排查缩进问题也方便。这个项目不需要装numpy、不需要装pandas零额外依赖是它作为练手项目的又一大优势——你根本不会被环境配置劝退。提示如果你在运行脚本时遇到ModuleNotFoundError: No module named hashlib那基本不可能是Python环境缺库这是内置库更可能是你电脑上装了多个Python版本当前命令行指向了一个精简版的环境。检查一下系统环境变量里PATH的顺序即可。2.2 第一步定义区块数据结构我们把区块定义成一个类字段就按照前面设计好的来。这里有个很重要的细节区块的哈希并不在一开始就存在它是在挖矿成功后才被计算出来并写进去的所以初始值设为None。nonce是工作量证明的核心变量一开始是0后面会不断累加。import hashlib import json from datetime import datetime class Block: def __init__(self, index, transactions, prev_hash, nonce0): self.index index self.timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.transactions transactions self.prev_hash prev_hash self.nonce nonce self.hash self.compute_hash() def compute_hash(self): 计算当前区块的SHA-256哈希值。 必须先把区块信息转成规范的字符串再编码成字节后计算。 block_string json.dumps({ index: self.index, timestamp: self.timestamp, transactions: self.transactions, prev_hash: self.prev_hash, nonce: self.nonce }, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(block_string.encode()).hexdigest()这里解释一下为什么用json.dumps而不是直接拼字符串。因为Python字典的键顺序是固定的但为了保险起见sort_keysTrue可以让键名按字母排序确保同样的内容永远生成同样的字符串进而生成同样的哈希。这是一个非常容易踩的坑如果你直接拼接字符串万一哪天改了字段顺序同样的区块就会算出完全不同的哈希你会排查到崩溃。2.3 第二步创建区块链类与创世区块区块链本身就是管理区块列表的类。创世区块是整条链的第一个区块它没有前一区块所以prev_hash我们人为地设定为一个固定值通常是64个0因为SHA-256输出的十六进制字符串固定是64位。class Blockchain: def __init__(self): self.chain [] self.pending_transactions [] self.difficulty 4 # 哈希前4位必须为0 self.create_genesis_block() def create_genesis_block(self): genesis_block Block(0, [], 0 * 64) genesis_block.hash genesis_block.compute_hash() self.chain.append(genesis_block)注意我在Block的构造函数里已经调用了compute_hash()但创世区块创建完之后我又专门重算了一次。这是为了演示一个原则hash字段应该始终等于对当前区块内容实时计算的哈希。后续如果你修改了区块数据必须重新计算哈希否则链就会处于哈希与内容不匹配的非法状态。3. 工作量证明与挖矿机制的精髓实现3.1 什么是工作量证明为什么它能让链变安全工作量证明的灵感其实特别朴素让计算变得费力但不让验证变得费力。就好比你解一道数独解出来要花很长时间但别人检查你的答案只需要几秒钟。在区块链里解数独的过程就是不断调整nonce使得整个区块的哈希值满足一个条件——在我们的代码里就是哈希的前difficulty位都是0。为什么前几位是0就能证明工作量哈希算法有个特性输入哪怕只改一个字符输出就是完全不同的随机字符串。所以你要找到某个nonce让哈希值恰好满足前4位是0平均需要尝试16的4次方也就是65536次。这个计算量对人类来说很烦但对计算机也就是几秒钟的事。如果有人想篡改链上的历史数据他不仅要重新计算被改区块的哈希还要把这个区块之后所有区块全部重新挖一遍因为后面的区块存的都是前一区块的哈希。链越长篡改成本就越高这就是区块链不可篡改的底气来源。3.2 实现工作量证明算法下一步在区块链类里加入两个核心方法一个是挖掘新区块并执行PoW另一个是单纯的PoW计算循环。def proof_of_work(self, block): 不断尝试 nonce直到区块哈希满足难度要求。 while True: hash_value block.compute_hash() if hash_value.startswith(0 * self.difficulty): return hash_value block.nonce 1 def add_block(self, block, proof): 验证 proof 是否合法合法则加入链。 prev_hash self.chain[-1].hash if prev_hash ! block.prev_hash: return False if not proof.startswith(0 * self.difficulty): return False if proof ! block.compute_hash(): return False block.hash proof self.chain.append(block) return True def mine_block(self, transactions): 打包交易创建新区块计算工作量证明加入链。 new_block Block(len(self.chain), transactions, self.chain[-1].hash) proof self.proof_of_work(new_block) self.add_block(new_block, proof) return new_block特别注意add_block里那三个判断前一区块哈希是否一致、工作量证明是否满足、哈希是否真正对应区块内容。区块链校验的本质就是这三个判断缺一不可。网络上很多简化教程会把这几个步骤混在一起写看起来代码更短但实际上混淆了挖矿和验证的边界不利于理解。真实的区块链系统里每个节点收到别的节点广播的新区块时都必须做完整验证验证不通过直接拒绝这个过程是被严格分离开的。3.3 怎么验证挖矿成功我们可以写一小段测试代码让区块链自动挖出几个区块然后打印出每个区块的哈希if __name__ __main__: my_chain Blockchain() my_chain.mine_block([Alice 转给 Bob 5 BTC]) my_chain.mine_block([Bob 转给 Charlie 2 BTC]) for block in my_chain.chain: print(f区块 {block.index} | 哈希: {block.hash})运行后你会看到类似这样的输出区块 0 | 哈希: 0000e8b7a2c8d1f6c9a1e6d2e6f28b3e9a6f9a6f3a7d24d0d5e6f2e6f8a1b3c9 区块 1 | 哈希: 0000f3b2e0b8d27a6f3d2a6f2c9e4b8a7f0d3c5e2a9b7c1d4e6f8a0b2c3d4 区块 2 | 哈希: 00009c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a每个哈希都是64个十六进制字符且都满足前4位是0。如果你在某些区块上看到的哈希长度不对多半是json.dumps里混入了中文字符但没加ensure_asciiFalse导致编码不一致或者是控制台把字符串截断了去代码里查一下就能发现。4. 完整实操加入交易、验证链完整性与模拟网络环境4.1 交易数据的简单建模区块链里真正装的有价值数据是交易。交易的本质是一个账本记录谁转给谁多少钱加上签名验证。完整实现数字签名需要引入非对称加密那会让代码量翻倍所以在教学版本里我们用字典来表示一笔交易def new_transaction(self, sender, recipient, amount): 创建一笔交易并加入待处理列表。 self.pending_transactions.append({ sender: sender, recipient: recipient, amount: amount }) return self.last_block().index 1实际的比特币系统里每笔交易还包含输入引用上一笔交易、输出收款地址和金额、脚本签名等字段用来解决双花问题。但核心的账本模型就是这个样子先理解交易是放在区块里的比一开始就扎进UTXO模型里要友好得多。4.2 完整性与篡改检测区块链的安全底线链的完整性校验是区块链项目里绝对不能省的部分。校验逻辑其实很简单从第二个区块开始遍历检查两件事——当前区块里存的hash是否等于用当前区块内容算出的哈希以及当前区块的prev_hash是否等于上一个区块的hash。def is_chain_valid(self): for i in range(1, len(self.chain)): current_block self.chain[i] prev_block self.chain[i - 1] if current_block.hash ! current_block.compute_hash(): return False if current_block.prev_hash ! prev_block.hash: return False return True这个方法写起来简单却是整个区块链安全模型的核心。我建议你故意做一次篡改测试挖完几个区块之后手动把第1个区块的交易数据改掉再调用is_chain_valid你会发现返回False。这个过程能让你非常直观地理解不可篡改到底是怎么做到的——不是没人能改而是改了之后整个链就废了。4.3 模拟P2P网络与节点间同步轻量版真实的区块链是运行在P2P网络上的每个节点都保存一条链节点间通过广播来同步数据。完整实现P2P通信需要用到socket或者websocket这已经超出简单区块链的范畴了。但我们可以做一个轻量级的模拟定义两个区块链实例让其中一个节点把整条链同步到另一个节点并验证同步过来的链是合法的。def sync_chain_from(self, other_chain): 从另一个节点同步链前提是对方的链更长且合法。 if len(other_chain.chain) len(self.chain) and other_chain.is_chain_valid(): self.chain other_chain.chain return True return False这里体现的是共识机制里最朴素的规则最长链优先。当网络里出现分叉时所有节点都倾向于承认更长的那条链因为那条链意味着更多的工作量投入。为什么这么设计因为如果有人想发动攻击他必须拥有超过全网一半的算力51%攻击才能持续制造出更长的链。而在这个教学项目里你不需要真的去模拟攻击只要理解这个比长度的逻辑就够了。4.4 一个完整的可运行Demo把上面的代码整合到一起写一个完整的演示程序让大家可以直接跑通import hashlib import json from datetime import datetime class Block: def __init__(self, index, transactions, prev_hash, nonce0): self.index index self.timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.transactions transactions self.prev_hash prev_hash self.nonce nonce self.hash self.compute_hash() def compute_hash(self): block_string json.dumps({ index: self.index, timestamp: self.timestamp, transactions: self.transactions, prev_hash: self.prev_hash, nonce: self.nonce }, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(block_string.encode()).hexdigest() class Blockchain: def __init__(self): self.chain [] self.pending_transactions [] self.difficulty 4 self.create_genesis_block() def create_genesis_block(self): genesis_block Block(0, [], 0 * 64) genesis_block.hash genesis_block.compute_hash() self.chain.append(genesis_block) def proof_of_work(self, block): while True: hash_value block.compute_hash() if hash_value.startswith(0 * self.difficulty): return hash_value block.nonce 1 def add_block(self, block, proof): prev_hash self.chain[-1].hash if prev_hash ! block.prev_hash: return False if not proof.startswith(0 * self.difficulty): return False if proof ! block.compute_hash(): return False block.hash proof self.chain.append(block) return True def mine_block(self, transactions): new_block Block(len(self.chain), transactions, self.chain[-1].hash) proof self.proof_of_work(new_block) self.add_block(new_block, proof) return new_block def new_transaction(self, sender, recipient, amount): self.pending_transactions.append({ sender: sender, recipient: recipient, amount: amount }) return self.last_block().index 1 def last_block(self): return self.chain[-1] def is_chain_valid(self): for i in range(1, len(self.chain)): current_block self.chain[i] prev_block self.chain[i - 1] if current_block.hash ! current_block.compute_hash(): return False if current_block.prev_hash ! prev_block.hash: return False return True def sync_chain_from(self, other_chain): if len(other_chain.chain) len(self.chain) and other_chain.is_chain_valid(): self.chain other_chain.chain return True return False if __name__ __main__: my_chain Blockchain() my_chain.new_transaction(Alice, Bob, 5) my_chain.new_transaction(Bob, Charlie, 2) my_chain.mine_block(my_chain.pending_transactions) print(链是否有效, my_chain.is_chain_valid()) for block in my_chain.chain: print(f区块 {block.index} | 交易: {block.transactions} | 哈希: {block.hash}) # 模拟篡改 my_chain.chain[1].transactions [{sender: Eve, recipient: Eve, amount: 100}] print(篡改后链是否有效, my_chain.is_chain_valid())把以上代码保存为simple_blockchain.py直接用python simple_blockchain.py运行即可输出内容大致如上。我个人建议亲手敲一遍代码而不是复制粘贴因为敲代码的过程中你会自然注意到哪些字段在哪个方法里被赋值这种手感是看多少教程都换不来的。5. 实战中的高频问题与调试技巧5.1 哈希值总是不对中文和编码问题最常见的坑就是json.dumps在包含中文字符时无法保持稳定。Python默认的ensure_ascii是True会把中文转成\uXXXX的转义序列虽然内容一样、哈希结果也一样但你在控制台打印里看到的是转义后的乱码很容易让你误以为数据错了。更严重的是如果你在某个地方手动拼字符串时忘了统一编码格式同样的内容就会算出不同的哈希。我的建议是所有序列化操作统一使用json.dumps(..., sort_keysTrue, ensure_asciiFalse)不要一半用json、一半用字符串拼接。5.2 算力爆炸难度调太高导致跑不出结果如果你把difficulty设成6甚至更高你会发现程序卡在挖掘某个区块上迟迟不结束。这是因为每增加一位0平均尝试次数就乘以16。4位需要6万多次5位需要100万次6位需要1600万次。对教学项目来说4是恰到好处的既能让你感受到CPU在真正计算又不会等太久。如果你想快速演示设3也行一秒能挖出好几个块非常适合调试。5.3 链校验永远返回False创世区块哈希没写好另一个常见问题是is_chain_valid()从第1个区块开始检查如果你在创建创世区块时没有正确赋值hash或者让创世区块的prev_hash字段跟compute_hash()不一致那么每次校验都会失败。记住一个简单的规则创世区块的prev_hash是人为约定的但它自己的hash必须等于真实计算的结果。5.4 交易列表被共享Python列表的引用陷阱如果你在代码里写了类似all_transactions []然后在循环里不断all_transactions.append(tx)并把同一个列表塞进多个区块你会发现所有区块的transactions字段都指向同一个列表对象改一个全变。这是Python的经典坑解决方法是创建新区块时传一份拷贝my_chain.mine_block(list(my_chain.pending_transactions))或者直接在mine_block里用transactions[:]做切片拷贝避免原列表后续被清空或修改影响已入块的交易。5.5 问题排查速查表症状可能原因解决方案哈希值带\u转义ensure_ascii未设为Falsejson.dumps(..., ensure_asciiFalse)挖矿超时不结束difficulty太高降到3或4链校验返回False修改字段后未重算哈希使用前先调用compute_hash()所有区块的哈希都相同区块内容没区别时间戳精度不足检查时间戳是否加入唯一字段交易数据相互干扰Python列表引用同一对象传入拷贝list(...)区块索引跳跃手动创建区块时index错误用len(self.chain)动态生成6. 项目扩展从玩具区块链到真实应用场景6.1 区块链溯源系统的简化实现搞懂这个项目之后你完全可以把它往区块链溯源方向扩展。最近经常听到区块链溯源系统代码这个热搜词它本质上就是在我们写的交易字段里把谁转给谁多少钱换成产品从工厂流转到经销商再流转到门店的每一步记录。每个溯源节点就是一个区块把生产、质检、物流信息逐一上链。这样做的好处是消费者拿到商品后可以验证整条流转记录是否可信任何一环被篡改链的校验立刻失败。6.2 区块链盲盒的公平性机制还有一个热门方向是把区块链用在盲盒抽签上。传统的盲盒抽签是由平台中心化运作的用户天然会怀疑是不是内定。如果把抽签规则和结果写到区块链上让每个参与者都能看到完整的抽签算法和开奖记录的哈希那就能做到可验证的公平。你只需要在交易字段里记录每个用户的抽签请求和随机种子再用我们写的mine_block把这些请求打包上链最后公布链上数据的哈希供所有人核对。这个方向非常有现实意义也是很多人感兴趣的点。6.3 后续可以添加的进阶功能如果你想把项目继续往下做我这里列几个建议引入数字签名用ecdsa或cryptography库给每笔交易签名实现只有私钥持有者才能花自己的钱。实现简单的P2P通信用WebSocket让两台电脑上的节点互相广播新块、同步链。动态调整难度根据最近N个区块的平均挖矿时间自动调整difficulty让出块速度稳定在一定范围内。实现UTXO模型仿照比特币把余额模型改成未花费交易输出模型理解真实比特币系统的账本逻辑。加入Merkle树把区块里的多笔交易做成默克尔树只需要存储树根就能高效验证某一笔交易是否存在。我自己在写完这个项目之后最大的收获倒不是代码本身而是对信任这件事有了更具体的理解。区块链并不是什么神秘黑魔法它就是一套谁也别想偷偷改数据的工程机制。当你亲手把那个nonce从0一点点加到几万次终于看到屏幕上跳出一个前四位全是0的哈希时那种原来是这么回事的感觉比读一百篇科普文章都实在。最后再分享一个小技巧调试区块链代码的时候别只在is_chain_valid()返回False之后才慌张。你可以在每个关键方法里加一个临时的print比如打印正在挖第N个区块当前nonce是X然后观察nonce是怎么变化的。当你看到nonce一步一个脚印地涨上去突然间某个值让哈希变成了0000开头整个计算过程在你眼里就没有任何秘密了。这就是这个项目最迷人的地方——它把宏大叙事变成了可触摸的代码逻辑。