先别急着敲代码,真的。我见过太多新手学区块链,第一周就在“分布式共识”“P2P 网络”这些词上疯狂划重点,结果真到自己动手的时候,连“区块和区块到底靠什么连起来”都没搞清楚。我的建议很简单:忘记那些高大上的名词,先亲手写一条极简区块链,把哈希、区块、链式校验、工作量证明这些概念落到代码里。当年我用 Python 从零实现了一个能挖矿、能校验、能处理大致分叉的教学链,前后代码量就 200 多行,但整个区块链最底层的逻辑基本都摸通了。这篇文章就是带新手快速入门“如何构建一个区块链”这件事,我们会一步步写代码,最终跑起来一条属于你自己的极简区块链。
1. 写代码前先想明白的三个底层问题
1.1 区块链本质是一串“带指纹的箱子”
很多人一开始就会被“去中心化”“分布式账本”这些词带偏,以为区块链是一个很复杂的网络系统。但网络只是它的部署方式,不是核心。你完全可以在本机用单文件模拟一条完整的区块链,核心逻辑一样不落。
我习惯把区块链理解成一队传送带上的箱子。每个箱子就是一个区块,里面装着一批数据——在入门项目里,这数据可以是一次转账记录、一条存证信息,甚至只是几段文本。箱子按时间顺序排成一队,每一个箱子封口时都要打上两个戳:一个是你自己内容的指纹(哈希),另一个是前一个箱子的指纹。只要有人偷偷改动前面某个箱子里的内容,这个箱子的指纹就会变,后面所有箱子里记录的“上一个指纹”就对不上了,链立刻宣告断裂。区块链技术的“防篡改”,靠的就是这种一层压一层的指纹链,听起来简单,做起来也很简单,但很多人写代码时仍然忽略关键细节。
这里有个常被问到的问题:既然是一串数据,我直接用数组往里面塞区块不就行了?答案是行,但那只是普通的链表存数据,不叫区块链。区块链和链表的本质区别在于每一个节点之间不是简单的“指针”关系,而是通过前一个区块的哈希值做安全校验关系。你能算出这个哈希,才能证明你跟前面的数据是同一条链上的。
1.2 为什么哈希函数是整条链的地基
哈希函数是整个区块链里最不能马虎的部分。它的输入可以是任意长度的数据,输出是一个固定长度的字符串。我们这里用的 SHA-256 算法,输出是 64 位十六进制字符,比如常见的那种e5d9...f0a2的形态。
哈希函数有几个对区块链极其重要的性质。
第一,确定性。相同输入永远得到相同输出,所以它才能作为“指纹”使用。
第二,雪崩效应。输入哪怕只改一个字母、一个标点、一个空格,输出的哈希值都会完全变样,两串看起来毫无规律关系。这保证了如果区块数据被动过,哈希就是最灵敏的报警器。
第三,不可逆。从哈希值反推原始输入在计算上基本不可能,只能暴力尝试。这是后面“工作量证明”能够成立的前提。
做区块链开发时,这些特性不是理论,而是每分每秒都在用的工具。我见过有人为了赶进度,随便拿个校验函数当哈希用,最后整条链的校验关系形同虚设。
实际开发里,我们用的是标准库自带的哈希模块,不需要额外安装东西,开箱即用。这也符合一个教学项目的原则:把注意力集中在逻辑上,而不是折腾环境。
1.3 一条链到底在防谁
还有个底层问题要想明白:我为什么不用普通数据库加备份?在很多场景下,一台中心服务器协同数据完全够了。但区块链面对的场景是多个互不信任的参与方,每个人手里都有一份数据副本,谁也不愿意无条件相信谁。
举个最简单的例子:几个合作方共享一份积分结算账本,如果账本存在其中某一方服务器上,那这一方偷偷改一笔历史积分,其他人毫无察觉。就算定期做备份,也没法证明“备份之前”的版本没被动过。区块链的做法是让每个参与方都保存完整链条,任何人改数据都会破坏后续区块的哈希链条,其他参与方一校验就能发现哪里出了问题。
所以写代码的时候,我会把“校验”当作第一公民来看待。不是为了跑通流程而顺便校验,而是这个系统存在的意义就是为了校验。理解了这一点,你在设计区块字段、设计校验函数时会自然做得更严谨。
2. 动手写第一个区块:字段设计和哈希计算
2.1 区块里的五个核心字段
先别写代码,我们先设计区块的字段。不同区块链的区块字段不完全一样,但教学链上这几个字段基本是标配:
| 字段 | 含义 | 是否参与哈希计算 |
|---|---|---|
| index | 区块在链上的高度,从 0 开始 | 是 |
| transactions | 区块里存放的数据,一般是交易列表 | 是 |
| timestamp | 出块时间戳 | 是 |
| previous_hash | 前一个区块的哈希值 | 是 |
| nonce | 工作量证明的随机数 | 是 |
还有一个字段叫hash,它是当前区块自己的哈希。注意,这个字段不应该参与自身的哈希计算,否则你会陷入“先有鸡还是先有蛋”的死循环。
从这张表可以看出,previous_hash就是连接链条的关键。第 N 个区块的哈希值会作为第 N+1 个区块的previous_hash被记录下来。一旦第 N 个区块的数据被篡改,它的哈希就变掉,第 N+1 个区块的previous_hash仍然指向旧值,对不上。
2.2 用 Python 定义 Block 类
我建议用 Python 来做教学实现,因为语法直观、贴近伪代码,而且标准库已经包含哈希和时间模块。下面就是区块的定义:
import hashlib import json import time class Block: def __init__(self, index, transactions, timestamp, previous_hash, nonce=0): self.index = index self.transactions = transactions self.timestamp = timestamp self.previous_hash = previous_hash self.nonce = nonce self.hash = self.compute_hash() def compute_hash(self): block_data = json.dumps({ "index": self.index, "transactions": self.transactions, "timestamp": self.timestamp, "previous_hash": self.previous_hash, "nonce": self.nonce }, sort_keys=True).encode() return hashlib.sha256(block_data).hexdigest()代码很简单,但有一点我要强调:compute_hash()方法会把区块的全部核心字段序列化成一个 JSON 字符串,再用 SHA-256 计算。为什么非要做序列化这一步?因为哈希函数吃的是一串字节,一个对象如果不序列化,没法直接喂给哈希函数,而且序列化之后你就能很清楚地看到每一个参与计算的字段到底是什么。
2.3 哈希序列化的两个隐藏细节
这一小节是我最想让你认真看的,因为新手踩坑有一半都踩在这里。
第一个细节是sort_keys=True。它让 JSON 序列化时按字典序对字段排序。为什么要排序?假设某天你在两个不同语言环境或不同版本的程序里计算同一个区块的哈希,如果字段顺序不一样,序列化出来的字符串不同,哈希值就不同。sort_keys就是给跨平台一致性铺路的,别小看这个参数。
第二个细节是.encode()。JSON 字符串不能直接喂给 SHA-256,必须转成 UTF-8 字节,不编码的话程序会直接报错,编码格式不一致的话哈希结果不一致。在实际项目里,团队必须统一序列化规范和编码方式,否则同一个区块在不同节点算出来的哈希对不上,这才是真正灾难。
我当年第一次写这个类的时候,没有给字典加sort_keys,结果本地跑测试一直报区块哈希不匹配,排查了整整一下午,最后才发现两个文件生成 JSON 时的字段顺序不一样。
3. 把区块连成链:链式校验才是防篡改的第一道防线
3.1 创世区块和连接逻辑
有了区块类,接下来就是定义链。区块链是一个特殊的链表,第一个区块是我们手动创建的,叫做创世区块。创世区块的previous_hash没有前驱可指,一般固定设置为"0"。
class Blockchain: def __init__(self): self.chain = [] self.pending_transactions = [] self.difficulty = 4 self.create_genesis_block() def create_genesis_block(self): genesis = Block(0, [], time.time(), "0") genesis.hash = genesis.compute_hash() self.chain.append(genesis) @property def last_block(self): return self.chain[-1]这里有个细节:创建创世区块时__init__会自动调用compute_hash()并赋值给self.hash,但我在函数里又显式计算了一遍genesis.hash = genesis.compute_hash()。表面看有点冗余,但这样做是为了明确一个设计原则:区块的hash字段可以由自己算,也可以在后续步骤中被重新赋值。
创世区块永远处于链的最底部,它是整条链信任链的起点。只要创世区块不出问题,后面的链就可以一层层校验上去。
3.2 添加区块时必须做的三层校验
光把区块对象追加到列表里不叫“上链”。真正上链前必须过三层校验,缺一不可:
第一层,检查新区块的previous_hash是否等于当前链上最后一个区块的hash。如果对不上,说明这个区块压根不是接在当前链后面的,可能来自另外一条分叉链。
第二层,检查这个区块的哈希是否满足工作量证明的难度要求。如果难度设置为哈希值以 4 个零开头,那就要验证block.hash确实以 4 个零开头。
第三层,重新计算区块哈希,确认block.hash真的等于block.compute_hash()。这是为了防止有人造了一个假哈希,或者数据在构造后被改动过。
这三层里,第一层解决“接在哪”,第二层解决“有没有干过活”,第三层解决“数据是不是真的”。我见过不少简化教程只做第一层,结果整条链毫无安全性可言。
3.3 校验整条链的完整实现
校验函数要能对整条链从头到尾扫一遍。注意,是从第 2 个区块开始循环,因为创世区块没有前驱可校验。
def is_chain_valid(self, chain): for i in range(1, len(chain)): current = chain[i] previous = chain[i - 1] if current.previous_hash != previous.hash: return False if not self.is_valid_proof(current, current.hash): return False return True写这个函数时,我特别提醒自己要检查每一对相邻区块,而不是只抽查头部或尾部。有一个直观的经验:你如果只查链尾,攻击者把中间区块换掉,你的校验依然可能显示“整条链有效”,这就是自欺欺人了。
4. 工作量证明:让“挖矿”从概念变成代码
4.1 为什么要设置计算成本
前面几节已经能构造一条可校验的链。但还有一个问题:如果任何人都能随意、免费地往链上添加区块,那恶意的参与者可以批量制造区块,把记录刷成对自己有利的样子。所以真实区块链要求每个出块者提供“工作量证明”——也就是你要证明,生成这个区块确实付出了一定的计算代价。
这个设定在经济上阻止了作恶,也直接体现在“难度”上。比特币里著名的“哈希前 N 位为 0”,本质上就是一个可调节难度的答案审核规则。
为什么说这个规则很巧妙?因为验证一个答案只需要算一次哈希,代价极低;但生成一个满足条件的答案,却需要反复穷举很多次。这就是典型的“验证容易、生成困难”,攻击者要篡改历史区块,就得把后面所有区块的工作量全部重做一遍,成本瞬间爆炸。
4.2 难度、随机数和“四零开头”
工作量证明的核心是nonce字段。它就是一个我们不断尝试的随机数。
计算过程是这样的:新区块先以nonce=0计算哈希,看结果是否满足难度要求。如果不满足,nonce+1,再算,再判断。直到某个nonce算出来的哈希以足够数量的零开头。
用具体的数字感受一下:如果难度设置为4,那么挖矿目标就是找到一个nonce使区块哈希以0000开头。SHA-256 输出的每一位是 16 进制字符,正好是 4 比特,所以 4 个零开头的概率是 16 的 4 次方分之一,也就是 65536 分之一。乍一看要试几万次,但计算机算 SHA-256 的速度非常快,本地测试几万次也就零点几秒的事。
4.3 PoW 代码实现与预期效果
下面这两个方法就是完整的工作量证明和区块提交:
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 is_valid_proof(self, block, block_hash): return (block_hash == block.compute_hash() and block_hash.startswith("0" * self.difficulty)) def add_block(self, block, proof): previous_hash = self.last_block.hash if block.previous_hash != previous_hash: return False if not self.is_valid_proof(block, proof): return False block.hash = proof self.chain.append(block) return True注意一个细节:proof_of_work里算出来的computed_hash其实已经是最新的正确哈希,但我在add_block里仍然会用is_valid_proof重新校验一遍。这个重复是刻意的,因为挖矿和上链解耦,调用add_block的人可能根本不经过proof_of_work。从外部传进来的proof必须单独验证,不验证就会留下造假入口。
实际运行时,如果难度设成了 4,我本地一般跑几千到几万次就能找出来,耗时可以忽略。如果你发现跑了很久都没出块,多半是难度设太高了,学习阶段调到 2 或 3 体验更好。
5. 让整条链跑起来:记账、出块、输出结果
5.1 交易池和出块流程
链已经能“认识”区块了,现在要让它真正用起来。现实中的区块链会有多个节点不断提交交易,交易不会立刻被打包进区块,而是先在“待处理池”里攒着。矿工从池子里取一批交易,打包成一个新块,接着开始工作量证明,挖矿成功后把新块追加到链上。
对照到代码里,就是两个方法:add_transaction负责往池子里加交易,mine_block负责把池子里所有交易打包成一个新区块并挖矿上链。
def add_transaction(self, transaction): self.pending_transactions.append(transaction) return self.last_block.index + 1 def mine_block(self): if not self.pending_transactions: return None new_block = Block( index=self.last_block.index + 1, transactions=self.pending_transactions, timestamp=time.time(), previous_hash=self.last_block.hash ) proof = self.proof_of_work(new_block) self.add_block(new_block, proof) self.pending_transactions = [] return new_block这里有一个我踩过的坑:一开始我在mine_block结束后没有清空pending_transactions,导致同一批交易被反复打包进多个区块。这虽然不影响哈希校验,但直接违背了记账系统的常识——同一条交易怎么能出现多次?
5.2 完整可运行代码
把前面的代码合并起来,就是一个非常完整的最小区块链:
import hashlib import json import time class Block: def __init__(self, index, transactions, timestamp, previous_hash, nonce=0): self.index = index self.transactions = transactions self.timestamp = timestamp self.previous_hash = previous_hash self.nonce = nonce self.hash = self.compute_hash() def compute_hash(self): block_data = json.dumps({ "index": self.index, "transactions": self.transactions, "timestamp": self.timestamp, "previous_hash": self.previous_hash, "nonce": self.nonce }, sort_keys=True).encode() return hashlib.sha256(block_data).hexdigest() class Blockchain: def __init__(self): self.chain = [] self.pending_transactions = [] self.difficulty = 4 self.create_genesis_block() def create_genesis_block(self): genesis = Block(0, [], time.time(), "0") genesis.hash = genesis.compute_hash() self.chain.append(genesis) @property def last_block(self): return self.chain[-1] def add_transaction(self, transaction): self.pending_transactions.append(transaction) return self.last_block.index + 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 is_valid_proof(self, block, block_hash): return (block_hash == block.compute_hash() and block_hash.startswith("0" * self.difficulty)) def add_block(self, block, proof): previous_hash = self.last_block.hash if block.previous_hash != previous_hash: return False if not self.is_valid_proof(block, proof): return False block.hash = proof self.chain.append(block) return True def mine_block(self): if not self.pending_transactions: return None new_block = Block( index=self.last_block.index + 1, transactions=self.pending_transactions, timestamp=time.time(), previous_hash=self.last_block.hash ) proof = self.proof_of_work(new_block) self.add_block(new_block, proof) self.pending_transactions = [] return new_block def is_chain_valid(self, chain=None): chain = chain or self.chain for i in range(1, len(chain)): current = chain[i] previous = chain[i - 1] if current.previous_hash != previous.hash: return False if not self.is_valid_proof(current, current.hash): return False return True这段代码没有依赖任何第三方库,直接用一个.py文件就能跑。
5.3 运行结果能看到什么
用一小段演示代码来看效果:
if __name__ == "__main__": bc = Blockchain() bc.add_transaction({"from": "alice", "to": "bob", "amount": 10}) bc.add_transaction({"from": "bob", "to": "carol", "amount": 5}) block_1 = bc.mine_block() print("区块 1 哈希:", block_1.hash) print("区块 1 随机数:", block_1.nonce) bc.add_transaction({"from": "carol", "to": "dave", "amount": 3}) block_2 = bc.mine_block() print("区块 2 哈希:", block_2.hash) print("区块 2 随机数:", block_2.nonce) print("整条链有效:", bc.is_chain_valid()) bc.chain[1].transactions = [{"from": "alice", "to": "eve", "amount": 999}] print("篡改后链有效:", bc.is_chain_valid())运行之后你会看到类似这样的规律:每个区块的哈希都不是随便生成的,而是满足“开头若干个零”的条件;nonce值会跳动,具体是多少取决于你的机器算力;最后篡改交易后,is_chain_valid()直接返回False。
这里我建议你亲自跑一遍,亲眼看到哈希值因为一个数字变化而完全改变,那种感觉比读十篇科普都有效。
6. 从单机走向多节点:冲突解决与真实世界差异
6.1 两个节点同时挖出区块时怎么办
单机版区块链没有竞争,但一旦把代码部署到多个节点,就会遇到一个新问题:两个节点同时完成工作量证明,各自往链上追加了一个不同的区块。这时候全网就出现了分叉。
真实区块链处理分叉有一套简单粗暴又自洽的规则:永远相信最长的那条链。短的链会被网络丢弃,短链里包含的未确认交易会被重新放回待处理池。之所以相信最长链,是因为它蕴含了更多的工作量,作恶者想伪造出一条比全网更长的新链,需要投入比全网所有诚实矿工加起来还要多的算力,这在现实中几乎不可能。
这个“最长链规则”可以在你的本地仿真里模拟出来:让两个区块链实例分别挖出各自的新块,然后把两条链摆在一起对比长度,短的作废。虽然真实网络里的共识机制会更复杂,但底层逻辑就是这样。
6.2 教学链和真实区块链差在哪里
你现在写出来的这条链,已经拥有区块链最核心的骨架。但真要和工业级区块链比,还有几个明显的差距。
第一,真实区块链的区块里一般只存放交易哈希的汇总值,而不是把所有交易原文直接塞进区块。区块链底层的默克尔树结构可以让你在不暴露全部交易细节的情况下,证明某个交易确实在区块里,这是教学链没有实现的。
第二,真实区块链有专门的 P2P 网络模块,节点之间要通过网络协议广播新块、发现彼此、交换完整链。我们的教学链只是单机数据结构,没有网络层。
第三,真实区块链的难度不是写死的,而是根据全网算力动态调整。比特币那种每 2016 个区块调整一次难度的机制,保证了出块时间稳定在 10 分钟左右。
第四,真实区块链还有惩罚机制。作恶节点提交无效区块,会被系统扣除抵押资产或信誉分。
6.3 完成之后下一步可以做什么
当你把这条教学链跑通并理解了每一行代码,接下来有几个很自然的进阶方向。
你可以尝试给区块增加默克尔根,把交易列表先用默克尔树收敛成一个哈希值,再放进区块参与哈希计算。你可以把区块链逻辑封装成一个 HTTP 接口,让外部程序通过网络来提交交易、发起挖矿请求、查询整条链。你甚至可以启动两个进程,模拟两个节点的挖矿竞争,手动实现最长链替代逻辑。
这些扩展里的每一项,单独拿出来都够写一篇长文。但有一个前提,你必须先把今天这条最基础的链吃透。
7. 新手避坑指南:我踩过的 5 个实战问题
7.1 改了一个字段后哈希“看似正确”却通不过校验
很多人第一次改代码时会有个疑问:我把transactions里的金额改了,重新调用compute_hash()也生成了新哈希,为什么整条链校验还是失败?原因很简单:你改了区块内容后,后面所有区块的previous_hash还盯着旧哈希。正确做法是改完数据之后,从被改区块之后的所有区块全部重新计算哈希,但这在真实区块链里就意味着要重做全部工作量证明。
这也是“防篡改”的本质:数据不是不能改,而是改动成本极高。
7.2 挖矿函数卡死,CPU 一直跑到满
如果你把难度设定为 5 或 6,proof_of_work里的while循环可能跑很久都不停。这不是死循环,而是难度超出了你机器的暴力搜索能力。学习用建议把difficulty设为 2 或 3,体验一下流程就好,想感受压力再调到 4。
另外,调试挖矿函数时,最好在循环体里加一个计数,每 10 万次打印一次进度,不然你会以为自己电脑中毒了。
7.3 校验代码写成“只查最后一个区块”
这是一个隐蔽的逻辑错误。有些开发者在is_chain_valid里只比较最后一个区块和前一个区块的哈希关系,然后就说“链有效”。但实际上中间的某个区块可能已经被替换成另一条分叉里的合法区块,只要末端的连接没断,这种校验就发现不了。必须老老实实从第 2 个区块遍历到最后一个。
7.4 创世区块哈希算错,整条链作废
如果你手动改过创世区块的任意字段,但没有同步更新它的hash字段,后续检查第一个真正区块的previous_hash时就会失败。由于创世区块没有前驱,这个问题在检查时特别容易漏掉。我自己的习惯是:在create_genesis_block里强制重新算一次哈希,确保创始块字段和哈希严格匹配。
7.5 用可变对象做交易数据,导致哈希不稳定
transactions字段如果用的列表或字典,而且后续又被其他地方修改了,哈希结果会跟着变。这会导致同一个区块在不同时间算出来的哈希不一样,非常头痛。做法很简单:一旦区块被创建并且哈希被计算出来,就不要再去改动它的核心字段了。如果需要变更数据,正确的做法是创建一个新交易、生成一个新区块,而不是回头修改旧区块。
下面把避坑要点整理成一个速查表:
| 问题 | 现象 | 解决思路 |
|---|---|---|
| 字段顺序不一致 | 两边计算哈希不一致 | 序列化时固定排序规则 |
| 难度过高 | 挖矿长时间卡住 | 学习阶段设为 2 或 3 |
| 只校验末端 | 篡改中段链仍显示有效 | 遍历全部相邻区块对 |
| 修改旧交易 | 哈希对不上 | 不改旧块,只追加新块 |
| 创世块数据错 | 后续校验全断 | 创建时强制重算哈希 |
我个人在实际操作里最深的体会是:区块链编程题最怕的不是算法太复杂,而是哈希不一致。只要序列化规则、字段顺序、编码方式这三样东西有任何一边没对齐,整条链的数据完整性就无法成立。所以写完代码第一件事,永远是写一个简单的测试:建一条链,连续挖几个块,校验返回True,再篡改一笔交易,校验必须返回False。这两个结果出来了,你的最小区块链才算真正达标。
做完这些,你已经亲手构建了一个完整的区块链。接下来想往哪个方向扩展,就看你自己的兴趣了。