☰
用Python从零实现极简区块链:哈希、挖矿与防篡改
2026/9/28 8:56:13 网站建设 项目流程

用Python写一个区块链,是我在带教学项目时反复做的一件事。很多人一听"区块链"就想到比特币、以太坊、共识算法、P2P网络这些庞大概念,但其实如果你只想理解它的核心机制,一个跑在命令行里的几十行Python脚本就够了。这篇文章会带你从零搭一个真正能挖矿、能校验、能防篡改的极简区块链Demo,不需要任何框架,只要解释器版本在Python 3.8以上就行。读完你不仅能复现,还能清楚地知道每个设计决策背后的原因。

1. 先想清楚:区块链的最小模型到底由哪几块拼成

很多人第一反应是去看比特币白皮书,或者去啃共识算法、Merkle树、椭圆曲线签名,结果五分钟劝退。我的建议是反过来——先做一个能跑的"假"区块链,再回头看那些理论就全通了。

1.1 去掉花哨,只剩下四个部件

一个最小可运行的区块链,本质上只需要四样东西:

  • 区块(Block):装着交易数据、时间戳、随机数(nonce)以及自己哈希值的容器
  • 链(Chain):按顺序链接起来的区块列表,每个区块都记录前一个区块的哈希
  • 工作量证明(Proof of Work):让"生成新块"变得有成本,简单版就是暴力搜索一个nonce,使区块哈希满足特定条件
  • 校验逻辑(Validation):从头遍历整个链,检查哈希是否连续、数据有没有被篡改

如果你把这四样东西用Python分别实现,每个部分其实都不超过三十行代码。真正的区块链比这个复杂得多,但骨架就是上面这副样子。

1.2 哈希:那条看不见的"锁链"

区块链为什么叫"链"?因为每个区块的头部都保存了previous_hash,指向前一个区块的哈希值。只要你改了链上任何一个区块的交易数据,那个区块的哈希就变了,后面所有区块的previous_hash都会对不上,校验立刻失败。

我把这个关系类比成一个班级排座位:每个人都记住前一个同学是谁,如果中间有个人偷偷换成了别人,后面所有人的记忆就全乱套了。哈希在这里起的作用,就是给每个区块盖一个唯一且不可伪造的指纹,只要原始内容动过一个字节,指纹就完全不同。

1.3 为什么Python适合写教学版

选Python不是因为它能上生产环境,而是因为三个字:可读性。区块链的核心思想是"如何用密码学手段建立信任",如果语言本身还要纠结指针、内存管理,新手很容易把注意力放错地方。Python的hashlib库原生支持SHA-256,数据结构用字典和列表就能表达,写出来的代码几乎能逐行对应到理论概念上。

提示:如果你已经熟悉面向对象,可以直接看第2节;如果刚入门,建议先动手把Block类抄一遍,再往后读。抄代码不丢人,关键是抄完要能回答"为什么要有一个compute_hash()方法"。

2. 动手写Block类:把数据结构先钉死

在写任何逻辑之前,我习惯先把数据结构定义清楚。Block是整个链条的基本单元,它需要同时承载业务数据和链上链接数据。

2.1 字段设计:哪些该放进区块,哪些不该

我用的最小字段如下:

import hashlib import json import time class Block: def __init__(self, index, transactions, timestamp, previous_hash, nonce=0): self.index = index # 区块在链上的位置,从0开始 self.transactions = transactions # 交易列表,教学版用list存字典 self.timestamp = timestamp # 出块时间 self.previous_hash = previous_hash # 前一个区块的哈希,链的粘合剂 self.nonce = nonce # 工作量证明的随机数,后面细说 self.hash = self.compute_hash() # 当前区块的哈希

这里有几个值得注意的设计选择:

  • index看起来多余,但调试时非常有用。遍历链条时,你一眼能看出坏在第几个区块。
  • transactions在真实系统里是一组经过签名的交易对象,教学版为了简单,直接用字典列表。比如[{"sender": "alice", "receiver": "bob", "amount": 5}]。
  • previous_hash必须是字符串,不能是None,因为创世块的上一任是虚空,我习惯用"0"表示。
  • timestamp用time.time(),单位是秒,注意同一秒内多个块可能出现相同时间戳,但这不影响教学逻辑。

不放进区块的东西同样重要:比如余额、账户列表这些状态,真实区块链需要借助"世界状态"来管理,在教学版里先省略,否则逻辑复杂度立刻上升。

2.2 计算哈希的细节:为什么用sort_keys=True

compute_hash是整个链的信任根基,实现如下:

