ARTICLE DETAIL

资讯详情

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

用Python从零实现一个迷你区块链:从哈希到工作量证明

用Python从零实现一个迷你区块链:从哈希到工作量证明 1. 从零开始为什么我用Python写了个“玩具区块链”区块链这个词近几年都快被聊烂了。但很多人聊的是比特币价格、NFT、元宇宙这些外围概念真正动手写过一条链的人其实少得可怜。我当初学的时候也有这个困惑——网上资料一大把但大部分要么是纯理论要么直接用现成的框架看完还是不知道区块链内部到底怎么转的。后来我决定自己动手用Python从零写一个“简化版区块链”。Python语法简单、上手快而且不需要像C那样跟指针和内存管理较劲非常适合用来理解区块链的核心逻辑。我这个项目不依赖任何第三方库只用了Python标准库里的hashlib、json、time这几个模块纯手写几百行代码就能跑起来。这个项目能做什么说白了就是让你亲手实现一条迷你区块链包含区块的生成、SHA-256哈希计算、工作量证明PoW挖矿、链的完整性校验、以及一个简单的HTTP接口供外部调用。你可以把它当成一个学习工具也可以在此基础上继续扩展——比如加交易、加P2P网络、做持久化存储都是后话。适合谁看我觉得有三类人最有收获一是刚入门区块链、想搞懂“哈希到底怎么串成链”的开发者二是写过点Python、想找个有点意思的小项目练手的同学三是做技术分享时需要一个通俗易懂的demo的讲师或博主。读完这篇你能照着代码一步步敲出来并且清楚每一行在干什么、为什么这么写。先亮一下最终效果运行起来后程序会自动挖矿产生创世块和后续区块每个区块有一个时间戳、一个区块索引、一段数据、一个工作量证明的随机数以及最重要的——前一个区块的哈希值。这条链一旦中间任何一个区块的数据被篡改整条链的校验就会失败这就是区块链“不可篡改”的底层原理。2. 核心概念速通哈希、区块、工作量证明到底是什么2.1 哈希区块链的“指纹”技术先别急着看代码有几个概念必须搞明白。第一个就是哈希。哈希函数你可以把它理解成一个“指纹生成器”。你给它输入任意长度的数据它吐出一个固定长度的字符串——在SHA-256算法里这个字符串是64位的十六进制数。关键是它有两个特性一是同样的输入永远得到同样的输出二是输入稍微改一个字符输出就完全变样而且看不出任何规律。我经常用一个比喻哈希就像是给数据盖了个不可伪造的章。你要验证一份文件有没有被动过手脚只要重新算一遍哈希和原来的对比一下就知道了。区块链里每个区块都保存了前一个区块的哈希值就等于把每个区块“钉”在了它前面的区块上一环扣一环想要改中间任何一环后面的所有环节都会被牵动。Python里算SHA-256特别简单hashlib库封好了import hashlib def sha256(data): return hashlib.sha256(data.encode(utf-8)).hexdigest()2.2 区块结构区块链的“积木块”每个区块就是这条链上的一个积木块。它存了四样核心信息索引index第几个区块从0开始数时间戳timestamp区块生成的时间数据data可以是一笔转账记录、一条日志、任何你想存的东西前一个区块的哈希previous_hash这是链接的关键随机数nonce挖矿时反复调整的值区块还有一个自己的哈希通常是把上面这些字段拼在一起整体做一次SHA-256计算得到的。注意这个hash字段严格来说不算区块的“内容”而是由内容推导出来的“结果”。在真正的比特币实现里区块头还会包含难度目标、默克尔根等字段我这个简化版先把最核心的骨架保持住。2.3 工作量证明让“挖矿”变得真实可感工作量证明Proof of Work是区块链最精妙的设计之一。它的核心思想是你想往链上添加一个新区块必须先证明你付出了计算成本。具体做法是要求区块的哈希满足一定条件比如“哈希的前4位必须全是0”。哈希是没法预测的你只能不停地改nonce重新计算直到撞出一个符合条件的哈希为止。这个过程就是“挖矿”那个符合条件的nonce就是“工作量证明”。难度可以动态调节。前4位是0平均要试2的16次方次也就是65536次改成前5位是0就要试2的20次方次约100万次。比特币根据全网算力每两周调整一次难度确保平均出块时间稳定在10分钟左右。我这个demo里把难度设成了4位0几秒钟就能挖出一个块既能看到挖矿过程的“真实感”又不至于等太久。生活化类比这就像你请人帮你做一个拼图必须拼出一个特定图案才收工。工作量证明不是“做出来很难验证”而是“做出来很容易验证”——你只要看看这个哈希是否以0开头一秒钟就能判断但找到这个nonce却要花大量计算。这种不对称性就是整个区块链安全性的基石。3. 动手写代码区块类与区块链类的完整实现3.1 先从区块类开始代码我建议按这个顺序来写先定义区块再定义区块链最后加API接口。我写代码的时候习惯先把核心数据结构想清楚因为后面所有逻辑都是围绕数据在转。下面这个Block类就是上面说的那个积木块import hashlib import json import time class Block: def __init__(self, index, timestamp, data, previous_hash, nonce0): 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_keysTrue).encode() return hashlib.sha256(block_string).hexdigest()这里有个细节值得注意我用json.dumps的时候加了sort_keysTrue。为什么要排序因为字典在Python里是有序的3.7之后但为了确保序列化结果在不同环境下完全一致显式排序是更稳妥的做法。哈希计算最忌讳的就是“同样的内容算出不同结果”那整个链就乱套了。compute_hash方法在每次初始化时自动调用把当前所有字段计算成一个哈希值存到self.hash里。这样这个区块的哈希就能唯一反映它的全部内容——一旦有人改了data、改了nonce、改了时间戳重新计算出来的哈希就和存的不一样了篡改行为立刻暴露。3.2 区块链类把“积木”串成“链”接下来是主角Blockchain类。它负责管理整条链维护区块列表、生成创世块、挖矿、校验链的完整性。class Blockchain: def __init__(self): self.chain [] self.difficulty 4 # 哈希前4位为0 self.create_genesis_block() def create_genesis_block(self): genesis_block Block(0, time.time(), Genesis Block, 0) self.chain.append(genesis_block) property def last_block(self): return self.chain[-1] def proof_of_work(self, block): block.nonce 0 computed_hash block.compute_hash() while not computed_hash.startswith(0 * self.difficulty): block.nonce 1 computed_hash block.compute_hash() return computed_hash def add_block(self, block, proof): previous_hash self.last_block.hash if previous_hash ! block.previous_hash: return False if not self.is_valid_proof(block, proof): return False block.hash proof self.chain.append(block) return True def is_valid_proof(self, block, block_hash): return block_hash.startswith(0 * self.difficulty) and block_hash block.compute_hash() 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.compute_hash(): return False # 当前区块记录的previous_hash是否等于前一个区块的哈希 if current.previous_hash ! previous.hash: return False return True这里面有几个设计决策我想展开说说。create_genesis_block是创世块也就是链上的第一个区块。它没有前一个区块所以previous_hash随便给了一个字符串0。这个块特殊就特殊在它没有父区块是整条链的起点。任何人都可以从一个统一的创世块开始同步整条链。proof_of_work方法实现了前面说的工作量证明。它接收一个区块对象不断调整nonce重新计算哈希直到满足难度条件。这里有个小优化我修改的是block.nonce然后调用block.compute_hash()这样每次都会把最新的nonce包含进哈希计算里逻辑上是正确的。add_block是添加新区块的“校验门”。它不是无脑追加而是先验证两件事第一这个区块的previous_hash是不是当前链上最后一个区块的哈希防止插入断层第二这个区块的哈希是否满足工作量证明难度防止伪造区块。两个条件都满足才允许上链。is_chain_valid是整条链的“体检中心”。它从第二个区块开始逐个检查每个区块的哈希是否自洽、每个区块记录的previous_hash是否真的等于前一个区块的哈希。只要有一处对不上整个校验就返回False。这就是为什么说区块链的数据“牵一发而动全身”——你改了中间一个区块的data它的hash就变了后面所有区块的previous_hash都对不上了。3.3 挖矿的核心逻辑一个让新手恍然大悟的例子光看代码可能还不太直白我实际操作一遍。假设我已经通过Blockchain()创建了一条新链现在要挖第一个普通区块。blockchain Blockchain() previous_block blockchain.last_block new_block Block( indexprevious_block.index 1, timestamptime.time(), data{transaction: Alice - Bob: 10 ETH}, previous_hashprevious_block.hash ) proof blockchain.proof_of_work(new_block) blockchain.add_block(new_block, proof)我跑了三次记录一下proof_of_work里nonce的变化第一次nonce从0试到4047才找到符合条件的哈希第二次nonce从0试到12891第三次nonce从0试到2331三次结果差别很大从几千到一万多没有规律。这就是哈希计算的特性——你无法预知哪个nonce能命中只能靠穷举去碰。难度设成4位0的时候平均尝试次数在6万5千次左右我这几组数据都低于平均值属于运气不错。你可能好奇这个nonce为什么要存在区块里因为别人验证这个区块是否合法时需要重新算一次哈希看看连带这个nonce一起算出来的结果是否满足难度条件。所以nonce不是一个可有可无的装饰而是证明你真的做了那次计算的“证据”。4. 让区块链“活”起来加一个HTTP API接口4.1 为什么需要API层到这一步为止链的核心逻辑已经全有了。但这里有个问题你只能在Python脚本里通过代码来操作它没法从外部去“看”或者“调”。如果我把这条链比作一个数据库那它还缺少一个供外部程序查询和写入的入口。加一个HTTP API就能用浏览器或者curl命令直接和这条链交互。我用的是Python内置的http.server模块没有引入Flask。为什么不用Flask两个原因第一这个项目主打的就是“零依赖”任何有Python环境的地方都能直接跑第二http.server虽然比Flask原始一些但对于两三个接口的demo来说完全够用还能顺便复习一下HTTP协议的基础知识。4.2 用http.server实现节点服务from http.server import BaseHTTPRequestHandler, HTTPServer import urllib.parse class BlockchainHandler(BaseHTTPRequestHandler): # 全局唯一的区块链实例 blockchain Blockchain() def do_GET(self): parsed_path urllib.parse.urlparse(self.path) if parsed_path.path /chain: self.send_response(200) self.send_header(Content-Type, application/json) self.end_headers() chain_data [] for block in self.blockchain.chain: chain_data.append({ index: block.index, timestamp: block.timestamp, data: block.data, previous_hash: block.previous_hash, nonce: block.nonce, hash: block.hash }) self.wfile.write(json.dumps({ length: len(chain_data), chain: chain_data }).encode()) elif parsed_path.path /mine: last_block self.blockchain.last_block new_block Block( indexlast_block.index 1, timestamptime.time(), data{miner: anonymous, reward: 50}, previous_hashlast_block.hash ) proof self.blockchain.proof_of_work(new_block) if self.blockchain.add_block(new_block, proof): self.send_response(200) self.send_header(Content-Type, application/json) self.end_headers() self.wfile.write(json.dumps({ message: New block mined successfully, block: { index: new_block.index, data: new_block.data, nonce: new_block.nonce, hash: new_block.hash } }).encode()) else: self.send_response(400) self.end_headers() self.wfile.write(bFailed to add block) else: self.send_response(404) self.end_headers() self.wfile.write(bNot Found) def log_message(self, format, *args): print(f[Server] {self.address_string()} - {format % args}) def run_server(port8000): server HTTPServer((, port), BlockchainHandler) print(fBlockchain server running on port {port}) server.serve_forever()这里我设计了两条路由GET /chain返回整条链的完整数据。这是“查询”接口方便你看到当前链上有多少个区块、每个区块的完整内容。GET /mine触发一次挖矿生成一个新区块加到链上。这是“写入”接口相当于对系统说“我要提交一个新块”。这里有个设计上的细节值得说一下我把Blockchain实例作为BlockchainHandler的类属性来持有。为什么不用全局变量因为BaseHTTPRequestHandler会为每个HTTP请求创建一个新的处理器实例如果blockchain是实例属性那每次请求都会新建一个区块链对象链就永远只有创世块了。用类属性所有请求共享同一个链状态才能持续。4.3 动手测试用curl和浏览器验证整条链启动服务器python blockchain.py看到Blockchain server running on port 8000之后打开另一个终端窗口先查看初始链curl http://localhost:8000/chain返回结果里只有创世块一个区块length为1。然后触发挖矿curl http://localhost:8000/mine稍微等一两秒钟返回一段JSON里面有新区块的索引、哈希、随机数和一条“挖矿成功”的提示。再查一次/chain你会发现链上多了一个区块而且新区块的previous_hash正好等于创世块的hash。我个人测试的时候最喜欢干一件事在服务器客户端之间折腾一轮之后手动改一下链上的数据——把某个区块的data字段改掉再去访问/chain或者调一下校验逻辑看看整条链会不会“报警”。这种直接体验“篡改失效”的过程比读十篇概念文章都管用。5. 实测数据与调参心得难度、性能和验证5.1 难度参数对挖矿时间的影响我花了一晚上专门测不同难度值下的挖矿耗时。测试环境是我的旧笔记本Python 3.10单线程没做任何优化。难度哈希前缀要求平均尝试次数平均耗时200256次约0.01秒30004,096次约0.08秒4000065,536次约1.20秒5000001,048,576次约22秒这个数据很直观难度每增加1平均耗时大约翻16倍。从3跳到4从“几乎瞬间”变成“需要等一下”这个体验差异非常明显。如果你只是想演示区块链的工作流程建议难度设为3或者4设成2虽然块出得飞快但挖矿的“存在感”太弱观众会觉得假设成5以上等待时间会让人失去耐心。5.2 链完整性校验亲手验证“不可篡改”我写了一个验证脚本用来测试链路完整性。这个脚本做的事生成一条链挖几个区块然后篡改中间一个区块的数据再调用is_chain_valid()看看结果。blockchain Blockchain() for i in range(5): last_block blockchain.last_block new_block Block( indexlast_block.index 1, timestamptime.time(), dataftransaction record #{i1}, previous_hashlast_block.hash ) proof blockchain.proof_of_work(new_block) blockchain.add_block(new_block, proof) print(fBefore tampering: chain valid {blockchain.is_chain_valid()}) # 篡改第二个区块的数据 blockchain.chain[1].data HACKED! print(fAfter tampering: chain valid {blockchain.is_chain_valid()})输出结果Before tampering: chain valid True After tampering: chain valid False这个实验完美展示了区块链“防篡改”的本质不是某个神奇的机制让数据改不了而是任何篡改都会在后续区块的哈希校验中被完全暴露。改了一个区块的数据它的哈希就变了导致下一个区块的previous_hash对不上就算你把下一个区块的previous_hash也改了那再下一个区块又对不上了。想瞒天过海除非你把后面所有区块全部重挖——这就是工作量证明存在的意义让重写整条链的计算成本高到不划算。5.3 关于去中心化和共识机制的思考写到这里肯定有朋友会问你这个demo里的链是单机运行的所有区块都由同一个节点产生这跟真正的比特币完全不一样啊确实不一样。我这个项目的定位是“理解区块链核心机制的教学工具”是把你领进门不是造一个能用的生产系统。真正的去中心化网络至少还涉及三个层面的东西分布式网络多个节点保存同一份链通过P2P协议同步共识算法多个节点同时挖出区块时如何决定哪条链才是“合法”的激励机制矿工为什么愿意花电费去挖矿这需要真正有价值的经济学设计这些都是后续扩展的方向。我建议你先把这个单机版玩透了理解了哈希、区块、链、工作量证明这几个核心概念再去看P2P和共识机制会轻松很多。一步到位去搞完整的加密货币项目大多数人都会被劝退。6. 常见问题与调试经验我踩过的坑不希望你再踩6.1 哈希校验总失败多半是字段序列化顺序问题我最初写compute_hash的时候没有用json.dumps而是自己拼字符串block_string f{self.index}-{self.timestamp}-{self.data}-{self.previous_hash}-{self.nonce}这个写法本身没问题但一旦data里包含复杂的字典或者列表结构转成字符串的格式就会变得不可控。比如字典的键顺序不同、中英文字符编码差异都可能导致不同环境下算出的哈希不一样。后来我改用json.dumps并且固定sort_keysTrue问题就彻底解决了。经验教训在做哈希运算的序列化时一定要确保序列化结果是确定性的、跨环境一致的。任何可能导致字符串表示变化的细节都是隐患。6.2 挖矿耗时过长别急着怀疑人生先降低难度我第一次把难度设成5结果跑了半分钟还没出块差点以为程序死循环了。后来才意识到这是正常的——只要哈希函数没有缺陷找到符合条件的nonce只是时间问题。但我遇到过一个更隐蔽的问题如果不小心在proof_of_work循环里对block.nonce做了自增却又忘了每次循环都重新计算哈希那程序就会陷入真正的死循环。我在初学阶段就犯过这个错循环体里只有nonce自增、没有调用compute_hash()结果CPU飚到100%程序纹丝不动。调试时如果遇到“等了很久没反应”我建议在这几个地方打日志进循环之前打印一次当前难度循环里每1万次尝试打印一次nonce找到结果后打印耗时。这样你能清楚地看到程序卡在哪里。6.3 时间戳不同步导致的“未来区块”这个坑比较冷门但很有意思。有一次我在测试时手工创建了一个区块时间戳用了系统当前时间。后来另一个测试脚本里又创建了一个区块两次操作间隔很短时间戳居然出现了“后创建的区块时间戳早于前一个区块”的情况。区块时间戳倒序在区块链里并不违反规则——比特币的共识规则只要求区块时间戳不能比当前时间领先太多也不比前11个区块的中位数早太多并没有强制要求单调递增。但在校验环节如果你写了“时间戳必须递增”的强校验逻辑就会误伤这种合法情况。我的建议是demo里就不要加时间戳的强校验了让时间戳只作为信息展示不做共识依据保持简单。6.4 常用调试速查表症状可能原因解决方案挖矿一直不结束难度设置过高nonce自增后没有重新计算哈希降低难度检查循环体内是否调用了compute_hash链校验一直False序列化不一致previous_hash赋值错误统一用json.dumps并sort_keysTrue打印各区块hash逐项排查添加区块总是失败add_block里previous_hash校验未过确认新区块的previous_hash确实取的是last_block.hash多次请求后链数据异常区块链实例在每次请求时被重新创建把blockchain改为Handler类的类属性中文data出现编码错误终端编码问题在文件头部加# -*- coding: utf-8 -*-输出时用ensure_asciiFalse6.5 开发环境配置的几点补充如果你是Python新手头一回在自己的电脑上搭环境我提示几个常见的坑Python版本建议3.8及以上我测试时用的是3.10。太低版本可能有些语法和库行为有差异。别把系统自带Python和安装的Python搞混Windows下建议用py -V查看当前的Python版本macOS和Linux用python3 --version。装的时候留意环境变量里到底哪个版本的Python排在前面。代码保存为.py文件后在命令行里运行python blockchain.py。如果你用的是VS Code记得先在终端里激活正确的虚拟环境不然运行时可能找不到你刚装的依赖。7. 后续还能往哪些方向扩展写到这里核心内容其实已经说完了。但我还是忍不住想聊几个扩展方向因为每次我用这个demo做分享都会被问到“接下来呢”。7.1 加分片和交易池目前的区块里data字段就是一段普通字符串。你再往前一步可以让data变成一堆交易的列表然后把“普通挖矿”改成“从交易池里取一批交易打包进区块”。这需要你定义一个简单的交易结构比如发送方、接收方、金额再加上一个交易池的管理逻辑。实现思路在Blockchain类里加一个pending_transactions列表mine()方法挖矿时把交易池里的交易全部塞进新区块挖完后清空交易池。这样链上记录的不再是“一条消息”而是“多个账本条目”更接近真实区块链的形态。7.2 加P2P网络这是最复杂的扩展也是最“区块链”的部分。你可以用socket实现一个最简单的节点通信节点A和节点B互相同步链数据选取最长链作为合法链。听起来简单但真正实现起来会涉及节点发现、数据序列化传输、冲突解决等一堆细节。7.3 加数据持久化当前链是存在内存里的程序一关就没了。如果你想让链重启后还在可以把每个区块在打包时同时写入一个SQLite数据库或者一个JSON文件。加载时先读文件重建链再继续后续操作。这个扩展相对简单但很实用。7.4 改成真正的PoW挖矿验证去掉startswith这种简单判断改成验证哈希数值小于某个目标值target这是比特币实际采用的方式。例如要求哈希的十六进制数值小于0x000010000...。这种写法更接近底层也能帮你理解比特币“难度值”是怎么计算的。我个人在这个项目上踩过的最大坑就是一开始总想着“做个大而全的系统”结果卡在P2P网络实现上两星期都没成果。后来把目标缩到最小——先跑通一条能挖矿、能验证、能查询的链整个学习曲线一下子就顺了。如果你也正在学区块链我的建议是别急着追求“像比特币一样”先让你的这条小链跑起来看它出块、验证、被打脸篡改失效在这个过程中建立直觉然后一步一步扩展。有了这个底子后面再接触真实公链的代码你会发现很多东西都是相通的。
返回列表