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

手写区块链核心逻辑:Python实现工作量证明与防篡改机制

手写区块链核心逻辑:Python实现工作量证明与防篡改机制 ★ FEATURED ARTICLE
从零手写区块链听起来像是个大工程但真动手做一遍你会发现核心逻辑其实并不复杂。这篇文章我打算用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_keysTrue保证序列化结果是确定性的。这个小细节可以在后面的代码里看到。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 难度值怎么定才合适挖矿难度不是拍脑袋定的要考虑演示场景的实际体验。我测试过不同难度的时间表现难度前导零个数平均尝试次数单块挖矿耗时普通电脑34096约30毫秒465536约400毫秒51048576约6秒616777216约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, 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()几个关键点__init__里先默认nonce0算出一个初始哈希。挖矿过程中会不断改nonce再重新算哈希。compute_hash是幂等的方法给定区块内容输出永远一致。这保证了验证的时候可以随时重算。sort_keysTrue必须加。如果不加同样的区块内容在不同解释器下可能得到不同的哈希。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( indexlatest.index 1, timestamptime.time(), datadata, previous_hashlatest.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_keysTrue一出马问题立刻解决。这个教训告诉我凡是涉及哈希计算的序列化输入必须是确定性的。第二个坑修改了nonce却忘了重算hash。写第一版挖矿循环的时候我天真地以为改完noncehash会自动更新。结果当然不是区块永远满足不了条件。调试的时候我在循环里打印了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这种体验比读十篇概念文章都来得深刻。最后分享一个小技巧把整个项目推到一个代码仓库里按一次提交一个功能节点的节奏提交代码从“定义区块结构”到“实现挖矿”再到“添加校验”每个阶段的提交信息都写清楚。过一个月再回来看这些提交你能清晰地看到自己当时是怎么一步步把区块链这个抽象概念落成代码的那种感觉本身就是学习编程最珍贵的东西。
阅读完成 · 觉得有帮助?
咨询建站