def compute_hash(self): block_string = json.dumps({ "index": self.index, "transactions": self.transactions, "timestamp": self.timestamp, "previous_hash": self.previous_hash, "nonce": self.nonce }, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest()

三个小细节务必理解:

  1. 必须把数据序列化成字符串再哈希。SHA-256接收的是字节流,不是Python对象。这里用json.dumps把区块内容变成JSON文本,再.encode()成UTF-8字节。
  2. sort_keys=True不是可选项。字典的键顺序在Python 3.7以后虽然默认保持插入顺序,但如果你在某次运行时改了字段添加顺序,同样的内容会算出不同哈希。加上sort_keys=True,JSON输出按字母序排列,保证哈希只依赖内容本身。
  3. 哈希值用hexdigest(),得到64位十六进制字符串。SHA-256输出256位二进制,用十六进制展示最方便比较。

很多新手会问:为什么不能只哈希transactions?因为如果两个区块内容恰好相同,哈希会一样,链条就无法区分它们。把index、timestamp、previous_hash、nonce全部纳入哈希,才能保证每个区块的指纹唯一。

2.3 创世块与链的初始化

有了Block类,接下来定义链本身。教学版通常用一个类管理整个链条:

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, [], time.time(), "0") genesis_block.hash = genesis_block.compute_hash() self.chain.append(genesis_block)

create_genesis_block是链的入口,它没有前序区块,previous_hash用"0"占位。为什么不让self.chain直接为空?因为校验逻辑需要从某个锚点开始,如果链是空的,last_block属性会直接报错,很多测试也会变得别扭。

注意:genesis_block.hash在__init__里已经被compute_hash赋值了一次,你在create_genesis_block里又赋了一遍。第一次赋值时nonce=0,第二次同样nonce=0,结果一样。这里刻意保留两次赋值,是为了让代码更清楚地表达"创世块也是算出来的,不是写死的"。

3. 工作量证明:让"挖矿"变成一行看得懂的循环

如果说哈希是区块链的脊梁,工作量证明就是让这条脊梁能站起来的力量。没有它,任何人可以随便制造一堆假区块把链撑爆——反正算一个哈希只要微秒级。

3.1 难度、nonce和"反复抽卡"的逻辑

工作量证明的思路非常朴素:要求新区块的哈希值必须以difficulty个0开头。因为哈希是不可预测的,你只能不断更换nonce重新计算,直到撞上一个满足条件的哈希为止。

我把这个过程类比成开盲盒:哈希就像盲盒里的编号,我们规定只有编号以4个0开头的盲盒才算有效,那你就必须一个个拆,拆到满足条件为止。验证方只需要看一眼编号前缀就知道对不对,根本不用重复拆盒过程——这就是"计算有成本,验证零成本"的核心。

3.2 挖矿函数实现:给Block类补上mine方法

在Block类中加入mine方法:

def mine(self, difficulty): prefix = "0" * difficulty while not self.hash.startswith(prefix): self.nonce += 1 self.hash = self.compute_hash()

逻辑就三行:

  1. 构造目标前缀。
  2. 只要当前哈希不满足前缀条件,nonce加一。
  3. 重新计算哈希,再检查。

这里有个关键点:初始hash已经是nonce=0时算出来的。如果它恰好满足条件,循环一次都不执行,直接成功。否则nonce=1、nonce=2……一直试下去。你可以加一行打印看进度,但教学版保持简洁。

然后在Blockchain类中添加挖矿入口:

def add_transaction(self, sender, receiver, amount): self.pending_transactions.append({ "sender": sender, "receiver": receiver, "amount": amount }) def mine_pending_transactions(self): new_block = Block( index=len(self.chain), transactions=self.pending_transactions, timestamp=time.time(), previous_hash=self.last_block.hash ) new_block.mine(self.difficulty) self.chain.append(new_block) self.pending_transactions = [] return new_block @property def last_block(self): return self.chain[-1]

3.3 难度调整:4个前导零意味着什么

difficulty=4表示哈希前缀必须是"0000"。每增加1个前导零,平均尝试次数变成原来的16倍。算一下:前缀4个0的期望尝试次数是16^4 = 65536次。我的笔记本上,这个难度大约耗时0.2到1秒,非常适合教学演示——既让人感受到"确实在计算",又不至于等太久。

如果设成6,期望次数是16^6 = 1677万次,单块可能要挖几分钟;设成1,基本瞬出,又失去了"工作量"的感觉。建议课堂演示用4,自己练习可以试3和5的差别。

提示:真实比特币的难度会动态调整,目标是保证平均出块时间约10分钟。教学版不需要那么复杂,一个固定难度就够你理解原理了。动态调整反而是后面做扩展时更有意思的课题。

4. 链的完整性校验与防篡改测试

到这里你已经有了一个能生成区块的链条。但这个链条到底安不安全?至少要能回答两个问题:能不能改历史数据?改完之后系统能不能发现?

