☰
用Python从零写一个区块链:核心原理与代码实践
2026/10/13 18:58:01 网站建设 项目流程

1. 项目拆解:一个区块链到底需要什么

先说说我为什么想写一个自己的区块链。很多人觉得区块链是那种高不可攀的底层技术,一上来就去看白皮书、啃共识算法,结果被各种术语劝退。但如果你愿意静下心,用自己最熟悉的语言——比如Python——把它从零撸一遍,你会发现它离你没有那么远。区块链的本质,说白了就是“一个由多个带时间戳的数据块串成的链表,且这个链在物理上很难被篡改”。

1.1 为什么选择Python来实现

选Python不是因为它性能最快,恰恰相反,它很可能是我能想到的跑得最慢的方案之一。但它有一个无法替代的优势:可读性极强,极其适合用来表达设计意图。写一次能给别人讲清楚“哦原来这个字段是这么用的”“哦原来哈希在这里起着这个作用”。

在这个项目里,Python的几个特性让我省了很多力气:类机制简单直接,几秒钟就能把一个区块的数据结构设计出来;字典、列表这些内置容器天然就和JSON兼容。后面你还会看到,用标准库就能完成哈希计算和序列化,不需要额外装任何第三方库。

1.2 区块链的核心构成:区块、链与挖矿

如果让你给路人解释区块链是什么,你可以这样讲:一个账本,每隔一段时间就会生成一页新的账页,这个账页上记录了这段时间里所有的交易。账页和账页之间用“盖章”的方式串联起来,这个章是数学上的印章,叫哈希值。任何人想改其中一页的内容,后面的所有章全部对不上,马上就会露馅。

对应到代码层面就是三样东西:

  • 区块:存储数据的最小单元,包含索引、时间戳、交易记录、前一个区块的哈希值、自己的哈希值。
  • 区块链:一个有序的列表,节点不断往尾部追加新区块。
  • 工作量证明(PoW):让节点在生成区块时必须做一道有一定难度的数学题,从而在机制上限制“造块”的速度和恶意篡改。

注意:这里有一个很多新手第一次写时会忽略的细节——区块里存的“前一个区块哈希值”,是整个链表安全性的灵魂。没有它,链就只是一堆普通的数据盒子而已。

2. 动手之前:先搞定两个关键的基础知识

在真正敲代码之前,有一件事值得先搞清楚:哈希到底是什么,以及为什么它能被当作“嫌疑指纹”来使用。如果你对哈希函数非常熟了,可以跳过这一节直接去看代码。

2.1 哈希函数的基本原理

哈希函数从一个任意长度的输入变成一个固定长度的输出。同样的一句话,哪怕只改一个字母,输出的哈希值也会面目全非。并且这个输出是单向的,你很难通过哈希值反推原始输入是什么。

在区块链里,我们用到的算法是SHA-256,它的输出永远是一个64位的十六进制字符串。Python的hashlib库里自带了它,不需要安装额外依赖。

import hashlib print(hashlib.sha256("hello".encode()).hexdigest()) print(hashlib.sha256("hellp".encode()).hexdigest())

输出两行完全不同的字符串。这就是哈希函数的一个最直观印象:敏感性极高,且很难伪造。

有了这个工具,区块身份的确认就变得非常容易:谁的区块数据是什么,计算出它的哈希值一比对就清楚了。如果我们把“内容哈希”再加进新区块里,就可以让整个链表前后绑定在一起,篡改任何一个旧区块,都会改变它后面的所有哈希链条。

2.2 工作量证明到底在证明什么

工作量证明这个概念经常被误解。它并不是“利用电脑空转代码消耗电源”,而是通过求解一个有一定难度的哈希问题,证明这台电脑确实投入了一定的计算资源。

说的更直白一点:挖矿的过程就是不断往区块里添加一个随机数(我们管它叫nonce),然后计算整个区块内容的哈希值,看这个哈希值是否满足一个特定条件——比如前4位全部为0。不满足就换下一个随机数继续算,直到碰运气找到合适的那个数字为止。

