从零手写区块链,听起来像是个大工程,但真动手做一遍,你会发现核心逻辑其实并不复杂。这篇文章我打算用Python带你一步步搭一个最小可用的区块链,包含区块结构、工作量证明(PoW)、链的校验这些关键环节,全程代码可运行。适合那些看过不少区块链概念、但一直没真正写过代码的开发者,也适合刚学完Python基础、想找个有挑战性的练手项目的朋友。
我先说结论:一个教学用的区块链,核心代码量不会超过两百行。难点不在于写代码,而在于理解为什么区块要这样设计、哈希在其中扮演什么角色、工作量证明到底在防什么。这篇文章会把这些“为什么”讲清楚,再配上能直接跑起来的代码。
1. 项目整体设计:这到底是在做一个什么东西
1.1 先把目标定清楚
这个项目不是要做一个生产级的区块链系统,那涉及P2P网络、分布式共识、加密签名、存储引擎,工作量是另一个量级。我们的目标是:在单机上用Python实现一条区块链的核心数据结构和共识逻辑,让它具备区块链最本质的三个特征——可追溯、防篡改、需要付出成本才能记账。
说得更具体一点,完成这个项目后,你应该能回答这几个问题:
- 一个区块里到底存了哪些字段,每个字段是干嘛的?
- 为什么说“一旦写入就难以篡改”?篡改一个区块会引发什么连锁反应?
- 挖矿的“工作量”体现在哪?为什么需要改一个随机数反复计算哈希?
- 如何验证一条链是健康完整的?
把这些问题想透了,再去接触真实区块链项目(比如比特币源码、各类联盟链框架),你会觉得亲切得多。因为不管系统多复杂,底层那套“区块+哈希+链式结构+共识代价”的骨架是相通的。
1.2 为什么选Python而不是Go、Java
选Python的理由很直接:表达力强、上手快,能把注意力放在区块链本身的逻辑上,而不是跟语言特性较劲。
- 标准库的
hashlib直接提供SHA-256,不需要引第三方包。 json模块做序列化很方便,哈希计算需要的是稳定、可重现的字符串输入,JSON天然合适。- 动态类型让代码写起来很流畅,区块、链用类或者字典都能表达,灵活度大。
当然,Python不是性能最优的选择,真实区块链几乎没有用纯Python写的。但作为教学和验证思路,Python完全够用。我之前也考虑过用Go写,并发模型确实适合区块链的节点通信场景,但对刚入门的朋友来说,Go的工程复杂度容易喧宾夺主。我们的目标是用最少的代码、最短的路径把区块链的主干逻辑跑通,那就果断用Python。
1.3 技术选型上的一些考虑
哈希算法选了SHA-256,这是业界最常用的选择。它输出固定长度的256位摘要,具备两个关键特性:
- 单向性:给你一个哈希值,你几乎不可能反推出原始输入。
- 雪崩效应:输入哪怕只改一个字符,输出的哈希值会面目全非。
这两个特性是区块链防篡改的第一道防线。后面你会看到,区块中任何字段被改动,都会导致该区块的哈希变化,进而让整条链的校验失败。
数据结构上,我直接用一个Python列表来存区块。列表天然有序,正好对应区块链“链式追加”的特点。真实系统中会用更复杂的存储结构,但教学场景下,列表是最直观的。
数据格式上用JSON序列化区块内容。这里有一个容易踩的坑:Python字典的键顺序不固定,直接对字典进行json.dumps,不同运行环境下键的顺序可能不一样,导致同样的内容算出不同的哈希。解决办法是json.dumps时加上sort_keys=True,保证序列化结果是确定性的。这个小细节,可以在后面的代码里看到。
2. 核心原理解剖:区块、哈希与工作量证明
2.1 一个区块到底长什么样
一个区块就是一个数据容器。我设计的每个区块包含以下字段:
| 字段 | 含义 | 作用 |
|---|---|---|
index | 区块在链中的位置(高度) | 保证顺序 |
timestamp | 区块生成时间 | 记录时间信息 |
data | 区块存储的业务数据 | 实际内容,比如交易记录 |
previous_hash | 上一个区块的哈希 | 把区块串成链的“链环” |
nonce | 随机数 | 工作量证明的变量 |
hash | 当前区块的哈希 | 当前区块所有内容的摘要 |
理解区块链,关键在于previous_hash和hash这一对字段。每个区块的哈希,是把当前区块所有字段(除了hash自己)打包后做SHA-256得到的。而previous_hash指向了上一个区块的哈希。这样一来,每个区块就像手拉手的一串人,后面的人紧紧抓着前面人的手。
如果中间某个区块的数据被篡改了,它的哈希会变,那么所有后续区块里记录的“上一个哈希”就对不上了。这种连锁反应,就是区块链防篡改的本质。
创世区块(第一个区块)比较特殊,它没有前驱,previous_hash我一般用一个全零字符串"0"表示。
2.2 哈希在这里到底起了什么作用
有人可能会问:我直接把整个区块内容存下来不也一样吗?为什么非得整个哈希?
三个原因:
第一,哈希是身份的浓缩。区块内容可能很大,但哈希只有固定长度。寻找一个区块时,拿着哈希比对,效率高得多。
第二,哈希是校验的币。给别人校验链的时候,不需要传完整内容,传哈希就能验证数据有没有被动过。
第三,哈希是共识的载体。后面讲工作量证明你就会看到,我们需要找到一个满足特定条件的哈希值。哈希的不可预测性,保证了这个“找”的过程必须付出真实的计算成本。
用个生活类比:哈希就像一个人的身份证号。你说你是某个人,我只要比对身份证号就能确认。如果你整容了、改名字了,身份证号就变了,那所有关联的记录立刻能发现不对劲。
2.3 工作量证明:让记账这件事变贵
在区块链里,不允许任何人随随便便就添加一个区块,否则攻击者可以瞬间写出几百万个伪造区块,把链撑爆。所以需要一种机制:添加区块要付出代价。
工作量证明的经典实现是这样的:
- 系统设定一个目标:新区块的哈希值必须以若干个零开头。
- 因为哈希结果无法预测,矿工只能不断改变区块里的
nonce值,重新计算哈希,直到碰运气撞出满足条件的哈希。 - 一旦找到,广播给全网,其他人验证一下哈希确实满足条件,就承认这个区块合法。
目标里前导零的个数,决定了难度。每多一个零,难度变成原来的16倍(因为一位十六进制数有16种可能)。我用4个前导零做默认难度,平均需要计算16^4 = 65536次才能挖出一个区块。这个数字在单机上需要跑几百毫秒,演示起来很有“挖矿感”,又不会等太久。
为什么这种机制能防攻击?因为攻击者要想篡改区块然后重新挖矿,需要把后面所有区块全部重新挖一遍,计算成本随时间线性增长。而诚实的节点只需要在原有链上继续挖新块。算力不足的话,攻击者的链永远赶不上诚实的链。这就是所谓的“算力为王”。
2.4 难度值怎么定才合适
挖矿难度不是拍脑袋定的,要考虑演示场景的实际体验。我测试过不同难度的时间表现:
| 难度(前导零个数) | 平均尝试次数 | 单块挖矿耗时(普通电脑) |
|---|---|---|
| 3 | 4096 | 约30毫秒 |
| 4 | 65536 | 约400毫秒 |
| 5 | 1048576 | 约6秒 |
| 6 | 16777216 | 约90秒 |
这个项目我用4,节奏舒服:既能让人明显感受到“挖矿需要等待”,又不会等得无聊。你可以自行调整difficulty参数,感受一下难度和耗时的指数关系。
3. 实操:从零手写区块链
3.1 环境准备
只需要一个Python环境,我推荐3.8及以上版本。全程只用标准库,不需要安装任何第三方依赖。用到的模块:
hashlib:计算SHA-256json:序列化区块内容time:生成时间戳
代码组织上,我建议分成三个文件:
block.py:区块类定义blockchain.py:区块链类定义main.py:演示和测试
在开发过程中,遇到不确定的地方,就把中间量打印出来看。这一步做对了,后面debug能省不少力气。
3.2 定义区块结构
# block.py import hashlib import json 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.compute_hash() def compute_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()几个关键点:
__init__里先默认nonce=0,算出一个初始哈希。挖矿过程中会不断改nonce,再重新算哈希。compute_hash是幂等的方法,给定区块内容,输出永远一致。这保证了验证的时候可以随时重算。sort_keys=True必须加。如果不加,同样的区块内容在不同解释器下可能得到不同的哈希。
3.3 实现区块链与工作量证明
# blockchain.py import time from block import Block class Blockchain: def __init__(self): self.chain = [] self.difficulty = 4 self.create_genesis_block() def create_genesis_block(self): genesis = Block(0, time.time(), "Genesis Block", "0") self.chain.append(genesis) def get_latest_block(self): return self.chain[-1] def proof_of_work(self, block): target = "0" * self.difficulty while not block.hash.startswith(target): block.nonce += 1 block.hash = block.compute_hash() return block def add_block(self, data): latest = self.get_latest_block() new_block = Block( index=latest.index + 1, timestamp=time.time(), data=data, previous_hash=latest.hash ) mined_block = self.proof_of_work(new_block) self.chain.append(mined_block) print(f"第 {mined_block.index} 个区块已挖出,nonce 为 {mined_block.nonce}")这里有几个设计决策:
创世区块用"0"作为previous_hash。它是链的起点,没有前驱,这个全零字符串是行业里常见的惯例写法。
proof_of_work直接修改传入的block对象。因为在前面已经通过compute_hash算过初始哈希,这里循环判断它是否满足目标条件,不满足就递增nonce重算。这个循环就是“挖矿”的本质。
add_block方法把流程串起来:取最新区块作为前驱、创建新块、挖矿、入链。
3.4 验证链的完整性
# blockchain.py 内继续添加 def is_valid(self): for i in range(1, len(self.chain)): current = self.chain[i] previous = self.chain[i - 1] if current.hash != current.compute_hash(): print(f"区块 {current.index} 的内容被篡改,哈希不一致") return False if current.previous_hash != previous.hash: print(f"区块 {current.index} 的前哈希与区块 {previous.index} 不匹配") return False return True验证逻辑就两条:
- 当前区块的哈希,必须等于把当前区块内容重算得到的结果。如果有人改了
data、timestamp、nonce任意一个字段,重算出来的哈希立刻对不上。 - 当前区块记录的
previous_hash,必须等于上一个区块的哈希。这保证了链的连续性。
两者都通过,说明整条链自洽完整。
3.5 数据封装与演示入口
# main.py import time from blockchain import Blockchain def main(): b = Blockchain() data_list = [ "A 转给 B 10元", "B 转给 C 5元", "C 转给 A 3元" ] for data in data_list: b.add_block(data) time.sleep(0.5) # 避免多个区块时间戳完全相同 print("当前链长度:", len(b.chain)) for block in b.chain: print(f"索引 {block.index}, 数据: {block.data}, 哈希: {block.hash}") print("链完整性校验:", b.is_valid()) if __name__ == "__main__": main()这里加了一个time.sleep(0.5),是一个小细节:区块链本身不依赖时间戳排序(我们用index排序),但时间戳完全相同的相邻区块,看起来会有点奇怪,观感不佳。实测间隔0.5秒就够了。
3.6 完整项目代码结构
python-blockchain/ ├── block.py ├── blockchain.py └── main.py三个文件加起来不到150行,逻辑非常清晰。你完全可以合并成一个文件,不过分文件组织更贴近真实工程的习惯,也方便后续扩展。
4. 运行测试:看看区块链是怎么工作的
4.1 正常运行的输出
在终端执行python main.py,类似的输出:
第 1 个区块已挖出,nonce 为 2847 第 2 个区块已挖出,nonce 为 563 第 3 个区块已挖出,nonce 为 12341 当前链长度: 4 索引 0, 数据: Genesis Block, 哈希: 5d8a5f2b9f2c3a1e... 索引 1, 数据: A 转给 B 10元, 哈希: 00ab31f2cfe1a2d4... 索引 2, 数据: B 转给 C 5元, 哈希: 0045bf12e0aa33c1... 索引 3, 数据: C 转给 A 3元, 哈希: 0007d9a12b5c8f21...注意观察:创世区块的哈希不是以00开头的,而后面挖出来的区块哈希都以至少4个零开头。这说明创世区块是系统直接初始化的,不需要经过工作量证明;而其他新区块都经过了挖矿确认。
每次运行的nonce会完全不同,因为时间戳在变,哈希碰撞的结果也在变。这是正常的,别担心。
4.2 篡改测试:区块链的报警机制
光看运行输出还不够,我们来做一个关键实验:人为篡改一个区块,然后调用is_valid,看看会发生什么。
# tamper_test.py from blockchain import Blockchain b = Blockchain() b.add_block("第一条交易") b.add_block("第二条交易") print("篡改前校验结果:", b.is_valid()) # 试图篡改第一个数据区块的内容 b.chain[1].data = "篡改后的交易记录" print("篡改后校验结果:", b.is_valid())运行结果:
篡改前校验结果: True 第 1 个区块的内容被篡改,哈希不一致 篡改后校验结果: False这个实验直观地演示了区块链防篡改的精髓。只改了data一个字段,compute_hash的结果就完全不同了,校验逻辑立刻抓到异常。而且后续所有区块也都会跟着校验失败,因为它们的previous_hash指向的是旧哈希,跟新算出来的对不上。
这给我们的启示是:区块链防的不是“不能改”,而是“改了就会被发现”。在共识网络里,一旦发现假链,整个网络会拒绝它,作恶者丢掉的不仅是信誉,还有投入的算力和时间。
4.3 尝试调整难度,感受共识的成本
把blockchain.py里的difficulty改成5,再运行一次。你能明显感觉到时间变长了,每个区块的nonce数值也大得多。
这正好验证了前面说的指数关系:难度从4升到5,只增加了一个前导零,但平均尝试次数从6万多次飙升到100多万次。为什么要这样设计?因为哈希结果不可预知,谁都无法“抄近路”,只好老老实实穷举。这就确保了记账权的获取成本是真实存在的。
5. 常见问题与排查实录
5.1 常见问题速查表
| 问题 | 表现 | 排查方向 |
|---|---|---|
| 哈希一直不满足条件 | 挖矿死循环,程序卡死 | 检查difficulty是否太大;检查nonce是否正确递增 |
| 两个区块哈希一样 | 校验失败或者数据混乱 | 检查timestamp是否相同;检查序列化是否用了sort_keys |
| 校验总是不通过 | is_valid始终返回False | 检查previous_hash是否正确传递;检查操作的是不是同一个区块对象 |
| 挖矿速度太慢 | 每个区块要等几十秒 | 降低difficulty;确认没有在循环里意外重置nonce |
| 数据里有中文报编码错误 | 字符串encode时报错 | Python 3默认UTF-8,一般不会;确认终端编码正常 |
5.2 我踩过的几个坑
第一个坑:json序列化时键顺序不稳定。刚开始我直接对dict做json.dumps,在自己的电脑上没问题,换个环境跑就出现哈希不一致。排查半天发现是键顺序变了。sort_keys=True一出马,问题立刻解决。这个教训告诉我:凡是涉及哈希计算的序列化,输入必须是确定性的。
第二个坑:修改了nonce却忘了重算hash。写第一版挖矿循环的时候,我天真地以为改完nonce,hash会自动更新。结果当然不是,区块永远满足不了条件。调试的时候,我在循环里打印了block.hash,发现从头到尾没变过,才意识到要重新调用compute_hash赋值。
第三个坑:测试时把sleep加错了地方。我一开始把sleep放在挖矿循环里,结果每个nonce都等0.1秒,挖一个区块要等一个多小时。后来才醒悟,sleep只是为了隔开时间戳,应该放在每次交易之间,而不是放在挖矿循环里。
5.3 对初学者的一些建议
如果你照着代码敲完,发现自己卡在某个地方,我建议按这个顺序排查:
- 先确认
compute_hash能正常算出64位十六进制字符串。 - 打印创世区块的哈希,确认它不以
00开头(这是正常的)。 - 单独调
proof_of_work挖一个区块,观察nonce的变化。 - 最后再跑整体流程。
还有个小技巧:把difficulty先设为2,跑通整个流程,看到效果后再调回4或5。难度2几乎瞬间出块,适合先验证流程,确认没问题再体验真正的挖矿。
5.4 后续可以怎么扩展
这个最小区块链跑通之后,往上加东西的思路就很清晰了:
- 添加交易池:
data里面不再直接存一条记录,而是存一个交易列表。 - 实现地址与签名:用非对称加密生成地址,交易要附上签名,这是真实区块链的标配。
- P2P网络:用WebSocket或HTTP接口,让多个节点同步链数据。
- 持久化:把链存储到SQLite或文件里,重启程序后仍然存在。
- 动态难度:根据最近区块的挖矿时长,自动调节
difficulty,维持稳定的出块速率。
每一步都值得单独写一篇实战记录,编程的乐趣就在于把最小模型一点点扩展成接近真实的东西。
写在最后的体会
把这段代码跑通的那一刻,我对区块链的理解有了一个质的变化。以前看各种科普文章,知道“区块链是不可篡改的账本”这句话,但不知道为什么不可篡改。真正自己写了校验逻辑、亲手改坏一个区块看到报错提示,才彻底明白:所谓不可篡改,其实是“篡改会留下痕迹、需要付出高昂代价”。我建议你也亲手把某个区块的数据改掉,然后看着is_valid返回False,这种体验比读十篇概念文章都来得深刻。
最后分享一个小技巧:把整个项目推到一个代码仓库里,按一次提交一个功能节点的节奏提交代码,从“定义区块结构”到“实现挖矿”再到“添加校验”,每个阶段的提交信息都写清楚。过一个月再回来看这些提交,你能清晰地看到自己当时是怎么一步步把区块链这个抽象概念落成代码的,那种感觉,本身就是学习编程最珍贵的东西。