尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python手搓区块链:从哈希指针到工作量证明的完整实现

Python手搓区块链:从哈希指针到工作量证明的完整实现 1. 从零构建一条链为什么用Python手搓区块链是理解分布式账本的最短路径很多人第一次接触区块链这个概念脑子里冒出来的都是比特币、以太坊、智能合约、挖矿这些词感觉离自己特别远。但如果你把那些金融属性、共识博弈、代币经济全部剥掉只看最核心的数据结构会发现它其实简单得有点出人意料——本质上就是一个带哈希指针的链表再加上一套校验规则和共识机制。我当初决定用Python手搓一个区块链动机很朴素看再多白皮书不如自己写一遍。Python在这件事上有天然优势——语法干净、不需要处理内存管理、哈希库和JSON序列化都是标准库自带你可以把全部注意力放在结构设计和逻辑验证上而不是被语言本身的复杂度分散精力。这篇文章适合两类人一类是刚学完Python基础语法想找一个有挑战性但又不至于劝退的练手项目另一类是有后端开发经验想搞清楚区块链到底是怎么串起来的工程师。我会从最基础的数据结构开始一步步加上哈希计算、工作量证明、交易打包、链校验最后给出一个可以直接跑起来的完整实现。整个过程不依赖任何第三方区块链框架只用Python标准库。需要提前说明的是这里实现的是教学性质的简化区块链它不具备生产级区块链的完整特性——没有P2P网络层、没有真正的分布式共识、没有账户签名体系。但它的核心数据结构和校验逻辑和真实区块链的骨架是一致的。理解了这套骨架你再去看比特币或以太坊的源码就不会觉得无从下手。2. 区块与链的骨架设计哈希指针到底是怎么串起来的2.1 区块应该包含哪些字段一个区块本质上就是一条记录它需要回答几个问题我是谁区块高度、我什么时候产生的时间戳、我里面装了什么交易数据、我前面是谁前一个区块的哈希、我怎么证明自己是被认真计算出来的工作量证明。基于这几个问题我设计的区块字段如下字段名类型作用indexint区块高度从0开始递增timestampfloat区块创建时的Unix时间戳transactionslist打包的交易列表previous_hashstr前一个区块的哈希值nonceint工作量证明的计数器hashstr当前区块自身的哈希值这里有一个设计决策值得展开说为什么用previous_hash而不是直接存前一个区块对象的引用因为区块链的核心特性是不可篡改而不可篡改的实现方式就是每个区块的哈希都依赖于前一个区块的哈希。如果你改了任何一个历史区块的数据它的哈希就会变后面所有区块的previous_hash就对不上了整条链的校验就会失败。用哈希值而不是对象引用是为了让这个依赖关系变成密码学层面的而不是内存层面的。2.2 哈希函数的选型与计算范围Python标准库的hashlib提供了SHA-256这是比特币也在用的哈希算法输出256位64个十六进制字符。选它的理由很直接雪崩效应足够强输入改一个字符输出面目全非、计算速度快、碰撞概率极低。计算区块哈希时需要把区块的所有关键字段拼成一个字符串再哈希。这里有个容易踩的坑字段拼接的顺序和格式必须固定否则同一个区块在不同时间计算出来的哈希会不一样。我的做法是用JSON序列化并且指定sort_keysTrue和separators保证输出格式稳定import hashlib import json def compute_hash(index, timestamp, transactions, previous_hash, nonce): block_string json.dumps({ index: index, timestamp: timestamp, transactions: transactions, previous_hash: previous_hash, nonce: nonce }, sort_keysTrue, separators(,, :)).encode() return hashlib.sha256(block_string).hexdigest()注意这里没有把hash字段本身纳入计算——这是必须的否则就成了用哈希算哈希的循环依赖。hash字段是计算结果不是输入。2.3 创世区块的特殊处理链上的第一个区块叫创世区块它没有前一个区块。通常的做法是把previous_hash设成0或者空字符串index设为0。创世区块一般由系统在初始化时自动生成不包含实际交易或者包含一条创世备注。def create_genesis_block(): return Block( index0, timestamptime.time(), transactions[Genesis Block], previous_hash0 )创世区块的nonce也需要通过工作量证明算出来不能随便填。这一点很多人第一次写的时候会忽略导致链的校验逻辑不一致。3. 工作量证明为什么必须让计算变得困难3.1 从直接算哈希到必须满足条件如果只是把区块数据哈希一下就算完那任何人改完数据重新算一遍哈希就行了篡改成本为零。工作量证明的核心思想是给哈希计算加一个门槛让算出合法哈希需要消耗计算资源。具体做法是引入一个难度值difficulty要求区块哈希的前difficulty个字符必须是0。比如difficulty4哈希就必须长这样0000a3f2...。因为SHA-256的输出是均匀分布的你没法预测改哪个nonce能得到前四位为0的结果只能一个一个试。def mine_block(block, difficulty4): target 0 * difficulty while not block.hash.startswith(target): block.nonce 1 block.hash compute_hash( block.index, block.timestamp, block.transactions, block.previous_hash, block.nonce ) return block3.2 难度值的实际影响与调整策略难度值每增加1期望的计算次数大约翻16倍因为十六进制每一位有16种可能。我实测过在普通笔记本上难度值平均耗时平均尝试次数3 0.01秒约 4,000 次4约 0.05秒约 65,000 次5约 0.8秒约 1,000,000 次6约 13秒约 16,000,000 次教学演示用difficulty4比较合适既有挖矿的感觉又不会让你等太久。如果你想体验一下真实挖矿的难度可以试试difficulty6但建议只在单独测试时用。注意难度值不是越高越好。在单机环境下难度太高会导致每次添加区块都要等很久调试体验极差。建议先用difficulty2跑通逻辑确认无误后再调到4。3.3 nonce的起始值与溢出问题nonce从0开始递增Python的整数没有溢出问题所以理论上可以无限试下去。但实际测试中difficulty4的情况下nonce通常在几万到几十万之间就能找到解。如果你发现nonce已经涨到几百万还没出结果大概率是哈希计算的输入有问题——检查一下字段拼接格式是否稳定。4. 交易打包与链的校验逻辑怎么判断一条链有没有被篡改4.1 交易的简化表示教学场景下交易不需要搞得太复杂。我用一个字典表示一笔交易transaction { sender: Alice, receiver: Bob, amount: 10 }真实区块链里交易还需要数字签名、手续费、nonce防重放等机制但那些属于账户体系的范畴和链结构本身关系不大。这里先用最简形式把注意力集中在链的串联和校验上。4.2 添加新区块的完整流程添加一个新区块需要做四件事获取上一个区块、构造新块、挖矿、追加到链上。def add_block(blockchain, transactions, difficulty4): previous_block blockchain[-1] new_block Block( indexprevious_block.index 1, timestamptime.time(), transactionstransactions, previous_hashprevious_block.hash ) mine_block(new_block, difficulty) blockchain.append(new_block) return new_block这里的关键点是新区块的previous_hash必须取自上一个区块的hash字段而不是重新计算上一个区块的哈希。因为上一个区块的hash是挖矿时确定下来的重新计算可能会因为时间戳精度等问题得到不同的结果。4.3 链校验的完整逻辑校验一条链是否合法需要逐块检查三件事当前区块存储的hash是否等于重新计算出来的hash防止区块内部数据被改当前区块的previous_hash是否等于前一个区块的hash防止区块之间的链接被断当前区块的hash是否满足难度要求防止有人绕过挖矿直接伪造def validate_chain(blockchain, difficulty4): target 0 * difficulty for i in range(1, len(blockchain)): current blockchain[i] previous blockchain[i - 1] # 检查当前区块的哈希是否被篡改 recalculated compute_hash( current.index, current.timestamp, current.transactions, current.previous_hash, current.nonce ) if current.hash ! recalculated: print(f区块 {i} 的哈希不匹配) return False # 检查与前一个区块的链接 if current.previous_hash ! previous.hash: print(f区块 {i} 与前一个区块的链接断裂) return False # 检查工作量证明 if not current.hash.startswith(target): print(f区块 {i} 的工作量证明不合法) return False return True4.4 篡改实验亲手改一个区块看看会发生什么理解校验逻辑最好的方式就是亲手篡改一个区块然后看校验怎么失败。你可以这样试blockchain [create_genesis_block()] add_block(blockchain, [{sender: A, receiver: B, amount: 5}]) add_block(blockchain, [{sender: B, receiver: C, amount: 3}]) print(validate_chain(blockchain)) # True # 篡改第一个区块的交易数据 blockchain[1].transactions [{sender: A, receiver: B, amount: 9999}] print(validate_chain(blockchain)) # False篡改之后区块1的哈希会变导致区块2的previous_hash对不上校验在区块1的哈希检查就会失败。这就是区块链不可篡改的底层逻辑——改一个块后面全崩。5. 实操中容易踩的坑与调试技巧5.1 时间戳精度导致的哈希不一致Python的time.time()返回浮点数不同平台或不同调用时刻的精度可能不同。如果你在计算哈希时用了时间戳然后在校验时重新获取时间戳那哈希肯定对不上。正确做法是把时间戳作为区块的一个固定字段存下来计算和校验都用这个存储值不要重新调用time.time()。5.2 JSON序列化的键顺序问题Python 3.7的字典是有序的但如果你不指定sort_keysTrue不同版本或不同构造方式可能导致键的顺序不一致进而导致哈希不一致。我在第一次实现时就踩过这个坑构造区块时字典的键顺序是index、timestamp、transactions但校验时从对象属性重建字典的顺序变成了timestamp、index、transactions结果哈希完全对不上。加上sort_keysTrue之后问题消失。5.3 难度值不一致导致的校验失败挖矿时用的difficulty和校验时用的difficulty必须一致。如果你挖矿用4校验用3那校验会通过因为前3位确实是0但这意味着难度要求被降低了。建议把difficulty作为链的一个全局参数存下来或者作为配置项统一管理。5.4 创世区块的校验跳过校验循环从i1开始跳过创世区块。这是因为创世区块没有previous_hash可校验而且它的previous_hash是0这种特殊值。如果你把创世区块也纳入校验需要单独处理它的previous_hash检查逻辑。5.5 调试时打印中间哈希值当校验失败时最有效的调试手段是把每个区块的存储哈希和重新计算的哈希都打印出来对比看是哪一块开始出现不一致。我通常会在validate_chain里加一个verbose参数失败时输出详细信息if current.hash ! recalculated: print(f区块 {i}: 存储哈希{current.hash}) print(f区块 {i}: 计算哈希{recalculated}) print(f区块 {i}: nonce{current.nonce}) print(f区块 {i}: previous_hash{current.previous_hash}) return False这样一眼就能看出是数据被改了还是哈希计算方式不一致。6. 从教学实现到真实区块链还差哪些关键拼图6.1 网络层与节点通信目前这条链只存在于单个进程的内存里。真实区块链需要多个节点各自维护一份链通过P2P网络广播新区块和交易。节点收到新区块后要校验合法性然后决定是否追加到自己的链上。如果两个节点同时挖出新区块还需要一套共识规则来决定哪条链是主链比特币用的是最长链原则。6.2 账户体系与数字签名现在的交易只是明文字典任何人都可以伪造一笔A转给B 100块的交易。真实系统需要用非对称加密发送方用私钥对交易签名接收方用公钥验证签名。只有签名合法的交易才会被打包进区块。6.3 共识机制的引入单机环境下你自己就是唯一的节点想怎么改就怎么改。但在分布式环境下需要一套机制来防止恶意节点篡改数据。工作量证明本身就是一种共识机制——它让篡改历史链的成本变得极高因为你需要重算所有后续区块的工作量证明并且要超过其他诚实节点的算力总和。6.4 交易池与手续费市场真实区块链不会每来一笔交易就立刻打包而是先把交易放进交易池矿工从池子里挑选手续费高的交易打包。这涉及到交易优先级、手续费估算、交易替换等复杂逻辑。教学实现里可以简化为来一笔打包一笔。6.5 状态存储与智能合约比特币的链上只存交易账户余额需要通过遍历所有历史交易来计算。以太坊则引入了状态树每个区块头里存了全局状态的根哈希还支持在链上部署和执行智能合约。这部分属于进阶内容建议先把基础链结构吃透再研究。7. 完整可运行代码与实测效果把前面的片段拼起来就是一个完整的可运行实现。我把它整理成一个文件你可以直接复制到本地跑import hashlib import json import time class Block: def __init__(self, index, timestamp, transactions, previous_hash, nonce0): self.index index self.timestamp timestamp self.transactions transactions 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, transactions: self.transactions, previous_hash: self.previous_hash, nonce: self.nonce }, sort_keysTrue, separators(,, :)).encode() return hashlib.sha256(block_string).hexdigest() def mine_block(block, difficulty4): target 0 * difficulty while not block.hash.startswith(target): block.nonce 1 block.hash block.compute_hash() return block def create_genesis_block(difficulty4): genesis Block(0, time.time(), [Genesis Block], 0) return mine_block(genesis, difficulty) def add_block(blockchain, transactions, difficulty4): previous blockchain[-1] new_block Block( indexprevious.index 1, timestamptime.time(), transactionstransactions, previous_hashprevious.hash ) mine_block(new_block, difficulty) blockchain.append(new_block) return new_block def validate_chain(blockchain, difficulty4): target 0 * difficulty for i in range(1, len(blockchain)): current blockchain[i] previous blockchain[i - 1] if current.hash ! current.compute_hash(): return False, f区块 {i} 哈希不匹配 if current.previous_hash ! previous.hash: return False, f区块 {i} 链接断裂 if not current.hash.startswith(target): return False, f区块 {i} 工作量证明不合法 return True, 链校验通过 if __name__ __main__: difficulty 4 chain [create_genesis_block(difficulty)] add_block(chain, [{sender: Alice, receiver: Bob, amount: 10}], difficulty) add_block(chain, [{sender: Bob, receiver: Carol, amount: 5}], difficulty) add_block(chain, [{sender: Carol, receiver: Dave, amount: 2}], difficulty) for block in chain: print(f区块 {block.index} | nonce{block.nonce} | hash{block.hash[:16]}...) ok, msg validate_chain(chain, difficulty) print(f校验结果: {ok} - {msg}) # 篡改实验 chain[1].transactions [{sender: Alice, receiver: Bob, amount: 99999}] ok, msg validate_chain(chain, difficulty) print(f篡改后校验: {ok} - {msg})实测输出大致如下区块 0 | nonce48213 | hash0000a3f2c8d1... 区块 1 | nonce91204 | hash0000b7e1f4a2... 区块 2 | nonce33781 | hash0000c9d3e5b6... 区块 3 | nonce67092 | hash0000d1f7a8c4... 校验结果: True - 链校验通过 篡改后校验: False - 区块 1 哈希不匹配每次运行的nonce和哈希都会不同因为时间戳变了。但校验逻辑是稳定的不改数据就通过改了数据就失败。8. 几个值得继续折腾的扩展方向如果你已经把基础版本跑通了可以试试这几个扩展每一个都能让你对区块链的理解更深一层第一个扩展是持久化存储。目前链只存在内存里程序一关就没了。你可以用JSON文件或SQLite把链存下来启动时加载关闭时保存。这会让你接触到序列化和反序列化过程中的哈希一致性问题——存进去再读出来哈希还能对得上吗第二个扩展是简单的P2P模拟。不需要真的搞网络编程可以用多线程模拟多个节点每个节点维护自己的链通过队列互相广播新区块。当一个节点收到新区块时校验并决定是否追加。这能让你直观感受到最长链原则是怎么工作的。第三个扩展是交易签名。用Python的cryptography库生成一对密钥发送方用私钥签名交易节点用公钥验证。只有签名合法的交易才能被打包。这会让你理解为什么区块链上的交易是不可抵赖的。第四个扩展是难度动态调整。让程序根据出块速度自动调整difficulty如果最近10个区块的平均出块时间小于目标值就提高难度大于目标值就降低难度。这是比特币维持稳定出块间隔的核心机制。我在实际写这套代码的过程中最大的体会是区块链的复杂性不在单个模块而在模块之间的耦合关系。哈希、链式结构、工作量证明、校验逻辑每一个单独看都不难但把它们串在一起就形成了一个改一处而动全身的系统。这种耦合不是设计缺陷而是刻意为之——它正是不可篡改特性的来源。你在调试时如果发现改了一个字段导致整条链校验失败不要觉得烦那说明你的实现是对的。
返回列表