1. 为什么用 Python 手搓一条链:从需求到方案选型
很多人第一次接触区块链概念时,脑子里全是问号:去中心化到底怎么去?哈希链为什么改不了?工作量证明到底在证明什么?看白皮书吧,满屏的 Merkle Tree、椭圆曲线签名、P2P gossip 协议,还没入门就被劝退。我当初也是这样,后来发现一个特别有效的办法——别管那些工程化的庞然大物,先用 Python 把最核心的那根“链”亲手搭出来。这个项目标题“python实现简单区块链结构”说的就是这么一件事:不依赖任何区块链框架,纯用标准库,写一个能跑、能挖矿、能校验、能防篡改的迷你链。
它解决的核心问题不是“造一个能上线的币”,而是把区块链最本质的三件事讲清楚:数据怎么串成链、新块怎么被合法添加、历史怎么被验证。适合谁看?适合刚学完 Python 基础语法、想找个有嚼头的练手项目的同学;也适合做后端或安全方向、想理解区块链底层数据结构的开发者;还适合需要给学生讲区块链原理的讲师,拿这份代码当课堂演示,比 PPT 直观一百倍。
我先把结论摆在这:一条最简区块链,核心代码量不到 200 行,用到的 Python 知识就是类、字典、列表、hashlib、time、json,连第三方库都不用装。但麻雀虽小,五脏俱全——区块结构、链式存储、哈希指针、工作量证明、一致性校验,一个都不少。下面我就按“设计思路→核心细节→完整实操→踩坑排查”的顺序,把这条链从里到外拆一遍。你跟着敲完,再回头看那些复杂的公链代码,会发现很多花哨的东西都只是在这根骨架上加肉。
1.1 极简区块链到底“简”在哪、“全”在哪
先划清楚边界,免得期望错位。这个项目里的“简单”体现在三个地方:第一,没有网络层,节点就是本地一个 Python 对象,不涉及 P2P 广播和节点发现;第二,没有账户和签名体系,不涉及公私钥、地址、UTXO 或账户模型,交易数据用普通字典模拟即可;第三,没有共识竞争,只有一个“矿工”,不处理分叉和最长链选择。这三点砍掉之后,代码量骤降,但区块链的灵魂还在。
那“全”在哪?全在数据结构与验证逻辑上。区块头里该有的字段——索引、时间戳、数据、前块哈希、随机数、自身哈希——一个不少;链的添加必须经过工作量证明,不是想加就加;任何对历史区块的篡改都会导致后续所有哈希对不上,校验直接失败。这三点恰好对应区块链的三大特性:链式防篡改、工作量证明、可验证性。把这三点吃透,再去理解比特币的难度调整、以太坊的状态树,就是顺藤摸瓜的事。
我个人的经验是,学区块链最忌讳一上来就啃工程代码。那些项目为了性能和安全做了大量抽象,新手根本分不清哪些是“区块链本质”,哪些是“工程优化”。先用 Python 写个裸的,把本质拎出来,后面加任何东西你都知道它加在哪一层、为什么加。
1.2 技术选型:为什么是 Python 标准库而不是框架
有人会问,学区块链为什么不直接用现成的库,比如某些链的开发框架?我的回答很直接:用框架你学的是框架,不是区块链。框架把哈希计算、链管理、共识都封装好了,你调几个 API 就跑起来,但问你“前块哈希是怎么参与当前块计算的”,大概率答不上来。手搓的意义就在于每一个字节都由你掌控。
具体到技术选型,我坚持三条:
- 哈希用
hashlib.sha256:SHA-256 是比特币采用的哈希算法,输出 256 位、64 个十六进制字符,雪崩效应明显(改一个字符,输出面目全非),是演示防篡改的最佳选择。Python 标准库直接支持,无需安装。 - 序列化用
json:区块要参与哈希计算,必须先把字典转成稳定的字符串。json.dumps配合sort_keys=True能保证同样的数据每次序列化结果一致,这点极其关键,后面会专门讲。 - 时间戳用
time.time():返回浮点秒数,够用。真实链用整数秒,我们演示无所谓,但要注意它参与哈希,所以格式必须固定。
不引入任何第三方库,还有一个隐性好处:代码可以在任何装了 Python 的机器上直接跑,不用配环境、不用管版本冲突。对于教学和分享来说,这个优势太大了。
2. 核心结构拆解:区块、链、哈希指针三件套
这一章我们把骨架搭起来。理解区块链,先理解三个概念:区块长什么样、链怎么把区块串起来、哈希指针为什么能防篡改。我用生活化的类比帮你建立直觉,再落到代码上。
2.1 区块的字段设计与每个字段的存在理由
一个区块就是一个数据容器,但每个字段都不是随便放的。我设计的区块包含六个字段,逐个说清楚:
- index(索引):区块在链中的位置,从 0 开始。创世块 index=0。它的作用是定位和排序,校验时要求后一块的 index 必须等于前一块 index+1,防止插入或删除。
- timestamp(时间戳):区块被创建的时间。它参与哈希,所以一旦确定就不能改。真实链用它做难度调整的参考,我们这里主要用来记录和展示。
- data(数据):区块承载的业务数据。简单链里可以是一句话、一笔转账记录、一个字典。我用字典模拟交易,比如
{"from": "A", "to": "B", "amount": 10}。 - previous_hash(前块哈希):这是哈希指针,是整个防篡改机制的命门。它存的是前一个区块的哈希值,把当前块和前块“焊”在一起。
- nonce(随机数):工作量证明的变量。矿工不断改变它,直到当前块的哈希满足难度条件。
- hash(自身哈希):由上面五个字段(加上 nonce)一起计算得出。注意,自身哈希不参与自己的计算,否则就成了“先有鸡还是先有蛋”。
这里有个新手最容易犯的错:把hash字段也放进参与哈希计算的字典里。结果就是计算哈希时需要哈希值本身,逻辑死循环。正确做法是:计算哈希时用前五个字段,算完之后把结果存进第六个字段。
2.2 哈希指针:一根看不见的“防篡改锁链”
哈希指针这个词听着玄,其实就是一个“存着别人哈希值的字段”。普通指针存的是内存地址,哈希指针存的是数据指纹。它的威力在于单向绑定:前块的任何改动都会改变前块哈希,而前块哈希又写在当前块里,于是当前块哈希也跟着变,再往后整条链全变。
打个比方:一排多米诺骨牌,每张牌上写着前一张牌的“指纹”。你偷偷改动了第 3 张牌,它的指纹变了,第 4 张牌上记录的旧指纹就对不上,第 4 张牌作废,第 5 张、第 6 张……全废。想蒙混过关,你得把第 3 张之后所有牌全部重算一遍,而且还要重新满足工作量证明。这就是区块链“改不了”的底层逻辑——不是物理上改不了,而是改的代价高到不划算。
代码上,这个绑定关系体现在calculate_hash方法里:计算当前块哈希时,必须把previous_hash纳入输入。只要这一行在,防篡改就成立。
2.3 链的容器选择:为什么用列表而不是链表
从数据结构角度,区块链天然适合链表——每个节点指向下一个。但我在 Python 实现里用list存区块,原因有三:
- 需要频繁按索引访问:校验时要遍历、要取前块,列表 O(1) 随机访问比链表方便。
- 需要序列化展示:列表直接
json.dumps就能输出整条链,链表还得自己写遍历。 - Python 的 list 本身就是动态数组,尾部 append 是均摊 O(1),性能足够。
真正的区块链在存储层用的是 LevelDB 这类 KV 数据库,用哈希做 key,那是工程层面的优化。教学场景下,list 是最直观的选择。链对象只需要维护一个chain列表,外加一个difficulty难度参数,以及创世块的初始化逻辑。
3. 完整实操:从零敲出一条能挖矿的链
前面讲的是“为什么”,这一章全是“怎么做”。我把完整代码拆成四步:写区块类、写链类、实现工作量证明、跑起来测试。每一步我都给出代码和逐行解释,你照着敲,十分钟能跑通。
3.1 第一步:实现 Block 类与哈希计算
先写区块类。核心是calculate_hash方法,它决定了哈希怎么算。
import hashlib import json import time class Block: def __init__(self, index, timestamp, data, previous_hash, nonce=0): self.index = index self.timestamp = timestamp self.data = data self.previous_hash = previous_hash self.nonce = nonce self.hash = self.calculate_hash() def calculate_hash(self): block_string = json.dumps({ "index": self.index, "timestamp": self.timestamp, "data": self.data, "previous_hash": self.previous_hash, "nonce": self.nonce }, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest()逐行拆解几个关键点。json.dumps里的sort_keys=True是必须的,因为 Python 字典在 3.7 之后虽然保持插入顺序,但不同代码路径构造字典的顺序可能不同,排序后能保证序列化结果稳定。.encode()把字符串转成字节,因为sha256只接受 bytes。hexdigest()返回 64 位十六进制字符串,可读性好。
注意:
timestamp用的是浮点数,json.dumps序列化浮点数时不同平台可能有细微差异。教学场景无所谓,但如果要做严格校验,建议统一转成整数秒或固定小数位。
3.2 第二步:实现 Blockchain 类与创世块
链类负责管理区块列表、添加新区块、校验整条链。创世块是链的起点,前块哈希约定为字符串"0"。
class Blockchain: def __init__(self): self.chain = [self.create_genesis_block()] self.difficulty = 4 def create_genesis_block(self): return Block(0, time.time(), "Genesis Block", "0") def get_latest_block(self): return self.chain[-1] def add_block(self, new_block): new_block.previous_hash = self.get_latest_block().hash new_block.hash = new_block.calculate_hash() self.chain.append(new_block) def is_chain_valid(self): for i in range(1, len(self.chain)): current = self.chain[i] previous = self.chain[i - 1] if current.hash != current.calculate_hash(): return False if current.previous_hash != previous.hash: return False return Trueis_chain_valid是灵魂方法,它做两件事:重算每个块的哈希,看是否和存储的一致(检测块内数据被改);检查每个块的 previous_hash 是否等于前块的实际哈希(检测链式关系被破坏)。两个检查缺一不可。只查前者,攻击者可以改数据后重算哈希;只查后者,攻击者可以改数据但保留旧哈希字段,前者就抓出来了。
3.3 第三步:工作量证明——让挖矿真的“挖”起来
上面的add_block是直接添加,没有挖矿。现在加上工作量证明。难度difficulty=4表示哈希必须以 4 个0开头。
def mine_block(self, block): target = "0" * self.difficulty while block.hash[:self.difficulty] != target: block.nonce += 1 block.hash = block.calculate_hash() print(f"挖矿成功: {block.hash}") return block这段代码的逻辑是:不断递增 nonce,重算哈希,直到哈希前difficulty位全是 0。因为 SHA-256 的输出是随机的,你无法预测哪个 nonce 能命中,只能暴力试。难度 4 意味着平均要试 16^4 = 65536 次,现代机器零点几秒就出来了。难度 5 是 100 万次,难度 6 是 1600 万次,耗时肉眼可见地增长。
这里有个参数选择的经验:教学演示用难度 4 最合适,既有“挖矿感”,又不会让你等太久。难度 3 太快,体现不出工作量;难度 5 以上,普通笔记本要等好几秒甚至几十秒,体验差。你可以把难度做成可配置,现场演示时动态调,效果很好。
3.4 第四步:跑通全流程并观察输出
把上面拼起来,写个测试脚本:
if __name__ == "__main__": bc = Blockchain() print("创世块哈希:", bc.chain[0].hash) block1 = Block(1, time.time(), {"from": "A", "to": "B", "amount": 10}, "") bc.mine_block(block1) bc.chain.append(block1) block2 = Block(2, time.time(), {"from": "B", "to": "C", "amount": 5}, "") block2.previous_hash = bc.get_latest_block().hash bc.mine_block(block2) bc.chain.append(block2) print("链是否有效:", bc.is_chain_valid()) bc.chain[1].data = {"from": "A", "to": "B", "amount": 9999} print("篡改后链是否有效:", bc.is_chain_valid())跑起来你会看到:前两次挖矿打印出以0000开头的哈希,is_chain_valid()返回True;篡改 block1 的数据后,再校验返回False。这个True → False的转变,就是区块链防篡改最直观的证明。我每次给别人演示到这一步,对方都会“哦——”一声,比讲十页 PPT 都管用。
4. 常见问题与排查技巧实录
代码虽短,坑不少。这一章我把实际敲代码时最常遇到的几个问题整理出来,附上排查思路和解决方法。这些都是文档里不会写、但新手一定会撞上的。
4.1 哈希校验总是不通过?先查序列化一致性
最常见的现象是:明明没改数据,is_chain_valid()却返回False。九成原因是序列化不一致。比如你构造区块时字典顺序是index, timestamp, data, previous_hash, nonce,但校验时某处顺序变了,json.dumps出来的字符串就不同,哈希自然对不上。
解决办法就一条:所有参与哈希计算的字典,统一用sort_keys=True。我建议把哈希计算逻辑收敛到一个方法里,全项目只此一处,杜绝各处手写序列化。另外,data如果是嵌套字典,sort_keys只对顶层排序,嵌套层也要保证顺序一致,必要时用json.dumps(..., sort_keys=True)递归处理。
4.2 挖矿卡死或耗时过长:难度与死循环排查
如果mine_block跑了很久没输出,先检查两件事。第一,难度是不是设太高了。难度 6 平均要 1600 万次哈希,Python 纯计算大概要十几秒到几十秒。先降到 4 确认逻辑通,再往上加。第二,检查 while 条件写反没有。正确写法是while block.hash[:difficulty] != target,如果写成==,那第一次就退出,根本没挖矿;如果 target 拼错,可能永远不满足。
还有一个隐蔽的坑:nonce 没有重置。如果你复用同一个 Block 对象反复挖矿,nonce 会一直累加,虽然不影响正确性,但每次挖矿起点不同,调试时不好复现。建议每次挖矿前把 nonce 归零,或者干脆新建 Block 对象。
4.3 篡改检测失效:你可能漏了哪一步校验
有人改完数据发现is_chain_valid()还是True,慌了。排查顺序如下:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 改数据后校验仍通过 | 校验时没重算哈希,只比对了存储的 hash 字段 | 在is_chain_valid里加current.hash != current.calculate_hash() |
| 改数据后校验通过 | 只查了 previous_hash,没查自身哈希 | 两个检查都要有 |
| 改数据后校验通过 | 篡改的是最后一个块,且没重算 | 确保遍历覆盖所有块,包括尾块 |
| 改数据后校验通过 | 难度校验缺失 | 可选:校验每个块哈希是否满足难度前缀 |
我个人的习惯是,is_chain_valid里至少做三重检查:自身哈希重算、前块哈希比对、难度前缀验证。第三项能防止有人伪造一个不满足难度的块塞进来。
4.4 扩展时的注意事项:别把教学代码当生产用
最后必须泼盆冷水。这条链是教学模型,不是生产系统。它缺的东西太多了:没有数字签名,任何人都能伪造交易;没有 P2P 网络,单机自娱自乐;没有共识机制,分叉无法处理;没有内存池和手续费,交易打包逻辑为零。如果你想往真实方向走,下一步该补的是非对称加密签名(用cryptography库给交易签名)和简单的 P2P 广播(用 socket 让多个节点同步链)。但那是另一个项目了,先把这条裸链吃透,比什么都强。
我在实际教学中发现,学员把这条链敲三遍——第一遍照着抄,第二遍不看代码默写,第三遍自己加一个“交易签名”功能——之后,再去看任何区块链项目的白皮书,理解速度至少快三倍。原因很简单:骨架已经在脑子里了,剩下的都是往骨架上挂东西。