ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零手写极简区块链:用Python实现PoW、哈希校验与SQLite持久化

从零手写极简区块链:用Python实现PoW、哈希校验与SQLite持久化 在教程里把区块链讲明白的人很多但能带着你从零手写一条链、把每个设计决策都讲清楚的人不多。最近正好有朋友在学 Python又总被区块链到底是啥这类概念绕晕我就拉了一个实战项目用纯 Python 实现一个极简区块链。不需要装任何框架不需要懂密码学只要会写类、函数、循环跟着这篇文章走一遍你就能亲手跑出一条带工作量证明、能持久化、能校验篡改的链出来。这个项目最值钱的部分不是写出来而是在写的过程中理解区块链为什么这么设计为什么区块要存上一个区块的哈希为什么挖矿要不断改 Nonce为什么说链上的数据一旦写入就很难改这些在教科书里用三句话带过的概念落到代码里全是细节。适合谁来读有 Python 基础、对区块链感兴趣但一直停留在看文章阶段的开发者或者想转 Web3、想搞懂底层原理的朋友。我不讲代币也不讲交易所只讲数据结构和逻辑放心很安全的。看完之后你能自己写一个带命令行交互的区块链 Demo也能更懂真实区块链项目里的技术取舍。1. 内容整体设计与思路拆解1.1 最小可行区块链到底需要几个类很多入门教程一上来就上 Flask、上 P2P 网络、上共识算法结果读者跑都跑不起来。我的思路是做减法先砍到只剩单机版本只有两个类——一个Block区块一个Blockchain区块链跑通核心逻辑之后再谈扩展。这就跟学 Web 开发先写个hello world接口而不是直接上微服务是一个道理。那这两个类里必须有什么区块有三个核心字段逃不掉索引index、时间戳timestamp、哈希hash、前一个区块的哈希previous_hash。当年中本聪的白皮书上写得很清楚每个区块包含上一个区块的哈希值这个哈希把整个链条串起来形成一种牵一发而动全身的结构。我在设计时还给区块预留了一个data字段用来存交易信息或者任意文本。很多教学版会叫它transactions但为了保持纯粹先用一个字符串就够了——你可以把它理解成这个区块里记的账哪怕内容是pay Alice 10 BTC也行后续要扩展成真正的交易列表时把这个字段改成二维数组即可。关于哈希字段我遇到过一个设计陷阱很多人会把哈希写在区块类的构造函数里也就是创建时就算好。这在单机 Demo 里问题不大但如果你想模拟真实矿工的行为区块哈希应该由矿工挖矿成功后再来计算并填充这样你的Block才更贴近真实逻辑——先有内容再有哈希内容变了哈希就会变。1.2 为什么哈希串能锁住整条链这是整个项目里最核心、也最容易讲不清的地方。一个区块的哈希是怎么算出来的我用的是SHA-256Python 标准库hashlib里自带的不需要额外安装任何东西。关键点在于这个哈希的输入包含了上一个区块的哈希。想象一下你有一串锁链A 锁着 BB 锁着 C。如果有人想把 B 里的数据偷偷改掉那么 B 的哈希一定会变——因为 B 的哈希是基于 B 的内容算出来的。B 的哈希变了C 里面存的previous_hash就跟 B 的新哈希对不上于是 C 就断链了。你要想让链条看起来正常就必须把 C 的哈希也改掉而 C 一变D 又对不上……这样一直改到链尾等于重写了整条链。这在实际当中有个很直观的检查方法我写了个is_chain_valid()函数遍历整条链对每个区块重新算一遍哈希然后比对区块里存储的哈希再比对previous_hash和上一个区块的哈希是否一致。只要有一处不一致立刻返回False。教学版的区块链其实就这么简单——验证一个区块链是否合法不是去看链上数据真不真实而是验证哈希链是否连续。安全说明这里用 SHA-256 不是为了防黑客而是为了让读者直观理解哈希不可逆和雪崩效应一个字符变了输出的 64 位十六进制字符串面目全非。这在后续调试的时候特别有用能帮你快速确认我到底改没改数据。1.3 为什么要有工作量证明把出块速度控制住如果只是简单地计算哈希那挖矿就太容易了一个区块创建出来哈希一算完事想多少块就多少块而且人人都能瞬间产出大量区块区块链毫无价值。所以真实区块链引入了工作量证明Proof of WorkPoW你必须找到一个特殊的哈希让这个哈希值满足一定的条件比如开头必须有一定数量的 0。这个条件是怎么实现的我在代码里定义了一个difficulty变量比如difficulty 4意思就是新出的区块哈希值必须以至少 4 个 0 开头。为了让哈希满足这个条件你只能改区块里的一个东西——Nonce随机数。每试一次 Nonce 就把整个区块内容重新算一次哈希直到撞上一个满足条件的 Nonce 为止。这个过程就叫挖矿本质是暴力穷举。类比一下就像你有一个密码锁密码是 4 位数字你只能从 0000 到 9999 挨个试直到打开为止。不同点在于区块链的密码锁不是人设计的而是规则设计的任何人都能快速验证你找到的 Nonce 是否满足条件但想反推出满足条件的 Nonce 只能穷举——这就是算力的来源。我把挖矿逻辑做得逼真了一点不光是改 Nonce 算哈希我还打印了尝试次数和耗时让你直观看到难度调高 1 位耗时可能涨十倍。这对理解真实比特币网络的难度调整很有帮助。1.4 工具选型只用标准库零第三方依赖这个项目我刻意没有用任何第三方库。Python 自带的hashlib提供 SHA-256json用来序列化argparse用来做命令行参数sqlite3用来持久化datetime处理时间戳。这些全部是 Python 标准库你不需要pip install任何东西这对初学者极其友好。为什么不用web3.py、flask这些听起来很酷的库因为它们是面向生产环境的工具不是教学工具。你把 Flask 加进来你的注意力就会被路由、端口、JSON 响应带走而忘了你本来的目的是理解哈希和链。什么时候该引入等你把这个纯 Python 版本吃透了再上 Flask 做一个 HTTP 接口把区块链通过 API 暴露出去成为多节点网络那才是有价值的扩展。我还专门测试过 Python 版本兼容性我本机跑的是 Python 3.10但代码里没用任何 3.9 的新语法所以Python 3.7 及以上应该都能跑。如果你是 Python 老版本2.7 之类的建议先装个 Python 3因为这个项目用到了f-string和类型注解2.7 跑不了别浪费时间。2. 核心细节解析与实操要点2.1 区块类的实现字段、构造函数、哈希计算先上代码这是整个项目的基石。我加了详细的中文注释你边看边敲每个字段都有它的用处别偷懒省略。import hashlib import json from datetime import datetime class Block: def __init__(self, index: int, data: str, previous_hash: str, nonce: int 0) - None: index : 区块的编号高度 data : 区块里存的内容教学版里就是一个字符串 previous_hash : 上一个区块的哈希关键字段 timestamp : 区块创建的时间 nonce : 挖矿时用来穷举的随机数 hash : 当前区块的哈希挖矿成功后赋值 self.index index self.data data self.previous_hash previous_hash self.timestamp datetime.now().strftime(%Y-%m-%d %H:%M:%S) self.nonce nonce self.hash self.compute_hash() def compute_hash(self) - str: 计算当前区块的 SHA-256 哈希。 序列化的字段index, timestamp, data, previous_hash, nonce 注意hash 字段本身不能参与计算否则会自引用。 block_string json.dumps({ index: self.index, timestamp: self.timestamp, data: self.data, previous_hash: self.previous_hash, nonce: self.nonce }, sort_keysTrue) return hashlib.sha256(block_string.encode(utf-8)).hexdigest()这个类是后面所有逻辑的地基有三个细节值得展开说说。第一为什么用json.dumps而不是直接字符串拼接直接拼接当然也行但json.dumps能把字典转成一个带统一格式的字符串而且我加了sort_keysTrue保证字段顺序固定。区块链的哈希计算对输入序列化的方式极其敏感——同一种数据如果拼字符串的顺序不同算出来的哈希就完全不同。用了 JSON我就能保证不管代码怎么重构序列化出来的字符串形式始终一致可复现性更强。第二时间戳用字符串而不是浮点数。真实项目里一般用time.time()返回的浮点数但教学版里人眼可读的字符串更能直观告诉你这个区块是什么时候创建的。代价是精度低但无所谓因为后面对比哈希时用的是完整的字符串不是时间戳的值。注意这里有个坑如果你在极短的时间内连续创建两个区块它们的时间戳可能完全一样但哈希不会相同因为 index 和 nonce 不同。第三compute_hash是公开方法不是私有方法。原因很关键挖矿过程中每穷举一个 Nonce 都要重新计算哈希所以这个方法要能反复调用。把哈希计算单独抽出来也让后面重新计算哈希以验证链是否合法变得方便。如果你把哈希计算逻辑直接写在构造函数里后面验证和挖矿的逻辑都会很别扭。2.2 区块链类的骨架创世区块、新块创建、链校验有了Block类接下来写Blockchain类。这个类负责管理整条链核心方法有三个创世区块的创建、添加新区块、验证链的合法性。class Blockchain: def __init__(self) - None: self.chain [] self.pending_data [] self.difficulty 4 self.create_genesis_block() def create_genesis_block(self) - None: 创世区块是链上的第一个区块它没有前驱 所以 previous_hash 人为指定为 0 * 64。 genesis_block Block(0, Genesis Block, 0 * 64) genesis_block.hash genesis_block.compute_hash() self.chain.append(genesis_block) def last_block(self) - Block: return self.chain[-1] def add_block(self, data: str, nonce: int 0) - Block: 创建一个新区块previous_hash 取当前链上最后一个区块的哈希。 注意这个是简化版本真正的矿工挖矿逻辑在后面的线程里。 previous_hash self.last_block().hash new_block Block(len(self.chain), data, previous_hash, nonce) self.chain.append(new_block) return new_block def is_chain_valid(self) - bool: 遍历整条链做两层校验 1. 每个区块的哈希是否等于按内容重新计算的哈希。 2. 每个区块的 previous_hash 是否等于上一个区块的哈希。 for i in range(1, len(self.chain)): current_block self.chain[i] previous_block self.chain[i - 1] if current_block.hash ! current_block.compute_hash(): print(f区块 {i} 的哈希不一致数据可能被篡改) return False if current_block.previous_hash ! previous_block.hash: print(f区块 {i} 的 previous_hash 与上一个区块哈希不一致) return False return True这一版我故意写得很直白add_block直接接受 data 和 nonce没有让矿工自己去找 Nonce。因为先跑通链的创建和校验再上工作量证明你会更容易分清哪些逻辑是纯数据结构的哪些是共识机制加进来的。创世区块有一个细节previous_hash我用了0 * 64也就是 64 个 0。为什么是 64因为 SHA-256 输出的十六进制字符串就是 64 个字符乱设一个短字符串虽然不影响逻辑但不够像样。真实区块链比如比特币的创世区块 previous_hash 全为零这是约定俗成的做法。写到这里你可以做一个小测试bc Blockchain() print(创世区块:, bc.chain[0].hash) block1 bc.add_block(转账 5 元给 Alice) block2 bc.add_block(转账 3 元给 Bob) print(链是否合法:, bc.is_chain_valid())正常输出应该是链是否合法: True。然后你手动改一下bc.chain[1].data 篡改再验证一次你大概就能直观感受到什么叫改一个区块引爆整条链的哈希连续性了。2.3 千万别忽略的两个细节字符串编码与哈希连续初学者在写这个类的时候最容易踩两个坑。第一个坑是encode缺省编码导致跨平台哈希不一致。在compute_hash里我写的是block_string.encode(utf-8)。如果你写成.encode()在大多数系统上默认是 utf-8但在 Windows 某些区域设置下可能会变成 gbk导致同样的区块内容算出不同的哈希。这在国内开发环境里尤其常见——你换一台电脑跑同一个代码哈希全都对不上排查半天发现是编码问题。以后凡是涉及哈希、签名这类对字节敏感的地方一律显式指明utf-8。第二个坑是列表直接 appendBlock对象和 append 序列化结果的差异。后面讲到持久化时你会看到SQLite 存的是 JSON 字符串而内存里存的是Block对象。很多教程把这两者混在一起讲导致新手以为chain里面存的是字典结果遍历的时候取block[hash]直接KeyError。我在这个项目里让内存和存储的表示分开内存始终是Block对象持久化是 JSON 字符串规则清晰后期扩展也不会乱。3. 实操过程与核心环节实现3.1 环境准备Python 与项目管理如果你想在自己电脑上动手跑起来建议按这个顺序操作。检查 Python 版本终端输入python --version确认是 3.7 以上。没有就先去官网下载安装安装时记得勾选Add Python to PATH这步不做后面有得折腾。创建项目目录我习惯建一个干净的目录比如simple-blockchain/。新建代码文件我拆成了blockchain.py核心逻辑和main.py命令行交互别把所有东西塞进一个文件不是不行但工程上拆开好维护。运行验证python main.py --help能跑通就说明环境没问题。如果你用的是 VS Code建议装 Python 扩展然后用CtrlF5运行而不是 F5 打开调试器不然每一步停下来等调试会非常慢。另外强烈建议打开 VS Code 的设置里python.linting.enabled: true默认的 Pylint 能帮你提示一些常见错误。我第一次跑的时候就是环境出了问题版本是Python 2.7然后傻傻地去跑代码报错f-string is not supported整个流程卡死。这里也建议各位跑任何 Python 教学项目的第一步都在终端里python --version确认一下别默认自己的 Python 版本就是新的。3.2 核心实现加进工作量证明和挖矿逻辑为了模拟挖矿这种真实逻辑我把Block改造成了支持挖矿的版本。class Block: # ... 前面的字段不变 def mine_block(self, difficulty: int) - None: 挖矿不断改变 nonce重新计算哈希直到哈希值前 difficulty 位全是 0。 import time start time.time() target 0 * difficulty attempts 0 while self.hash[:difficulty] ! target: self.nonce 1 self.hash self.compute_hash() attempts 1 elapsed time.time() - start print(f挖矿完成尝试次数: {attempts}, 耗时: {elapsed:.2f}秒, 哈希: {self.hash})这个函数是整个项目里最出戏的地方跑起来你会看到终端刷屏一样的尝试次数。但请你忍住好奇心先设置difficulty 4大约几秒就能挖出来如果你一上来就设成difficulty 6那可能要等上一两分钟纯属自己折腾自己。Blockchain.add_block也要相应调整def add_block(self, data: str) - Block: previous_hash self.last_block().hash new_block Block(len(self.chain), data, previous_hash) new_block.mine_block(self.difficulty) self.chain.append(new_block) return new_block这样每次添加区块都会触发一次小小的挖矿nonce完全由内部逻辑自己找外部不需要传。你会看到区块哈希不再是随便算出来的而是满足前四位全是 0 的哈希——这看起来就很像真实的链了。难度调整策略真实区块链是会动态调整难度的但教学版不用那么复杂一个静态的difficulty 4就够。我的经验是如果添加前几个区块时已经感觉有明显的停顿超过 5 秒就把 difficulty 调回 3基本保持秒出块的速度就行。等到你后面想体验难度暴涨的恐怖感时再调高不迟。3.3 核心实现用 SQLite 持久化让数据不丢现在你的链还在内存里只要程序一关全没了。真实区块链的数据是存在节点本地数据库里的比特币用的是 LevelDB以太坊也类似。我们教学用直接用 SQLite它是 Python 自带的最简单的关系型数据库文件型存储不需要单独起服务。我建了一个表blocksCREATE TABLE IF NOT EXISTS blocks ( index INTEGER PRIMARY KEY, data TEXT NOT NULL, previous_hash TEXT NOT NULL, timestamp TEXT NOT NULL, nonce INTEGER, hash TEXT NOT NULL )然后是保存和读取函数import sqlite3 DB_FILE blockchain.db def save_chain(chain: list) - None: conn sqlite3.connect(DB_FILE) cursor conn.cursor() cursor.execute(DELETE FROM blocks) # 清空旧数据 for block in chain: cursor.execute( INSERT INTO blocks VALUES (?, ?, ?, ?, ?, ?), (block.index, block.data, block.previous_hash, block.timestamp, block.nonce, block.hash) ) conn.commit() conn.close() def load_chain() - list: conn sqlite3.connect(DB_FILE) cursor conn.cursor() cursor.execute(SELECT * FROM blocks ORDER BY index) rows cursor.fetchall() conn.close() chain [] for row in rows: block Block(row[1], row[2], row[4]) # index, data, previous_hash block.timestamp row[3] block.nonce row[4] block.hash row[5] chain.append(block) return chain这段代码里有几个容易踩的坑我得提前给你打个预防针。第一save_chain里的DELETE FROM blocks。如果你的链是新增了几个区块然后保存但链上总共已经有 10 个区块直接插入 10 条如果不清空下次加载时就会把旧数据和旧数据混在一起。除非你能保证每次保存都是全量覆盖否则必须清空。我的做法是全量覆盖简单不容易出错。第二load_chain里恢复Block对象时的参数顺序。Block类的构造函数是__init__(self, index, data, previous_hash, nonce0)所以Block(row[1], row[2], row[4])对应的是indexrow[1]、datarow[2]、previous_hashrow[4]。这里row的顺序必须在建表和查询时保持一致不然恢复出来的区块哈希、Nonce 全不对一跑is_chain_valid()就崩。建议你在写完后打印一下row的所有字段确认顺序不出错。第三读取的时候不需要重新计算哈希因为哈希已经存在了库里。加载时直接把原来的哈希赋回去即可。注意这里有个信任问题数据库中存的 hash 可能是被篡改过的所以任何人调用is_chain_valid()时它会重新计算哈希然后比对不能因为你从数据库读到了hash字段就无条件相信它。3.4 命令行入口做一个可交互的小工具为了让项目像个东西我加了一个main.py支持几个命令用argparse解析参数。这样你不需要改代码直接在终端里操作方便得多。# 显示整条链的信息 python main.py --show # 添加一个区块区块内容是转账 5 元 python main.py --add 转账 5 元 # 校验链的完整性 python main.py --validate # 保存到 SQLite python main.py --save # 从 SQLite 加载 python main.py --load核心逻辑import argparse import sys from blockchain import Blockchain, save_chain, load_chain def main(): parser argparse.ArgumentParser(description极简区块链命令行工具) parser.add_argument(--add, help添加新区块内容为传入的字符串) parser.add_argument(--show, actionstore_true, help打印整条链) parser.add_argument(--validate, actionstore_true, help验证链的合法性) parser.add_argument(--save, actionstore_true, help保存区块链到 SQLite) parser.add_argument(--load, actionstore_true, help从 SQLite 加载区块链) args parser.parse_args() bc Blockchain() if args.load: bc.chain load_chain() print(已从数据库加载区块链。) if args.add: bc.add_block(args.add) print(f区块已添加内容: {args.add}) if args.show: for block in bc.chain: print(fIndex: {block.index}) print(fData: {block.data}) print(fPrevious Hash: {block.previous_hash}) print(fHash: {block.hash}) print(fNonce: {block.nonce}) print(- * 40) if args.validate: if bc.is_chain_valid(): print(✅ 链合法) else: print(❌ 链不合法) sys.exit(1) if args.save: save_chain(bc.chain) print(已保存到 SQLite) if __name__ __main__: main()注意我的--validate参数如果链不合法就sys.exit(1)这是一种程序员的习惯——命令行的退出码 0 表示成功非 0 表示失败这样你在 CI 或者别的系统里可以方便地判断命令是否执行成功。如果你开发的时候懒得管退出码也可以写成print完直接 return看个人习惯。有交互感地跑一遍这串命令你的终端体验大概是这样的$ python main.py --add 转账 5 元给 Alice 挖矿完成尝试次数: 1523, 耗时: 0.28秒, 哈希: 0000a27f... $ python main.py --add 转账 3 元给 Bob 挖矿完成尝试次数: 4568, 耗时: 0.91秒, 哈希: 0000e4c1... $ python main.py --show ... $ python main.py --validate ✅ 链合法当你看到0000开头的哈希时你应该有一种卧槽这真的是区块链的成就感。所以我一直觉得——先跑出来再谈理论比先看三天理论再动手强太多。4. 常见问题与排查技巧实录4.1 问题一区块哈希永远对不上先检查你修改的是哪个对象有朋友跑完代码后觉得不过瘾尝试改一下链上的某个区块bc Blockchain() bc.add_block(A) bc.add_block(B) bc.chain[1].data 篡改数据 print(bc.is_chain_valid())按理说is_chain_valid()应该返回False结果返回了True然后他一头雾水来找我。排查后发现他改的是bc.chain[1]而is_chain_valid遍历的是self.chain其实这是因为他的data字段压根没改进去而是改了Block对象的一个临时属性。其实真正的问题在于区块链类的add_block方法里有没有把新的区块 append 进链。我之前写的简化版本里add_block是先创建一个Block再append顺序搞反了导致bc.chain[1]不是你想要的那个对象。排查这种问题最直接的方式是打印对象的idprint(id(bc.chain[1])) # 看看链上的对象id是多少 block1 bc.chain[1] block1.data 篡改数据 print(id(block1)) # 看看你修改的对象id是多少两个id不一致说明你不是在改链上的那个对象。这种问题在 Python 里经常出现大部分情况下不是区块链逻辑错了而是你把引用和对象搞混了。4.2 问题二哈希长度不对或者类型报错有次我让学生去单独算一个区块的哈希他直接打印出了hash的值function Block.compute_hash at 0x000001...注意他犯了两个错误第一他打印的是block.compute_hash而不是block.compute_hash()第二他误把hash这个字段和compute_hash这个方法名搞混了。在 Python 里类属性hash存的是字符串方法compute_hash是函数对象——如果你不小心在代码里写了self.hash compute_hash而不是self.hash compute_hash()整个类都乱套。检查手段很简单打印block.hash时判断是不是一个长度为 64 的十六进制字符串如果不是基本就是这个错误。另外如果你在Block中定义了属性hash之后又调用hash(...)内置函数会报类似TypeError: str object is not callable的错误这是命名冲突建议把属性名改成block_hash而不是hash能省去很多麻烦。4.3 问题二点五粘到代码里的中文引号这个坑不是逻辑问题而是编辑器或输入法造成的神奇 Bug代码里出现了中文全角引号“”程序跑到那一行直接SyntaxError: invalid character in identifier。这种报错不算多但会浪费你不少时间。我之前也遇到过好多次仔细一看代码里某行的字符串边界明明应该是结果变成了”。解决办法就是让编辑器高亮显示特殊字符VS Code 里按CtrlShiftP输入 render whitespace 打开渲染空格中文标点就会以不同的颜色显示一眼就能找出来。4.4 问题三挖矿难度调高之后运行太慢卡死当你把difficulty 6然后跑add_block程序会像死机一样卡住好几分钟。其实它还在算也许再过一会就能算出来了。别慌这是正常现象。真实挖矿就是干这个事的只不过真实网络难度动辄要求几十个零开头普通电脑根本算不出来。如果你只是想做功能演示我强烈建议你把difficulty设置为3甚至2控制在 1 秒以内如果你想感受卡顿感可以开difficulty 5体验一下。实在等不了就按CtrlC中断然后把难度调低重来。4.5 问题四is_chain_valid()里previous_hash对不上这种问题分两种可能一是你的previous_hash在add_block的时候取错了比如取的是self.chain[-2]而不是self.chain[-1]二是因为你在创建创世区块之后又手动在链上插入或者删除过区块。出现这种问题最简单的调试办法是在is_chain_valid()的循环里增加打印每遍历到一个区块就把当前哈希、前哈希、上一个区块的哈希都打印出来肉眼比对一下差异。有一个非常隐蔽的坑是你修改了链上的区块之后忘了更新这个区块的hash字段。结果你一算发现compute_hash()和新值对不上但其实数据已经变过了只是哈希没更新所以报错的时候会乱指。这种情况建议你在代码开头加入如果 data 变化就重新计算哈希的逻辑或者干脆每次验证前都重新计算一遍哈希再比对存储值。4.6 问题五SQLite 读取出来的链莫名其妙不合法有朋友加载完数据库之后运行is_chain_valid()立刻报错。我用load_chain()恢复区块时把timestamp和nonce赋值回去了但恢复previous_hash的时候用了错误的字段顺序。比如Block构造函数是(index, data, previous_hash, nonce)但你给的时候previous_hash位置放的是hash结果所有区块的前哈希全错了。排查方式非常直接打印load_chain()出来的第一个非创世区块和前一个区块的hash字段比对一下。第二个可能性是你保存和读取的数据库不是同一个文件。比如先在一个目录下运行--save又在另一个目录下运行--load那肯定加载不到。4.7 技巧把验证脚本写成常驻测试一劳永逸我不想每次手动改区块、手动验证于是我写了一个简单的测试函数def test_chain(): bc Blockchain() bc.add_block(A) bc.add_block(B) assert bc.is_chain_valid() True, 新增区块后链应该合法 bc.chain[1].data 篡改 assert bc.is_chain_valid() False, 篡改后链应该不合法 print(全部测试通过) if __name__ __main__: test_chain()每次改完代码运行一遍这个脚本立刻知道有没有把链的基本逻辑改坏。这也算是对回归测试的一个极简体验以后你扩展功能比如加交易、加 Merkle 树改完代码第一件事就是跑一下这个测试确保原来的链逻辑没被破坏。5. 给真实应用的扩展建议与个人体会5.1 从单机到网络你需要 P2P 和共识协议把这个教学区块链变成真正能用的区块链现实距离还挺远。但方向其实很清晰无非是两条主线和一条暗线。主线一是P2P 网络层。真实区块链是一大堆节点各自维护一份数据副本不是一台服务器。如果你想让多个节点同步就得让节点之间互相广播新区块、互相请求完整链、解决分叉。教学版里最简单的是用 Flask 起 HTTP API每个节点暴露/blocks和/mine接口然后节点之间用轮询或者 WebSocket 同步。这点我有空可以单独写一篇但思路不难。主线二是共识机制。单机版里只有你一个节点你说链合法它就合法。真实系统里可能有几万个节点都持有一份不同的链大家必须通过某种规则比如最长链规则来决定哪个版本才是真的。这部分才是共识算法PoW、PoS 等真正发挥威力的地方也是区块链去中心化的灵魂。教学版里没有共识因为没必要但你要是想深入必须先明白单机版只是数据结构网络版才是分布式系统。暗线是数据模型。现在一个区块只有一条data字符串真实区块里是一个交易列表每个交易有输入、输出、金额、签名。这涉及 UTXO 或账户模型、数字签名、Merkle 树。这一块我建议大家先学 Merkle 树因为它的思路和哈希链一脉相承但要更灵活——哈希链保证区块不可篡改Merkle 树保证区块里的交易不可篡改且能快速验证。5.2 从教学版到生产版你要补的安全功课一个教学项目做到这里我正式提醒你永远不要用这个代码存真正的资产数据甚至连演示资产都别存太多。因为这里缺了太多安全机制——私钥、签名、验证节点身份、防止双花、内存腐蚀防护……这些一步不到位整个系统都是纸糊的。个人建议接下来不要急着写代码先把比特币白皮书和以太坊黄皮书翻一遍看不懂专业术语没关系但你至少能对工作量证明最长链Merkle 树有个整体印象。然后回到代码里试着加入数字签名模块Python 的ecdsa库或cryptography库都行让每笔交易都带上签名这应该就是下一步最有意思的挑战。5.3 这个项目对 Python 新手意味着什么我当初学 Python 时靠递归和爬虫建立起我能写代码的信心但真正让我理解代码能设计出可靠系统的就是这种数据结构类的小项目。这个极简区块链跑通之后你会发现自己对 Python 类、哈希、JSON、列表这些基础知识的理解都通透了——因为你在真实场景里用到了它们而不是背概念。对我个人而言做这种项目最大的乐趣是一个在新闻里被神化的概念被我拆成了几个类、几个方法输出成一段段可以打印的字符串。那层神秘感剥掉之后区块链就不再是玄学而是一个你可以随意改动、随时调试、不断重构的玩具。最后再分享一个小技巧如果你想让自己的项目看起来更完整可以在README.md里画一个简单的流程图然后把你跑通的终端输出截图放上去再把difficulty 4那种恰到好处的挖矿耗时写进文档。这样以后你面试聊到区块链项目就能从容地说我从零手写过一条链包括 PoW 挖矿、哈希校验和 SQLite 持久化底气非常足。
返回列表