你写的这个程序越简单,计算越快,就越能让你直观感受到:为了让哈希值满足条件,你需要尝试多少个nonce。这种体验会带来一个很深刻的觉悟——真实的比特币网络之所以如此难以攻击,不是代码有多神奇,而是所有人都得花掉大量的算力才能产出一个区块。想伪造一批交易,就等同于把整条链从头用算力重写一遍。

3. 从零写代码:区块类与区块链类

现在正式进入代码环节。下面这段代码我用的是Python 3.8以上版本,所有实现都只用标准库。

3.1 定义区块的数据结构

先定义一个区块类,它要包含以下字段:索引、时间戳、数据(这个项目里我们用交易列表充当)、前一个哈希、自身的哈希、以及挖矿用的随机数。

import hashlib import json from time import time class Block: def __init__(self, index, timestamp, transactions, prev_hash, nonce=0): self.index = index self.timestamp = timestamp self.transactions = transactions self.prev_hash = prev_hash self.nonce = nonce self.hash = self.compute_hash() def compute_hash(self): block_dict = { "index": self.index, "timestamp": self.timestamp, "transactions": self.transactions, "prev_hash": self.prev_hash, "nonce": self.nonce } block_string = json.dumps(block_dict, sort_keys=True).encode() return hashlib.sha256(block_string).hexdigest()

我特意把block_dict按字段名拼出来并做JSON序列化,而不是直接对self.__dict__操作,是因为json.dumps对内容顺序敏感,而sort_keys=True可以保证同一个区块不管在哪台机器上计算,输出都是相同的字符串。这点在分布式场景非常重要——不同机器如果字段顺序不一致,得出的哈希就会不一样,链就分叉了。

3.2 设计区块链:创世区块与连接逻辑

接下来是区块链的主类。它负责维护整条链,并暴露两个核心方法:构建创世区块、获取最新区块并追加新区块。

class Blockchain: def __init__(self): self.chain = [] self.pending_transactions = [] self.create_genesis_block() def create_genesis_block(self): genesis_block = Block(0, time(), [], "0" * 64) genesis_block.hash = genesis_block.compute_hash() self.chain.append(genesis_block) @property def last_block(self): return self.chain[-1] def add_block(self, block, proof=None): self.chain.append(block)

创世区块的prev_hash设置成64个0,这是业内的惯例——它没有真正的上一个本体,只是人为规定了一个起点。你可能会问:那创世区块的数据可以随便改吗?答案是不可以,因为整条链的权威性都建立在“所有人都从同一个创世区块开始校验”的基础上。

3.3 实现一个简单的挖矿函数

工作量证明的核心函数可以这样写。它接收一个区块,然后不断尝试不同的nonce值,直到计算出的哈希满足难度条件。

DIFFICULTY = 4 def proof_of_work(block): block.nonce = 0 computed = block.compute_hash() while not computed.startswith("0" * DIFFICULTY): block.nonce += 1 computed = block.compute_hash() return computed

这段代码的逻辑非常朴素:把难度系数设为4,代表要求哈希值以4个0开头。整个过程就是穷举,没有捷径。我跑单机程序时,难度4大概要试几千到几万次随机数,时间在零点几秒到几秒之间浮动。

注意:难度系数真的不要太贪心。我之前试过一次把DIFFICULTY设成6,结果非得跑几十万次不可,白白等了十来分钟。合理设置是:在演示环境里,4是黄金参数;要是你想加快演示速度,降到3也可以;真实场景里可以结合预估的算力动态调整目标值。

4. 完整操作流程:录入交易、生成区块、校验链

光有区块和链的骨架还不够,一个区块链系统还得有两个能力:把交易写进“待打包区”、验证整条链的一致性。

4.1 设计交易与新区块生成流程

我们需要在Blockchain类里增加两个方法:一个是new_transaction,把一笔交易记录到待处理列表;另一个是mine,将待处理交易打包成一个新区块,并把这个块链接到主链上。