4.1 校验逻辑拆解:一个is_valid方法做到的三件事

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(): return False if current.previous_hash != previous.hash: return False prefix = "0" * self.difficulty if not current.hash.startswith(prefix): return False return True

这段代码在每个区块上做了三件事:

  1. 重算哈希比对:检查当前区块内容是否被修改。任何字段变动都会让compute_hash()结果对不上current.hash。
  2. 检查链接:current.previous_hash必须等于前一个区块的hash,这保证链的连续性。
  3. 检查工作量证明:每个区块哈希必须满足难度前缀,防止有人插入一个不经挖矿就生成的假块。

为什么从range(1, ...)开始而不是range(0, ...)?因为创世块没有前序块,特殊处理会引入额外分支,直接跳过它是教学版最常见的简化。但你必须在心里清楚:创世块本身是链的信任锚点,如果创世块被篡改,整个链的信任就崩塌了。真实系统的创世块是硬编码在全节点里的。

4.2 篡改实验:当你偷偷改掉一笔交易

写一个测试脚本看看效果:

if __name__ == "__main__": bc = Blockchain() bc.add_transaction("alice", "bob", 10) bc.add_transaction("bob", "carol", 3) bc.mine_pending_transactions() bc.add_transaction("carol", "alice", 2) bc.mine_pending_transactions() print("校验结果(一切正常):", bc.is_valid()) # 篡改第一笔交易 bc.chain[1].transactions[0]["amount"] = 999 print("校验结果(篡改后):", bc.is_valid())

输出:

校验结果(一切正常): True 校验结果(篡改后): False

这就是防篡改的直观体验。注意:你确实改了区块内容,但current.hash还是原来那个旧值,compute_hash()算出来的新值已经完全不同,所以第一道检查就把你拦住了。

更有意思的是第三种情况:篡改之后,如果有人把current.hash也同步改成新值呢?这时候第一道检查会通过,但第二道检查会失败——因为下一个区块的previous_hash仍然指向旧哈希。如果你想把previous_hash也全改掉,那就要从篡改点开始把后面所有区块全部重新挖一遍,这就是工作量证明的意义所在:让"重挖整条链"变得昂贵,从而保护历史记录不被轻易改写。

4.3 一个容易忽略的边界:空交易区块

在挖矿时,如果pending_transactions为空,我们依然可以挖出一个"空块"。这在教学演示里经常遇到,比如:

bc.mine_pending_transactions() # 忘记添加交易

此时新区块包含一个空列表。is_valid依然返回True,因为区块链本身不要求每个块都有交易。这不是bug,而是简化带来的自然行为。真实区块链对空块有自己的奖惩机制,但教学版不必纠结。

不过有个地方需要小心:pending_transactions = []这行代码,如果是pending_transactions.clear()效果相同,但如果你在其他地方持有了这个列表的引用,=赋值会让原引用继续指向旧列表,可能引发诡异bug。我在教学现场见到过不止一次,后面第5节会专门讲这个坑。

5. 从单节点到能演示的Demo:命令行交互与持久化

写到这里,链已经能跑能验证了。但直接跑Python脚本只有print输出,不够直观。我习惯给它加一个极简的命令行交互层,让用户可以输入交易、挖矿、查看链、保存链。

5.1 编写一个简单的REPL循环

def repl(): bc = Blockchain() while True: cmd = input("\n命令: (t)转账 (m)挖矿 (v)验证 (p)打印 (s)保存 (q)退出: ").strip().lower() if cmd == "t": sender = input("发送方: ") receiver = input("接收方: ") amount = float(input("金额: ")) bc.add_transaction(sender, receiver, amount) print("交易已加入待打包列表") elif cmd == "m": block = bc.mine_pending_transactions() print(f"新区块 #{block.index} 已生成,哈希: {block.hash}") elif cmd == "v": print("链有效!" if bc.is_valid() else "链无效!检测到篡改!") elif cmd == "p": for b in bc.chain: print(b.__dict__) elif cmd == "s": save_chain(bc) elif cmd == "q": break if __name__ == "__main__": repl()

这个REPL的教学价值在于,它强迫学生理解"转账"和"入链"是两个动作。很多人一开始以为add_transaction会立刻变成链上数据,实际上它只是放进待打包区,必须等mine_pending_transactions才会真正写进新块。这个流程和真实区块链中"交易进入交易池、矿工打包出块"的概念是一致的。

5.2 JSON序列化与链保存

区块链的数据结构天然适合JSON,但Block对象不是纯字典,需要转一下:

def block_to_dict(block): return { "index": block.index, "transactions": block.transactions, "timestamp": block.timestamp, "previous_hash": block.previous_hash, "nonce": block.nonce, "hash": block.hash } def save_chain(bc, filename="blockchain.json"): data = [block_to_dict(b) for b in bc.chain] with open(filename, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) print(f"链已保存至 {filename}")

