比特币本身并不是特别复杂,中本聪的白皮书只有十页左右。
可从该链接下载(有各种语言版本):Bitcoin: A Peer-to-Peer Electronic Cash System
但是,真正投入使用的比特币系统实际上非常复杂,包含诸多因素,涉及诸多细节,甚至对一个手续费机制研究几天还云里雾里。
如何有效地学习理解比特币的框架?如果只是看了理论,觉得总是少了什么,又该如何实战?对于大佬来说可以直接实战真实的区块链项目,那对于像我的这样菜鸡又该如何实战?
然后,便诞生了这样的想法:我能否用简单的编程语言(选了python)来实现一个最简单的但是又能体现比特币基本运行框架的程序?为了简化,我甚至不用它联网,就在本地模拟也行。
该用怎样的编程范式来实现?我采用了面向对象编程。
所以,首先思考一下比特币有什么对象。或者区块链这个名字让我们第一个想起的就是区块(block)。然而,在宏观描述一个区块链网络的时候,首先应该想到的是节点(node)。
该图为黄老师在讲解比特币网络(当时的情况是他请了一位同学Miking-G MikingG-CSDN博客上去讲解,然后Miking-G在他的指导下完成了对前半学期比特币学习的回顾)。他强调不要一开始就上来讲区块。黄老师是在区块链领域颇有影响力的研究者,大家有兴趣可以从下面链接查看相关资料:
HuangLab
BlockEmulator
也可进行关注:
比特币网络的各节点相互连接,通过共识机制维护在逻辑上的一条区块链。
这条链就是由一个个区块组成的。
各区块通过密码学算法,组成一条不断增长的链式数据结构。每个区块都包含了一些数据和元数据,例如时间戳、前一个区块的哈希值、当前区块的哈希值等。这些元数据可以用来验证区块的有效性和顺序,以及防止区块和区块链被篡改或伪造。
区块链中区块的构成详解 - 知乎 (zhihu.com)
一分钟了解“区块的构成” - 知乎 (zhihu.com)
由此我们可以定义区块的数据结构:
# 定义区块类
class Block:
def __init__(self, index, prev_hash, transactions, nonce, hash):
self.index = index # 区块高度
self.prev_hash = prev_hash # 前一个区块的哈希值
self.transactions = transactions # 包含的交易列表
self.nonce = nonce # 随机数
self.hash = hash # 区块的哈希值
def __str__(self):
return f"Block {self.index}: \nPrev_hash: {self.prev_hash} \nTransactions: {self.transactions} \nNonce: {self.nonce} \nHash: {self.hash}"
__init__
方法是一个特殊的方法,它用来初始化一个区块对象,也就是创建一个区块实例。它接受五个参数,分别是index
、prev_hash
、transactions
、nonce
和hash
,并将它们赋值给区块对象的属性。index
属性是一个整数,表示区块在区块链中的位置或高度,从0开始递增。第一个区块叫做创世区块,它的index
为0。(创世区块对启动区块链的作用很大,当时我就是在这里卡了很久)prev_hash
属性是一个字符串,表示前一个区块的哈希值,也就是前一个区块的数字指纹。它用来连接区块链中的区块,保证区块的顺序和完整性。创世区块的prev_hash
为一个全0的字符串。transactions
属性是一个列表,表示区块包含的交易数据。交易数据可以是任何形式的信息,例如比特币的转账记录,或者智能合约的执行结果。在这里只记录转账记录。nonce
属性是一个整数,表示区块的随机数,也就是区块的工作量证明。它用来调整区块的哈希值,使其满足一定的条件,例如小于等于一个目标值。这个条件的难度会根据网络的算力而动态调整,以保持区块的平均出块时间。在这里出于简单考虑,并没有根据nonce来调整目标难度。nonce只起到区块的工作量证明记录,当然也很重要。比特币的 Nonce 是什么? - 知乎 (zhihu.com)这篇文章说明了nonce是什么,可以知道nonce是怎样体现工作量的。不过后面也会讲到相关的内容,也可以不看。hash
属性是一个字符串,表示区块的哈希值,也就是区块的数字指纹。它用来标识区块的唯一性和有效性,以及防止区块被篡改或伪造。它是由区块的其他属性(index
、prev_hash
、transactions
和nonce
)通过一个哈希函数(例如在这里使用SHA-256)计算得到的。区块高度(index)、上一个区块的哈希(prev_hash)、交易列表(transactions)、随机数(nonce)和当前区块的哈希(hash)这五个属性值已经非常精简,可以说少了一个就不能让区块链在本地运行起来。但是也足以说明比特币的框架和区块链的链式数据结构。
从零开始创建一个区块链 - 知乎
https://www.runoob.com/w3cnote/blockchain-intro.html
比特币网络的共识机制是一种使得网络中的各个节点能够就数据的正确性和顺序达成一致的方法。比特币网络采用了**工作量证明(Proof of Work,PoW)**的共识机制,它要求节点通过解决一个复杂的数学问题来竞争记账权,从而获得区块奖励和交易费。
简单介绍一下主要的步骤:
可以参考以下的资料:
我们可以定义节点的组成:
# 定义节点类
class Node:
def __init__(self, name):
self.name = name # 节点名称
self.chain = [] # 节点维护的区块链
self.pool = [] # 节点收集的交易池
__init__
用来初始化一个节点对象,也就是创建一个节点实例。name
属性是一个字符串,表示节点的名称,也就是节点的标识符。它可以是任何有意义的字符串,例如节点的IP地址、域名、编号等。chain
属性是一个列表,表示节点维护的区块链,也就是节点存储的区块数据。它是由区块对象组成的一个有序的序列。pool
属性是一个列表,表示节点收集的交易池,也就是节点待处理的交易数据。它是由交易对象或字符串组成的一个无序的集合,每个交易对象或字符串都包含了交易的发送者、接收者、金额等信息(在这里我们每个交易都是用一个字符串来表示)。然后就到了关键的部分,也是最复杂和艰巨的部分,就是定义节点的一系列操作,让这些对象产生联系,从而让区块链真正运行起来。
这里的部分操作可能跟上面提到的步骤有些出入(为了简化)。
def create_transaction(self):
# 模拟创建一个交易并添加到交易池中
sender = self.name # 确定转账转出方
receiver = random.choice(nodes).name # 确定转账转入方,这里是随机选择一个节点,方便进行模拟
amount = random.randint(1, 10) # 确定转账金额,这是随机金额,方便进行模拟
transaction = f"{sender} sent {amount} BTC to {receiver}" # 定义交易字符串
self.pool.append(transaction) # 加入交易池
print(f"{self.name} created a transaction: {transaction}") # 打印交易信息,假装在广播
实际上节点产生交易应该能够决定给谁转账,转多少钱,但是为了后面的模拟方便这里直接采用随机值。
产生交易之后,可以看到我们进行了两个操作:
transaction
变量追加到节点自己的交易池(self.pool
)中,表示该交易等待被打包进区块。print
函数打印出节点创建了一个交易的信息,假装在广播。为什么说假装在广播,这里实际上并没有广播,或者说假设广播出去但没有节点接受,是为了化简。其实不妨也可以化简为广播出去所有节点直接都接收。真实的交易广播大概是这样的(可以略过不看):
比特币是什么?比特币交易的完整流程。 - 知乎 (zhihu.com)
【进阶篇一】挖矿与新区块的广播机制 - 知乎 (zhihu.com)
这个对应上面的步骤3 、4:
其他节点都尝试在自己的候选区块中进行具有足够难度的工作量证明(PoW)。这个过程就是不断地变更区块头中的一个随机数(nonce),并对每次变更后的区块头做SHA256运算,将结果值与当前网络的**目标值(target)**做对比,如果小于目标值,则解题成功,工作量证明完成。
当某个节点完成了工作量证明(PoW),它就会向全网广播自己的候选区块,让其他节点验证其完成结果。
def create_block(self):
# 尝试创建一个区块并广播给其他节点
# 获取前一个区块的信息,注意这里操作决定了区块链需要一个创世区块,否则启动的时候哪里有前一个区块
prev_block = self.chain[-1]
index = prev_block.index + 1 # 区块高度(位置)+1
prev_hash = prev_block.hash
# 设置目标值,即哈希值要小于等于该值才算有效
# 这里实际中应该动态调整,这里直接设了一个比较简单的target
target = "0000" + "f" * 60
# 从交易池中选择一些交易作为候选,这里直接选的交易池的前5个
transactions = self.pool[:5]
# 初始化随机数和哈希值
nonce = 0
hash = ""
# 不断尝试不同的随机数,直到找到满足条件的哈希值或者收到其他节点的新区块通知为止
while hash > target or hash == "":
# 如果收到其他节点的新区块通知,就停止出块并更新自己的区块链和交易池,这里化简为直接跟node[0]来比较
# 实际上收到其他节点的新区块并不一定要停止更新自己的区块链,这里是为了化简
if len(self.chain) != len(nodes[0].chain):
print(f"{self.name} stopped creating block {index} because a new block was received.")
self.chain = nodes[0].chain.copy()
self.pool = nodes[0].pool.copy()
break
# 计算区块的哈希值
nonce += 1 # 不断尝试不同的随机数
# 根据区块高度(index)、上一个区块的哈希(prev_hash)、交易列表(transactions)、随机数(nonce)来计算hash值
message = str(index) + prev_hash + str(transactions) + str(nonce)
hash = hashlib.sha256(message.encode()).hexdigest() # sha256输出的是256bit的值,转化为16进制表示就是一个64位的数
# 如果找到满足条件的哈希值,就创建一个新区块并广播给其他节点
if hash <= target:
new_block = Block(index, prev_hash, transactions, nonce, hash)
print(f"{self.name} created a new block: \n{new_block}")
# 这里是让所有的节点都收到这个新创建的区块
for node in nodes:
node.receive_block(new_block)
上面的创建区块操作可以认为只干3件事:
可以先稍微解释一下target值:
# 设置目标值,即哈希值要小于等于该值才算有效
# 这里实际中应该动态调整,这里直接设了一个比较简单的target
target = "0000" + "f" * 60
这里设置的target是四个“0”后面接60个“f”的字符串,实际上表示的是一个64位的十六进制数,因为使用的是sha256算法,它输出的是256bit的哈希值,转化为16进制表示就是一个64位的数(因为一个16进制数需要4bits来表示)。
target值是比特币网络中用来决定挖矿难度的一个重要参数,它表示区块头的哈希值必须小于等于的一个数字。target值越小,挖矿难度就越大,因为满足条件的哈希值的范围就越小。
这里找到使hash值小于target(“0000” + “f” * 60)的nonce值,其实也就是找到使hash值的前四位都是“0”的nonce值。这其实是比较容易的一个目标,是为了减少计算量,方便在本地进行模拟。
真实的target会动态调整,可以参考以下的资料:
比特币工作量证明难度值及难度调整详解 - 知乎 (zhihu.com)
说说比特币的 “全网难度” - 知乎 (zhihu.com)
然后稍微解释一下**“挖矿”的及时止损和权衡**:
# 如果收到其他节点的新区块通知,就停止出块并更新自己的区块链和交易池,这里化简为直接跟node[0]来比较
# 实际上收到其他节点的新区块并不一定要停止更新自己的区块链,这里是为了化简
if len(self.chain) != len(nodes[0].chain):
print(f"{self.name} stopped creating block {index} because a new block was received.")
self.chain = nodes[0].chain.copy()
self.pool = nodes[0].pool.copy()
break
因为实际的挖矿是一个困难而漫长,并且有一定风险(但是收益也大)的一个东西,由于比特币采用了最长链机制(就是只承认最长的那条链),所以自己在遍历随机数的时候还要看一下别人有没有已经得到了正确的随机数,然后拥有了更长的链。但是这时候是否放弃自己的计算是需要权衡的,你可以选择不放弃搏一搏(如果自己掌握了足够的算力),也可以直接放弃。
这里是直接化简为跟node[0]做比较,如果发现node[0]和自己不一致就停止挖矿,这个是为了方便模拟比特币的运作。实际上这段代码也不会产生作用,一方面是因为在测试的时候节点只是串行地创建区块,另一方面是因为在一个节点成功创建区块后就直接让所有节点都接收到了这个区块。
而在真实场景中,比特币的最长链机制中存在着各种博弈,例如:矿工需要在挖矿收益和挖矿成本之间做出权衡,选择是否参与挖矿,以及选择在哪条链上挖矿。一般来说,矿工会选择在最长链上挖矿,因为这样可以获得最大的收益,同时也有利于维护网络的安全性和稳定性。但是,如果矿工拥有超过51%的算力,他们也可能选择在自己的私有链上挖矿,试图制造分叉或者双花,从而攻击网络和其他用户。这就是所谓的51%攻击。
比特币“最长链”是怎么选择? - 知乎 (zhihu.com)
或许还有其他的疑问,比如为什么计算hash值需要根据区块高度(index)、上一个区块的哈希(prev_hash)、交易列表(transactions)、随机数(nonce)来计算hash值?能不能少一个减少计算量?
这个就交给大家思考了:少了其中一个可能会导致什么事情能够发生,使得区块链不能维护其正确性和顺序?
def receive_block(self, new_block):
# 接收其他节点广播的新区块并验证其有效性,如果有效就添加到自己的区块链中,并从交易池中删除已确认的交易
index = new_block.index
prev_hash = new_block.prev_hash
transactions = new_block.transactions
nonce = new_block.nonce
hash = new_block.hash
# 验证新区块的前一个区块是否是自己的区块链的最后一个区块
# 这里为了简化,如果发现不是就会直接拒绝掉,
# 实际上并不一定直接拒绝,如果他的链更长可能会承认他的链
prev_block = self.chain[-1]
if prev_block.index != index - 1 or prev_block.hash != prev_hash:
print(f"{self.name} rejected block {index} because the previous block does not match.")
return
# 验证新区块的哈希值是否满足目标值
target = "0000" + "f" * 60
message = str(index) + prev_hash + str(transactions) + str(nonce)
# 如果发现计算出来的hash值和这个区块的hash值不一致
# 或者计算出来的hash > target
# 就拒绝这个新区块
if hashlib.sha256(message.encode()).hexdigest() != hash or hash > target:
print(f"{self.name} rejected block {index} because the hash value is invalid.")
return
# 如果新区块有效,就添加到自己的区块链中,并从交易池中删除已确认的交易,实际上这里只是删除创建区块的节点的自己的交易池中的交易,因为上面假设交易并没有真正广播出去
self.chain.append(new_block)
for transaction in transactions:
if transaction in self.pool:
self.pool.remove(transaction)
print(f"{self.name} accepted block {index} and updated its chain and pool.")
上面的接受区块操作也可以认为只干3件事:
实际上这个可以直接在节点的初始化中定义,不过为了表示一种启动的过程,这里把创建创世区块写做一个函数:
def create_genesis_block(self):
# 创建创世区块并添加到区块链中
genesis_block = Block(0, "0" * 64, ["Hello, Bitcoin!"], 0, "0" * 64)
self.chain.append(genesis_block)
print(f"{self.name} created the genesis block: \n{genesis_block}")
哈哈哈,到这里其实我们的主要代码已经编写完成啦。
我们可以测试我们的代码能否正常运行,并观察其中的结果,整体的代码如下:
# thinkerhui 2023/10/08
# 导入所需的库
import hashlib
import random
import time
# 定义区块类
class Block:
def __init__(self, index, prev_hash, transactions, nonce, hash):
self.index = index # 区块高度
self.prev_hash = prev_hash # 前一个区块的哈希值
self.transactions = transactions # 包含的交易列表
self.nonce = nonce # 随机数
self.hash = hash # 区块的哈希值
def __str__(self):
return f"Block {self.index}: \nPrev_hash: {self.prev_hash} \nTransactions: {self.transactions} \nNonce: {self.nonce} \nHash: {self.hash}"
# 定义节点类
class Node:
def __init__(self, name):
self.name = name # 节点名称
self.chain = [] # 节点维护的区块链
self.pool = [] # 节点收集的交易池
def create_genesis_block(self):
# 创建创世区块并添加到区块链中
genesis_block = Block(0, "0" * 64, ["Hello, Bitcoin!"], 0, "0" * 64)
self.chain.append(genesis_block)
print(f"{self.name} created the genesis block: \n{genesis_block}")
def create_transaction(self):
# 模拟创建一个交易并添加到交易池中
sender = self.name # 确定转账转出方
receiver = random.choice(nodes).name # 确定转账转入方,这里是随机选择一个节点,方便进行模拟
amount = random.randint(1, 10) # 确定转账金额,这是随机金额,方便进行模拟
transaction = f"{sender} sent {amount} BTC to {receiver}" # 定义交易字符串
self.pool.append(transaction) # 加入交易池
print(f"{self.name} created a transaction: {transaction}") # 打印交易信息,假装在广播
def create_block(self):
# 尝试创建一个区块并广播给其他节点
# 获取前一个区块的信息,注意这里操作决定了区块链需要一个创世区块,否则启动的时候哪里有前一个区块
prev_block = self.chain[-1]
index = prev_block.index + 1 # 区块高度(位置)+1
prev_hash = prev_block.hash
# 设置目标值,即哈希值要小于等于该值才算有效
# 这里实际中应该动态调整,这里直接设了一个比较简单的target
target = "0000" + "f" * 60
# 从交易池中选择一些交易作为候选,这里直接选的交易池的前5个
transactions = self.pool[:5]
# 初始化随机数和哈希值
nonce = 0
hash = ""
# 不断尝试不同的随机数,直到找到满足条件的哈希值或者收到其他节点的新区块通知为止
while hash > target or hash == "":
# 如果收到其他节点的新区块通知,就停止出块并更新自己的区块链和交易池
# 实际上收到其他节点的新区块并不一定要停止更新自己的区块链,这里是为了化简
if len(self.chain) != len(nodes[0].chain):
print(f"{self.name} stopped creating block {index} because a new block was received.")
self.chain = nodes[0].chain.copy()
self.pool = nodes[0].pool.copy()
break
# 计算区块的哈希值
nonce += 1 # 不断尝试不同的随机数
# 根据区块高度(index)、上一个区块的哈希(prev_hash)、交易列表(transactions)、随机数(nonce)来计算hash值
message = str(index) + prev_hash + str(transactions) + str(nonce)
hash = hashlib.sha256(message.encode()).hexdigest() # sha256输出的是256bit的值,转化为16进制表示就是一个64位的数
# 如果找到满足条件的哈希值,就创建一个新区块并广播给其他节点
if hash <= target:
new_block = Block(index, prev_hash, transactions, nonce, hash)
print(f"{self.name} created a new block: \n{new_block}")
# 这里是让所有的节点都收到这个新创建的区块
for node in nodes:
node.receive_block(new_block)
def receive_block(self, new_block):
# 接收其他节点广播的新区块并验证其有效性,如果有效就添加到自己的区块链中,并从交易池中删除已确认的交易
index = new_block.index
prev_hash = new_block.prev_hash
transactions = new_block.transactions
nonce = new_block.nonce
hash = new_block.hash
# 验证新区块的前一个区块是否是自己的区块链的最后一个区块
# 这里为了简化,如果发现不是就会直接拒绝掉,
# 实际上并不一定直接拒绝,如果他的链更长可能会承认他的链
prev_block = self.chain[-1]
if prev_block.index != index - 1 or prev_block.hash != prev_hash:
print(f"{self.name} rejected block {index} because the previous block does not match.")
return
# 验证新区块的哈希值是否满足目标值
target = "0000" + "f" * 60
message = str(index) + prev_hash + str(transactions) + str(nonce)
# 如果发现计算出来的hash值和这个区块的hash值不一致
# 或者计算出来的hash > target
# 就拒绝这个新区块
if hashlib.sha256(message.encode()).hexdigest() != hash or hash > target:
print(f"{self.name} rejected block {index} because the hash value is invalid.")
return
# 如果新区块有效,就添加到自己的区块链中,并从交易池中删除已确认的交易
self.chain.append(new_block)
for transaction in transactions:
if transaction in self.pool:
self.pool.remove(transaction)
print(f"{self.name} accepted block {index} and updated its chain and pool.")
# 创建四个节点
nodes = [Node("Alice"), Node("Bob"), Node("Charlie"), Node("David")]
# 让四个节点创建创世区块
for node in nodes:
node.create_genesis_block()
# 模拟每个节点轮流进行一次出块或创建交易的操作,重复10次
for i in range(10):
node = nodes[i % len(nodes)]
action = random.choice(["create_block", "create_transaction"])
if action == "create_block":
node.create_block()
else:
node.create_transaction()
time.sleep(1) # 暂停一秒,方便观察输出结果
可以看到如下的输出结果(每个人每次运行都会不一样,因为有随机的过程):
Alice created the genesis block:
Block 0:
Prev_hash: 0000000000000000000000000000000000000000000000000000000000000000
Transactions: ['Hello, Bitcoin!']
Nonce: 0
Hash: 0000000000000000000000000000000000000000000000000000000000000000
Bob created the genesis block:
Block 0:
Prev_hash: 0000000000000000000000000000000000000000000000000000000000000000
Transactions: ['Hello, Bitcoin!']
Nonce: 0
Hash: 0000000000000000000000000000000000000000000000000000000000000000
Charlie created the genesis block:
Block 0:
Prev_hash: 0000000000000000000000000000000000000000000000000000000000000000
Transactions: ['Hello, Bitcoin!']
Nonce: 0
Hash: 0000000000000000000000000000000000000000000000000000000000000000
David created the genesis block:
Block 0:
Prev_hash: 0000000000000000000000000000000000000000000000000000000000000000
Transactions: ['Hello, Bitcoin!']
Nonce: 0
Hash: 0000000000000000000000000000000000000000000000000000000000000000
Alice created a transaction: Alice sent 8 BTC to Alice
Bob created a new block:
Block 1:
Prev_hash: 0000000000000000000000000000000000000000000000000000000000000000
Transactions: []
Nonce: 22844
Hash: 0000cbc7a7c49dacc4a0b61f51c10cbc1c806ccb48db8f0e1ada6ac0f8f0e7b4
Alice accepted block 1 and updated its chain and pool.
Bob accepted block 1 and updated its chain and pool.
Charlie accepted block 1 and updated its chain and pool.
David accepted block 1 and updated its chain and pool.
Charlie created a new block:
Block 2:
Prev_hash: 0000cbc7a7c49dacc4a0b61f51c10cbc1c806ccb48db8f0e1ada6ac0f8f0e7b4
Transactions: []
Nonce: 38441
Hash: 00007abd6de3c175698cd372254801fdd6550ebb761f540bc25558e1ee1f8fb0
Alice accepted block 2 and updated its chain and pool.
Bob accepted block 2 and updated its chain and pool.
Charlie accepted block 2 and updated its chain and pool.
David accepted block 2 and updated its chain and pool.
David created a transaction: David sent 4 BTC to David
Alice created a transaction: Alice sent 2 BTC to Bob
Bob created a new block:
Block 3:
Prev_hash: 00007abd6de3c175698cd372254801fdd6550ebb761f540bc25558e1ee1f8fb0
Transactions: []
Nonce: 34800
Hash: 0000c6dbf042a301da5fed4bb83b00d637a02a804f3959c7f3e63335ea10d6ff
Alice accepted block 3 and updated its chain and pool.
Bob accepted block 3 and updated its chain and pool.
Charlie accepted block 3 and updated its chain and pool.
David accepted block 3 and updated its chain and pool.
Charlie created a transaction: Charlie sent 3 BTC to Charlie
David created a transaction: David sent 10 BTC to Alice
Alice created a new block:
Block 4:
Prev_hash: 0000c6dbf042a301da5fed4bb83b00d637a02a804f3959c7f3e63335ea10d6ff
Transactions: ['Alice sent 8 BTC to Alice', 'Alice sent 2 BTC to Bob']
Nonce: 101424
Hash: 0000c6b36c5336c2d83c411509ab5216fdd20719c6c9d9682a0e1e035358c0c6
Alice accepted block 4 and updated its chain and pool.
Bob accepted block 4 and updated its chain and pool.
Charlie accepted block 4 and updated its chain and pool.
David accepted block 4 and updated its chain and pool.
Bob created a transaction: Bob sent 6 BTC to David
上面便是此次实战,但是你也在实战过程中发现我这个代码的种种不完善(因为它是如此简单,因此也和真实的bitcoin有如此的出入),所以大可尝试在下面的方向来改进代码(也可以自行探索),这才算是你自己的真正的区块链python实战,不然只是在抄我的代码(菜鸡互啄):
由于本人学识有限,只是SSE,SYSU的一名普通学生,如有疏漏和错误请在评论区指出。如果有疑问也可以在评论区提出。如果没有发现什么问题,也可以在评论区水一水。感谢大家的点赞、收藏和关注。