def new_transaction(self, sender, recipient, amount): self.pending_transactions.append({ "sender": sender, "recipient": recipient, "amount": amount }) def mine(self): if not self.pending_transactions: return False last_block = self.last_block new_block = Block( index=last_block.index + 1, timestamp=time(), transactions=self.pending_transactions, prev_hash=last_block.hash ) new_block.hash = proof_of_work(new_block) self.chain.append(new_block) self.pending_transactions = [] return new_block

这里值得说说pending_transactions清空的位置。我把它放在“成功挖出区块并执行追加”之后,而不是proof_of_work之前。因为如果挖矿失败或者中途抛出异常,待处理交易还能保留下来重试,不会白白弄丢数据。这个顺序问题我踩过一次坑:有一版代码在挖矿前就清空了列表,后面没能成功打包,结果那条交易彻底不见了。

4.2 验证整条链是否合法

校验函数要同时检查两件事:第一,每个区块的哈希值是否和区块内容匹配;第二,区块之间是否通过prev_hash顺利衔接。缺一不可。

def is_valid_chain(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 if current.prev_hash != previous.hash: return False return True

这段代码每次重新计算每个块的哈希并与块内存储的哈希做比对,同时检查前向链接。任何人如果想修改链上某个区块里的交易金额,哪怕只改一个小数点,重新计算的哈希值就会对所有后续块“失效”,这个链条就会立刻被判定为非法。

4.3 本地跑通一个最小演示实例

把上面的代码凑齐后,我们可以跑一个最简体验流程:

if __name__ == "__main__": bc = Blockchain() bc.new_transaction("Alice", "Bob", 1.5) bc.new_transaction("Bob", "Charlie", 0.5) block1 = bc.mine() print("区块1哈希:", block1.hash) bc.new_transaction("Charlie", "Alice", 0.2) block2 = bc.mine() print("区块2哈希:", block2.hash) print("链是否合法:", bc.is_valid_chain())

运行之后你会看到两个区块被先后生成,验证返回True。这个最小的“账本”已经具备记账、打包、校验的完整闭环,虽然简陋,五脏俱全。

5. 常见问题与排查技巧实录

写完这套最小实现还不够,实际动手时肯定会撞上不少坑。我把自己遇到过的和我们团队里新手常问的问题整理出来了。

5.1 哈希值稳定但系统重启后无法验证

这个坑非常隐蔽。如果你把区块链数据直接存成Python对象,进程重启后内存里的对象自然全没了。你以为没事,只要重新执行一遍创建流程就行——但问题是要保证创世区块的哈希一致性,一旦创世区块里用了和之前不同的时间戳,整条链就是从新的起点开始,旧链数据全部无法对上了。

解决办法也很简单:要么把所有区块序列化到本地文件里,重启后加载并逐块重算哈希校验;要么设计一个程序外的约定,比如创世区块的时间戳固定为某个特定常量,让任何人都能重建出相同的起始状态。在这套教学代码里,我建议使用后者,它更简单也更容易演示。

5.2 挖矿耗时过长或过短

耗时的背后是DIFFICULTY这个参数在起作用。调大它会指数级增加计算量,调小了又容易被瞬间造出区块。在实际项目里,更强的网络算力会要求你动态调整难度,而在教学环境里你只要掌握一个小技巧:先设一个很小的难度值确认整个流程通顺,再逐步加大,感受一下指数增长的曲线。我印象最深的一次体验是把难度从3调到4,耗时直接翻了七八倍——这个直观感受远比看文字描述强烈。

5.3 交易列表被篡改但链检查却显示正常

这种情况只会发生在校验代码有缺陷的时候。某个版本的is_valid_chain只检查了prev_hash连续,没有重新计算每个区块的内部哈希,于是有人改了交易内容以后,链两侧的哈希结对不上,但校验函数却视而不见。修复方式就是确保校验函数里每次都调用compute_hash()而不是直接拿存储的hash字段出来比对,这个代码我在上面已经给出,请务必照着写。

5.4 常见报错速查表

现象可能的原因排查方法
哈希值全为None忘记在创建区块后调用compute_hash并赋值构造方法里补齐self.hash = self.compute_hash()
交易打包了但查不到清空pending_transactions过早确认待处理列表在成功追加到链后再清空
链验证返回False时间戳不一致或字段拼接顺序错乱用json.dumps(... , sort_keys=True)固定序列化
挖矿永远死循环DIFFICULTY设得过大调小难度,或增加一个最大迭代次数保护

5.5 一个容易忽略但很关键的调试建议

强烈建议你在proof_of_work循环里加一个打印进度的控制开关,比如每1000次尝试输出一次当前nonce和计算出的哈希前缀。这个小细节看起来不起眼,但当你面对一个长时间跑不出结果的程序时,你会非常需要它来区分情况——到底是代码死循环了,还只是纯粹运气差、还没撞到满足条件的nonce。

6. 更进一步:还能往这个项目里加什么

一个“最简单”的区块链,它的价值其实不在于代码有多高深,而在于它是你理解更复杂系统的出发点。顺着这条路往下探索,还有好几个值得玩的方向。

6.1 增加交易签名认证

现在的交易是明文并且没有身份的,任何人可以伪造任意sender的转账记录。真实系统要解决的第一个问题就是身份与授权。你可以在new_transaction收到请求之后,要求使用发送方的私钥对交易内容做一次签名,再用公钥验签,验不过就拒绝打包。这套东西用标准的加密库就能做。

6.2 增加Ethereum式的智能合约执行器

区块里的transactions字段目前只是普通字典,但如果里面存的可执行代码,链就从“账本”变成了“分布式计算机”。你可以设计一个小型的指令集:转移、读取、存储、条件跳转,然后让每个节点按相同顺序地执行这些指令。这样实现出来的是一条最小化的“链上状态机”,虽然性能惨不忍睹,但对理解状态同步的机制帮助很大。

6.3 引入P2P网络节点同步

单机版的区块链只能叫“区块单例”,称不上网络。你可以用socket或者websocket搭一个简单节点间通信协议:节点A挖到一个新块,广播给节点B,B收到后校验合法性,再追加到本地链。一旦有分叉出现,再约定“取最长的链”。做到这一步,你才算真正摸到分布式系统的门把手。

7. 我踩过的一些真实坑与心得

最后这部分,说说我在从零到完整跑通的过程中,印象最深的三件事。

第一,少即是多。最开始我也试图二三十行代码写一个像模像样的区块链出来,结果发现,这个系统的核心复杂度几乎全在“校验”和“前后衔接”上,根本没有所谓的花哨写法。新手最容易陷入的误区是过度设计,上来就把P2P、加密签名、共识机制全塞进去,可一旦出问题,调试起来会非常痛苦。先把单机版写稳,再逐层叠加复杂性,是我反复验证过最舒服的学习路径。

第二,打印中间值是排错的第一生产力。有一次我写出的链怎么都不合法,翻来覆去检查都觉得逻辑没问题,最后加了一行打印,才发现compute_hash()里把transactions和prev_hash的顺序搞反了——字段顺序变了,哈希输出自然变了。而当你看不清数据流时,花在“猜”上的时间往往比修复代码的时间还长。

第三,把测试用例当成第一等公民。我在这个项目里写的最有价值的代码不是区块链本身,而是一组边界用例:空交易列表打包、难度阈值恰好满足边界值、长时间无新块的空闲状态。有了这些测试,每一次重构才有底气和安全感,才敢舒服地改动核心逻辑而不提心吊胆。

这个项目到这里,已经能让你亲手摸到区块链的骨骼了。下一步把它变成什么形态,玩具也好、底层模块也罢,都属于你自己的延伸。我个人体会是:与其问“它有什么用”,不如先问自己“写完这一版我懂了多少”——而在你合上编辑器的那一刻,答案通常会比预想中好很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询