读取时对应地恢复:

def load_chain(filename="blockchain.json"): bc = Blockchain() with open(filename, "r", encoding="utf-8") as f: data = json.load(f) bc.chain = [] for item in data: block = Block( index=item["index"], transactions=item["transactions"], timestamp=item["timestamp"], previous_hash=item["previous_hash"], nonce=item["nonce"] ) block.hash = item["hash"] bc.chain.append(block) return bc

读取时要注意一个顺序问题:Block.__init__会自动调用compute_hash()算出self.hash,但你读入的是别人挖好的区块,它的nonce可能已经很大,算出来的哈希理应等于文件里的hash。保险起见,加载后最好跑一次is_valid()确认文件没被手改过——这正好又是一个防篡改的演示场景。

5.3 我实际踩过的坑:内存对象、精度与暗中截断

这一节想聊我在带学员和自测时真正踩过、也最容易被忽略的几个坑。

坑一:pending_transactions = []带来的引用问题

在mine_pending_transactions中,我把self.pending_transactions作为交易列表传给了Block。然后清空时用了self.pending_transactions = []。如果外面有人早就持有了pending_transactions的引用,比如你用ptr = bc.pending_transactions把地址记下来,赋值清空后ptr仍然指向旧列表——区块里已经包含了旧列表,不会有问题,但下一次新增交易时你可能会惊讶:怎么ptr里还残留着上一轮的数据?

改用self.pending_transactions.clear()会更语义化,它是在原列表上清空元素,所有引用都会反映清空结果。这个细节在代码上只有一行之差,排查起来却能折腾半小时。

坑二:time.time()的浮点数精度

timestamp用浮点数保存,JSON序列化时没问题,但如果多次重算哈希,浮点数在不同平台上的位表示可能略有差异。不过大多数情况下同一台机器算出的json.dumps结果是一致的。更麻烦的反而是:教学演示时大家复制代码,有人改成time.time()放在mine之后,结果每个块的时间戳都几乎相同,打印时看起来就像同一秒挖出的,容易引起"到底有没有挖矿"的疑问。

坑三:忘记重新赋值hash导致校验永远失败

新手在手动修改区块后常会写下block.compute_hash(),然后困惑为什么is_valid()还是False。因为compute_hash()只是返回值,并不会自动更新block.hash。正确做法是block.hash = block.compute_hash()。这一点值得反复强调,是整个流程中最容易出现的操作失误。

坑四:不恰当的float金额

我最初用float存交易金额,比如0.1 + 0.2的经典精度问题会悄悄出现。教学版当然无所谓,但如果你想做得干净,金额改成整数分(int)保存,比如10代表0.1元,或者直接存Decimal。真实区块链大多有固定的最小单位,避免浮点数,这个意识越早建立越好。

6. 这个"玩具"还能往哪个方向长

到这一步,你已经拥有一个带挖矿、校验、持久化的极简区块链。它不能上生产环境,但作为理解区块链原理的教学工具已经足够扎实。如果想继续深入,我整理过三条我认为性价比最高的路线。

6.1 从单节点到多节点的思路

最值得做的扩展是引入"多个节点"。简单的做法是:每个节点都保存一份链的副本,新增区块时广播给其他节点。代码上你只需要一个简单的Peer类,用HTTP API接收新区块和交易。真实网络里的共识算法就是这层设计的复杂版,但在教学阶段,你甚至可以用flask的几行接口让两个终端进程互相同步。多节点跑通之后,你再去看"最长链规则""孤儿块""分叉"这些概念,会发现它们都是被需求逼出来的合理设计。

6.2 教学项目的三条进阶路线

  • 路线一:把交易升级成账户模型。加一个简单的余额映射表,转账时检查余额是否足够。这相当于实现最简单的状态机,会逼迫你面对"如何保证交易原子性"的问题。
  • 路线二:给交易加签名。用ecdsa或cryptography库让用户用私钥签名交易,其他人用公钥验证。这一步会真正建立起"密码学在区块链中的角色"的认知,而不是停留在哈希层面。
  • 路线三:实现动态难度。比如根据前若干个区块的出块平均时间,自动上调或下调difficulty。这是一个特别好的算法题,既涉及数学统计,又涉及工程策略。

带过几轮学生之后,我发现自己最大的感受是:区块链本身并不神秘,它只是一群经过精心设计的数据结构,加上密码学工具,组合出了一套"难以篡改的账本规则"。用Python把最小模型跑通之后,再去看以太坊黄皮书、比特币代码,至少你知道他们讨论的每一个部件,在你自己的玩具链里都能找到对应物。这就是我推荐你先动手写一个的原因——纸上得来终觉浅,绝知此事要躬行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询