首页 / 资讯中心 / 文章详情

Python手搓区块链:从零实现区块结构与链式校验

Python手搓区块链:从零实现区块结构与链式校验 ★ FEATURED ARTICLE
1. 为什么用 Python 手搓一条链从需求到方案选型很多人第一次听到“区块链”这三个字脑子里蹦出来的要么是行情K线要么是各种矿机、钱包、交易所。但如果你把那些外围的东西全部剥掉只留下最核心的那一层会发现它其实就是一个带校验的链式数据结构外加一套让多方在不互相信任的前提下还能达成一致的规则。我当初动手写这个项目动机特别朴素市面上讲原理的文章看了一大堆什么“分布式账本”“共识机制”“默克尔树”每个字都认识连在一起就发懵。后来我干脆换了个思路——既然看不懂那就自己用 Python 撸一个最小可运行版本把每个概念都变成能跑起来的代码。这个项目适合谁我的判断是三类人。第一类是刚学完 Python 基础语法、想找个有挑战性又不至于劝退的练手项目的朋友区块链结构天然涉及类、哈希、列表、时间戳、JSON 序列化覆盖面广但难度可控。第二类是面试前想快速补齐区块链常识的开发者自己写过一遍再被问到“区块里到底存了什么”“为什么改一个区块后面全废”时你能张口就来。第三类是对密码学、分布式系统感兴趣但没时间啃论文的爱好者这个项目能给你一个可以随意折腾的沙盒。需要先明确一点这个项目做出来的是教学性质的简化区块链不是能发币、能上主网的生产系统。它没有 P2P 网络层没有真正的共识算法比如工作量证明的完整难度调整也没有钱包和签名体系。但它把区块链最骨架的东西——区块结构、哈希链接、链式校验、防篡改验证——全部实现了。你把这几个东西吃透再去看那些复杂的真实系统会发现它们无非是在这个骨架上不断加肌肉。技术选型上我几乎没犹豫直接锁定 Python 标准库。理由有三条。第一hashlib内置了 SHA-256这是比特币用的哈希算法拿来即用不需要装任何第三方包。第二json模块能帮我们把区块对象序列化成字符串再算哈希保证哈希结果稳定可复现。第三time模块提供时间戳dataclasses能让区块类的定义干净利落。整个项目零第三方依赖你复制到一个只有 Python 环境的机器上就能跑这对教学项目来说太重要了——少一个依赖就少一个劝退新手的理由。提示不要一上来就想着引入 Flask 做 Web 接口、引入 requests 做节点通信。先把单机版的链跑通、校验逻辑写对再考虑扩展。我见过太多人卡在“想做一个完整系统”的野心上结果连第一个区块都没挖出来。方案的整体思路是这样的定义一个Block类每个区块包含索引、时间戳、数据、前一个区块的哈希、自己的哈希、以及一个用于工作量证明的随机数。再定义一个Blockchain类内部维护一个区块列表提供“创建创世区块”“添加新区块”“校验整条链”三个核心方法。哈希的计算方式是把区块的关键字段拼成一个字符串喂给 SHA-256得到一个 64 位的十六进制摘要。链式校验的逻辑就是遍历每个区块重新计算它的哈希看是否被篡改同时检查它的previous_hash是否等于前一个区块的哈希。就这么点东西但已经能演示区块链最本质的防篡改特性了。2. 区块与链的核心结构拆解2.1 一个区块里到底该放哪些字段区块是整个系统的原子单元它的字段设计直接决定了后面校验逻辑能不能写干净。我最终定下来的字段有六个每一个都有它存在的理由缺一个都会让某部分功能变得别扭。index索引区块在链中的位置从 0 开始。它的作用主要是方便人类阅读和调试同时在添加新区块时用来确定“下一个区块的编号应该是多少”。严格来说光靠前向哈希也能定位区块但有个显式的序号排查问题时一眼就能看出是第几个区块出了岔子。timestamp时间戳区块被创建的时间用time.time()拿到的浮点数。它的意义在于给区块一个时间维度的锚点真实系统里时间戳还参与难度调整的计算。教学项目里它更多是记录作用但别省省了区块就失去了“何时发生”的信息。data数据这个区块承载的实际内容。在真实链上这里放的是交易列表在我们的简化版里我建议直接放一个字符串或者字典比如A转账给B 10元。数据字段是区块的“业务载荷”也是你测试篡改效果时最方便动手脚的地方。previous_hash前区块哈希这是链式结构的灵魂。每个区块都牢牢记住它前一个区块的哈希值正是这个字段把一个个孤立的区块串成了一条链。一旦前一个区块的内容有任何改动它的哈希就会变后面这个区块里存的previous_hash就对不上了链就断了。nonce随机数工作量证明的产物。在挖矿过程中我们不断改变这个数字直到算出的哈希满足某个条件比如以若干个 0 开头。教学项目里可以把它简化但保留这个字段能让读者理解“挖矿到底在挖什么”。hash自身哈希由上面所有字段共同计算得出。它是这个区块的“指纹”任何字段变动都会导致指纹变化。注意计算哈希时不能把 hash 字段本身算进去否则就成了自己引用自己的死循环。用dataclasses来定义这个类会非常清爽字段一目了然还能自动生成__init__。不过要注意dataclass默认的__eq__和__repr__在调试时挺好用但计算哈希时我们要自己控制序列化方式不能依赖默认的 repr因为字典的键顺序在不同 Python 版本或不同构造方式下可能不一致会导致同样的数据算出不同的哈希。这是个隐蔽的坑后面讲序列化时会重点说。2.2 哈希函数为什么选 SHA-256哈希函数的选择上SHA-256 几乎是教学场景的标准答案。它有三个特性对区块链至关重要。第一是确定性同样的输入永远得到同样的输出这是校验能成立的前提。第二是雪崩效应输入哪怕只改一个字符输出的 64 位十六进制串会面目全非你根本看不出改了多少。第三是单向性从哈希值反推原始输入在计算上不可行这保证了区块内容不会被轻易还原。我实测过雪崩效应有多夸张。把数据从转账10元改成转账11元哈希值从a3f...开头直接变成7c1...开头中间没有任何规律可循。正是这种“改一点点就全变”的特性让篡改者无处遁形——他想改一个区块的数据就必须重算这个区块的哈希然后还得改后面所有区块的previous_hash再重算它们的哈希工作量呈指数级增长。在 Python 里调用 SHA-256 就三行代码的事import hashlib def sha256(text: str) - str: return hashlib.sha256(text.encode(utf-8)).hexdigest()这里有个细节必须强调编码格式要统一用 UTF-8。如果你在 Windows 上默认用了 GBK在 Linux 上用了 UTF-8同样的中文数据算出来的哈希就不一样跨平台校验直接崩。我踩过这个坑当时在本地跑得好好的链换台机器一校验就报错排查了半天才发现是编码问题。所以.encode(utf-8)这个参数一定要显式写出来别偷懒。2.3 链式校验的完整逻辑校验函数是整个项目的“质检员”它要回答一个问题这条链有没有被动过手脚逻辑分两步走缺一不可。第一步是逐个区块自检。遍历链上每个区块用它的字段重新计算一次哈希和它自己存的hash字段比对。如果不相等说明这个区块的内容被改过但哈希没同步更新或者更新错了直接判定链无效。第二步是相邻区块互检。从第二个区块开始检查它的previous_hash是否等于前一个区块的hash。如果不相等说明两个区块之间的链接断了可能是前一个区块被篡改导致哈希变了也可能是当前区块的previous_hash被恶意修改。这两步必须都做。只做第一步攻击者可以改完数据后把哈希也一起改对自检就通过了但链式关系会暴露他只做第二步攻击者可以改一个区块的数据和哈希同时把后一个区块的previous_hash也改对互检也能通过但自检会发现哈希对不上。只有两步结合才能堵死所有篡改路径。注意校验函数返回布尔值就够了但调试阶段建议返回具体是哪个区块、哪一步校验失败这样排查问题效率高很多。我一开始只返回 True/False结果链一断就得手动一个个打印后来改成返回错误信息省了大量时间。3. 从零到一完整实操过程与关键代码3.1 环境准备与项目骨架环境要求低到令人发指Python 3.7 及以上即可因为dataclasses是 3.7 引入的。不需要 pip 安装任何东西标准库全包了。我建议新建一个目录里面放一个blockchain.py文件所有代码都写在这一个文件里方便阅读和分享。如果你想更规范一点可以拆成block.py、chain.py、main.py但对教学项目来说单文件反而降低了理解成本。项目骨架大致是这样先导入hashlib、json、time、dataclasses然后定义Block类再定义Blockchain类最后写一个if __name__ __main__的测试入口。这个结构清晰到不需要画图从上往下读一遍就懂。3.2 区块类的实现细节区块类的核心是哈希计算方法。我把它设计成一个实例方法compute_hash()它把 index、timestamp、data、previous_hash、nonce 这五个字段按固定顺序拼成一个字符串然后算 SHA-256。这里的关键是顺序必须固定不能这次先拼 data 再拼 index下次反过来否则同样的区块会算出不同的哈希。拼接方式我推荐用 JSON 序列化而不是简单的字符串相加。原因是字符串相加容易产生歧义比如 index 是 1、data 是 23和 index 是 12、data 是 3拼出来可能都是 123哈希就撞了。用 JSON 序列化后字段名和值之间有明确的分隔不会出现这种歧义。代码大概长这样import json import hashlib import time from dataclasses import dataclass, field dataclass class Block: index: int timestamp: float data: str previous_hash: str nonce: int 0 hash: str field(default, initFalse) def compute_hash(self) - str: block_dict { index: self.index, timestamp: self.timestamp, data: self.data, previous_hash: self.previous_hash, nonce: self.nonce, } block_string json.dumps(block_dict, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(block_string.encode(utf-8)).hexdigest()注意sort_keysTrue这个参数它保证字典的键按字母顺序排列这样无论你在哪个 Python 版本、哪台机器上跑序列化结果都一致。ensure_asciiFalse是为了让中文数据不被转义成\uXXXX虽然转义后哈希依然稳定但调试时看着一堆转义字符很痛苦关掉它可读性好很多。hash字段用field(default, initFalse)声明意思是它不参与初始化参数创建区块时不用传后面手动赋值。这样设计是为了避免“创建区块时就要知道哈希”的逻辑矛盾——哈希是算出来的不是传进来的。3.3 区块链类的三大核心方法区块链类维护一个chain列表并提供三个方法。第一个是create_genesis_block()创建创世区块。创世区块的previous_hash我习惯设为0或者空字符串表示它前面没有区块。它的 index 是 0data 可以写创世区块。第二个是add_block(data)添加新区块。流程是拿到链上最后一个区块用它的哈希作为新块的previous_hashindex 设为最后一个区块的 index 加 1timestamp 取当前时间然后执行工作量证明最后把算好的哈希赋给新块并追加到链上。第三个是is_chain_valid()校验整条链。前面已经讲过逻辑这里补充一个实现细节重新计算哈希时要调用区块自己的compute_hash()方法而不是用区块里存的hash字段。因为hash字段可能是被篡改过的只有重新算才能发现不一致。class Blockchain: def __init__(self): self.chain [] self.create_genesis_block() def create_genesis_block(self): genesis Block(0, time.time(), 创世区块, 0) genesis.hash genesis.compute_hash() self.chain.append(genesis) def add_block(self, data: str): last_block self.chain[-1] new_block Block( indexlast_block.index 1, timestamptime.time(), datadata, previous_hashlast_block.hash, ) new_block.hash new_block.compute_hash() self.chain.append(new_block) return new_block def is_chain_valid(self) - bool: 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 return True这段代码里有个容易忽略的点创世区块的哈希也要算。我见过有人创建创世区块时忘了算哈希导致它的hash是空字符串后面第二个区块的previous_hash就指向空字符串校验时第一步就挂了。所以创世区块创建完必须立刻compute_hash()并赋值。3.4 工作量证明的简化实现真实的工作量证明要求算出的哈希以若干个 0 开头这个“若干个”就是难度值。难度为 4 意味着哈希要以0000开头平均要尝试 16 的 4 次方也就是 65536 次才能碰上一个。教学项目里我建议难度设为 2 或 3既能让你感受到“挖矿需要时间”又不至于等太久。实现方式是在add_block里加一个循环不断递增 nonce重新计算哈希直到满足条件def proof_of_work(self, block: Block, difficulty: int 3): target 0 * difficulty block.nonce 0 while True: block.hash block.compute_hash() if block.hash.startswith(target): break block.nonce 1难度为 3 时平均尝试 4096 次在现代电脑上基本是瞬间完成但你能明显感觉到比难度为 0 时慢了一点点。如果你想体验“挖矿好慢”的感觉把难度调到 5平均要尝试 100 万次大概要等几秒到十几秒这个等待过程能让你对矿工的工作量有个直观感受。提示难度值不要设太高教学项目里 5 已经是上限了。我试过设成 6等了快一分钟才出一个块调试效率极低。真实比特币网络的难度是动态调整的我们这里固定住就行重点是理解机制而非追求真实。4. 踩坑实录与常见问题排查4.1 哈希不一致的三大元凶调试这个项目时最常见的报错就是“链校验失败”而失败原因九成出在哈希计算上。我总结了三类元凶按出现频率排序。第一类是序列化方式不统一。比如你在compute_hash里用了json.dumps带sort_keysTrue但在校验时又手动拼字符串两种方式算出来的哈希肯定不一样。解决办法是哈希计算逻辑只写一处所有地方都调用同一个方法绝不重复实现。第二类是编码问题。前面提过中文数据在不同编码下哈希不同。统一用 UTF-8 能解决。另外要注意json.dumps默认会把非 ASCII 字符转义虽然转义后哈希稳定但如果你在某个地方用了ensure_asciiTrue另一个地方用了False算出来的字符串就不一样。所以这个参数也要统一。第三类是浮点数精度问题。时间戳是浮点数json.dumps序列化浮点数时不同 Python 版本对精度的处理可能有细微差异。虽然概率很低但为了绝对稳定我建议把时间戳转成整数秒或者用round()固定小数位数。这个坑比较隐蔽一旦踩中很难排查因为哈希值看起来完全随机你根本不知道是哪里变了。4.2 篡改实验亲手验证防篡改特性光看代码不过瘾我强烈建议你动手做一次篡改实验这是理解区块链价值最快的方式。步骤很简单先创建一条有三个区块的链校验通过。然后手动修改第二个区块的data字段比如把转账10元改成转账100元再调用is_chain_valid()你会看到它返回False。这时候你可以进一步观察如果只改数据不改哈希自检就挂了如果你把第二个区块的哈希也重新算一遍并更新自检通过了但第三个区块的previous_hash还指向旧哈希互检会挂如果你再把第三个区块的previous_hash也改对那第三个区块的哈希又变了还得继续往后改。改一个区块后面所有区块都得跟着改这就是链式结构的威力。我实测下来三个区块的链改第一个区块的数据后要手动修复后面两个区块的哈希和指针才能让校验通过。如果链有 100 个区块这个工作量就完全不可接受了。你可以写个脚本模拟一下看看修复一条 100 区块的链需要改多少次这个数字会让你对“不可篡改”有全新的认识。4.3 常见问题速查表问题现象可能原因排查方法解决方案创世区块校验失败创建时未计算哈希打印创世区块的 hash 字段创建后立即调用 compute_hash第二个区块开始校验失败previous_hash 未正确赋值对比前块 hash 和当前块 previous_hash添加区块时用 last_block.hash中文数据哈希不稳定编码或转义不一致检查 encode 和 ensure_ascii 参数统一用 UTF-8 和 ensure_asciiFalse挖矿循环不结束难度值设得过高打印 nonce 增长情况降低难度到 2 或 3时间戳导致哈希漂移浮点数精度差异打印序列化后的字符串时间戳取整或固定小数位链校验通过但数据明显被改只改了数据没改哈希检查自检逻辑是否执行确保校验时重新计算哈希这张表里的每一行都是我实际踩过的坑尤其是“链校验通过但数据明显被改”这一条当时让我怀疑人生。后来发现是我在校验函数里偷懒直接用了区块存的hash字段去比对而没有重新计算。这个错误很隐蔽因为大部分情况下两者是相等的只有篡改后才暴露。所以校验函数里必须重新计算哈希这是铁律。4.4 性能与扩展的边界思考这个简化版区块链在性能上没什么好优化的因为它的瓶颈根本不在计算上。加一个区块的时间主要花在挖矿循环上难度为 3 时大概几毫秒难度为 5 时几秒。链的长度对校验时间的影响是线性的1000 个区块的校验也就几十毫秒完全够用。但如果你想扩展它有几个方向值得考虑。第一是把数据字段改成交易列表每个交易包含发送方、接收方、金额这样更接近真实链的形态。第二是引入简单的签名机制用hashlib配合一个共享密钥做 HMAC模拟交易签名。第三是加一个简单的 P2P 通信层用 Python 的socket模块让两个节点互相同步链这能让你理解“分布式账本”到底难在哪。不过我要提醒一句每加一个功能复杂度都是指数级上升的尤其是网络层各种边界情况能把你折磨到崩溃。我的建议是先把单机版吃透再考虑往外扩。5. 几个让代码更健壮的小技巧第一个技巧是给区块加一个to_dict()方法把区块的所有字段导出成字典方便打印和调试。调试时直接print(block.to_dict())比看对象的内存地址友好一万倍。这个方法还能复用在哈希计算里避免字段列表写两遍导致不一致。第二个技巧是在区块链类里加一个print_chain()方法格式化输出整条链的摘要信息每个区块一行显示 index、data 前几个字符、hash 前几位。这样你一眼就能看出链的结构和哈希分布排查问题时效率翻倍。第三个技巧是把难度值做成可配置的参数而不是硬编码在方法里。这样你可以在测试时用低难度快速跑通流程在演示时用高难度展示挖矿过程。配置方式可以是一个类属性或者从命令行参数读取怎么方便怎么来。第四个技巧是写单元测试。虽然教学项目不强制但给compute_hash和is_chain_valid写几个测试用例能帮你在改代码时快速发现回归问题。比如测试“相同输入哈希相同”“修改数据后校验失败”“添加区块后链长度增加”这几个场景用 Python 内置的unittest就够了不需要装 pytest。我在实际使用中发现这个项目最大的价值不在于代码本身而在于它逼着你去思考“信任”这件事。当你亲手改了一个区块的数据然后眼睁睁看着整条链校验失败时你会突然理解为什么区块链被称为“信任机器”。它不依赖任何中心机构只靠数学和结构就让篡改变得几乎不可能。这种体验是看一百篇文章都给不了的。最后再分享一个小技巧如果你想直观感受哈希的雪崩效应可以写个循环把数据从a改到z每次打印哈希的前 8 位。你会看到这 26 个哈希值毫无规律地散布在十六进制空间里没有任何渐变或聚集。这个实验只要十行代码但视觉效果非常震撼拿去给不懂技术的人演示区块链原理比任何PPT都管用。
阅读完成 · 觉得有帮助?
咨询建站