本文受启发于Learn Blockchains by Building One ,但是做了大量改进,使之更符合于真实的比特币系统。
在linux下确保已经安装Python, pip , Flask, requests
一个方便查看http结果的插件,比如chrome下插件restlet client
放在python文件block.py中,大致框架是:
# -*- coding: utf-8 -*-
from collections import defaultdict
import time
import hashlib
import requests
class Blockchain(object):
def __init__(self, node_id, need_genesis=False):
# all data store in memory
self.chain = []
self.in_chain_transactions = []
self.not_in_chain_transactions = []
def new_transaction(self, sender, recipient, amount):
pass
def hash(self, struct):
pass
def mine_block(self, proof, node_id, previous_hash=None):
pass
其中在构造函数中创建了3个列表,一个用于储存区块链,一个用于储存在区块链中,也就是确认过的交易,一个用于储存没有确认过的交易。也就是说我们的区块链一直存储在内存中,实际的比特币是存储在文件中并且使用leveldb做索引的。
提供创建新交易的函数,以及挖矿函数。
根据《精通比特币》第五章的介绍,我们的交易结构,保留关键属性的前提下简化为:
{
"input": [
[0, 0, 50 ],
[1, 0, 50 ],
],
"output": [
["cf5c82d91ae3f4826f0ea80924d02843", 10 ],
["af5c8924d028432d91ae3f4826f0ea80", 40 ]
],
"time": 1525705520.58631
}
有多个输入和多个输出。
输入是指向之前的某个输出下面的某个UTXO,第一项为:之前的某个交易在in_chain_transactions中的下标;第二项为:该UTXO在交易中的第几个;第三项为:输出金额。
输出是指此次输出的接收地址和收到的金额。注意为了简化,没有加上锁定脚本和解锁脚本,也就是没有私钥的签名和公钥的验证过程。
还有当前时间戳。
{
"header":{
"merkle_root": "b80972590a71076b3c16019b167cd528d1c71356e4a5f0edda760cabb94a8a1f",
"nonce": 0,
"previous_hash": "6ff20431f67c221ef7fb832dc6538c6024d153f5ccab44cdeae9223f11f62e40",
"proof": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"timestamp": 1525705542.136177
},
"transactions":[
{
"input":["coinbase"],
"output":[["af5c8924d028432d91ae3f4826f0ea80", 50 ]],
"time": 1525705542.136026
},
{
"input":[[0, 0, 50 ]],
"output":[["jack", 10 ], ["af5c8924d028432d91ae3f4826f0ea80", 40 ]],
"time": 1525705520.58631
}
]
}
主要是区块头和交易信息。为了演示,难度设为最大值,也就是没有挖矿的难度。其中merkle_root也做了简化,变成直接对所有交易做一个hash。
生成一个交易,接收的参数是发送方地址和接收方地址以及此次发送的金额(忽略签名)。我们需要根据发送方之前的utxo,组合出交易,发给接受者,如果找到的发送方的utxo总金额大于需要发送的金额,需要找零。(这里不考虑交易费)
这里需要在初始化中添加一个记录所有用户的utxo的字典。
class Blockchain(object):
def __init__(self, node_id, need_genesis=False):
# all data store in memory
self.chain = []
self.in_chain_transactions = []
self.not_in_chain_transactions = []
self.utxo = defaultdict(list)
生成交易的代码为:
class Blockchain(object):
...
def new_transaction(self, sender, recipient, amount):
tx, msg = self.assemble_transaction(sender, recipient, amount)
if tx is None:
return msg
self.not_in_chain_transactions.append(tx)
return msg
def assemble_transaction(self, sender, recipient, amount):
amount = int(amount)
tx_input = []
tx_output = []
sender_utxo = self.utxo[sender]
sender_utxo = sorted(sender_utxo, key=lambda x: x[2], reverse=True)
remaining_need_amount = amount
for tx_id, output_index, cur_amount in sender_utxo:
tx_input.append((tx_id, output_index, cur_amount))
remaining_need_amount -= cur_amount
if remaining_need_amount == 0:
tx_output.append((recipient, amount))
break
elif remaining_need_amount < 0:
tx_output.append((recipient, amount))
tx_output.append((sender, -remaining_need_amount))
break
else:
continue
if remaining_need_amount > 0:
return (None, "transaction amount is beyond your balance")
return (
{
"input": tx_input,
"output": tx_output,
"time": time.time()
},
'submit success, but you MUST wait for 6 confirmations at least'
new_transaction是生成新交易的函数,其中主要的就是利用发送方的utxo组装交易的输入,函数assemble_transaction负责这块。
class Blockchain(object):
...
def mine_block(self, proof, node_id, previous_hash=None):
# mine
self.new_transaction('0', node_id, 50)
# 仍然要检查交易的合法性!!!因为节点之间区块、交易同步有延迟,有的余额可能超出了
transactions_to_be_mined = self.check_transactions()
header = {
"previous_hash": previous_hash if previous_hash is not None else self.hash(self.chain[-1]["header"]),
"merkle_root": self.hash(transactions_to_be_mined),
"timestamp": time.time(),
"proof": proof,
"nonce": 0,
}
for nonce in range(1024):
header["nonce"] = nonce
header["timestamp"] = time.time()
if self.hash(header) <= proof:
break
block = {
"header": header,
"transactions": transactions_to_be_mined
}
self.chain.append(block)
self.update_utxo(transactions_to_be_mined)
self.in_chain_transactions += transactions_to_be_mined
self.not_in_chain_transactions = []
self.new_transaction('0', node_id, 50)
其中这一行
是指矿工每次将区块中的第一笔交易,设为转给自己50btc的coinbase交易(这里忽略奖励金额的调整)。那么需要一个地址标记矿工,这里先将node_id这个变量来标识矿工。这个交易和其他交易不同,没有发送方,只有接收方。为了实现这个目的,修改new_transaction和assemble_transaction函数,特殊处理一下奖励区块的交易:
class Blockchain(object):
...
def new_transaction(self, sender, recipient, amount):
tx, msg = self.assemble_transaction(sender, recipient, amount)
if tx is None:
return msg
if sender == '0':
self.not_in_chain_transactions.insert(0, tx)
else:
self.not_in_chain_transactions.append(tx)
# self.broadcast_transaction()
return msg
def assemble_transaction(self, sender, recipient, amount):
amount = int(amount)
tx_input = []
tx_output = []
if sender == '0':
tx_input = ["coinbase"]
tx_output = [(recipient, amount)]
return (
{
"input": tx_input,
"output": tx_output,
"time": time.time()
},
'submit success, but you MUST wait for 100 confirmations at least'
)
sender_utxo = self.utxo[sender]
sender_utxo = sorted(sender_utxo, key=lambda x: x[2], reverse=True)
remaining_need_amount = amount
for tx_id, output_index, cur_amount in sender_utxo:
tx_input.append((tx_id, output_index, cur_amount))
remaining_need_amount -= cur_amount
if remaining_need_amount == 0:
tx_output.append((recipient, amount))
break
elif remaining_need_amount < 0:
tx_output.append((recipient, amount))
tx_output.append((sender, -remaining_need_amount))
break
else:
continue
if remaining_need_amount > 0:
return (None, "transaction amount is beyond your balance")
接着看mine_block,其中有check_transactions函数,是为了检查区块里面的交易是否合法,主要是检查同一个utxo没有被双重支付,以及交易是不是利用的之前的某个utxo。代码如下:
class Blockchain(object):
...
def check_transactions(self):
checked_transactions = []
utxo_set = set()
for cur_tx in self.not_in_chain_transactions:
if cur_tx["input"] == ["coinbase"]:
checked_transactions.append(cur_tx)
else:
is_append = True
for cur_input in cur_tx["input"]:
if (cur_input not in utxo_set and
self.in_chain_transactions[cur_input[0]]["output"][cur_input[1]][1] == cur_input[2]
):
utxo_set.add(cur_input)
else:
is_append = False
break
if is_append:
checked_transactions.append(cur_tx)
return checked_transactions
接着看mine_block,挖出区块之后,需要更新utxo信息,由函数update_utxo完成。
class Blockchain(object):
...
def update_utxo(self, transactions_to_be_mined):
append_transactions_start_index = len(self.in_chain_transactions)
for i in range(len(transactions_to_be_mined)):
cur_transaction = transactions_to_be_mined[i]
for j in range(len(cur_transaction["input"])):
# tx_input.append((tx_id, output_index, cur_amount))
cur_input = cur_transaction["input"][j]
if cur_input == "coinbase":
continue
# 可以优化速度,这一行完全体现了交易和交易之间的链接关系:每一个交易的输入都是之前某个交易的输出的某个utxo
input_addr = self.in_chain_transactions[cur_input[0]]["output"][cur_input[1]][0]
self.utxo[input_addr].remove(cur_input)
tx_id = i + append_transactions_start_index
for j in range(len(cur_transaction["output"])):
cur_output = cur_transaction["output"][j]
output_index = j
recipient, cur_amount = cur_output
self.utxo[recipient].append((tx_id, output_index, cur_amount))
其中相当于没有设置挖矿的难度,nonce值也只在0-1023遍历,永远都是第一次就挖的区块。挖矿函数到此结束。
首先需要一个节点,首先挖出一个创世区块,也就是比特币中,在2009年由中本聪挖出的创世区块,它是所有区块的初始父节点。修改一下初始化函数即可:
class Blockchain(object):
def __init__(self, node_id, need_genesis=False):
# all data store in memory
self.chain = []
self.in_chain_transactions = []
self.not_in_chain_transactions = []
self.utxo = defaultdict(list)
if need_genesis:
self.mine_block('f' * 64, self.node_id, 0)
至此,区块链的结构到此结束,下一篇介绍,如何搭建起一个运行区块链的节点和网络。完整代码为:
# -*- coding: utf-8 -*-
from collections import defaultdict
import time
import hashlib
import requests
class Blockchain(object):
def __init__(self, node_id, need_genesis=False):
# all data store in memory
self.chain = []
self.in_chain_transactions = []
self.not_in_chain_transactions = []
self.utxo = defaultdict(list)
self.node_id = node_id
self.neighbors = set()
if need_genesis:
self.mine_block('f' * 64, self.node_id, 0)
def hash(self, struct):
return hashlib.sha256(str(struct)).hexdigest()
def check_transactions(self):
checked_transactions = []
utxo_set = set()
for cur_tx in self.not_in_chain_transactions:
if cur_tx["input"] == ["coinbase"]:
checked_transactions.append(cur_tx)
else:
is_append = True
for cur_input in cur_tx["input"]:
if (cur_input not in utxo_set and
self.in_chain_transactions[cur_input[0]]["output"][cur_input[1]][1] == cur_input[2]
):
utxo_set.add(cur_input)
else:
is_append = False
break
if is_append:
checked_transactions.append(cur_tx)
return checked_transactions
def mine_block(self, proof, node_id, previous_hash=None):
# mine
self.new_transaction('0', node_id, 50)
# 浠嶇劧瑕佹鏌ヤ氦鏄撶殑鍚堟硶鎬�!!!鍥犱负鑺傜偣涔嬮棿鍖哄潡銆佷氦鏄撳悓姝ユ湁寤惰繜锛屾湁鐨勪綑棰濆彲鑳借秴鍑轰簡
transactions_to_be_mined = self.check_transactions()
header = {
"previous_hash": previous_hash if previous_hash is not None else self.hash(self.chain[-1]["header"]),
"merkle_root": self.hash(transactions_to_be_mined),
"timestamp": time.time(),
"proof": proof,
"nonce": 0,
}
for nonce in range(1024):
header["nonce"] = nonce
header["timestamp"] = time.time()
if self.hash(header) <= proof:
break
block = {
"header": header,
"transactions": transactions_to_be_mined
}
self.chain.append(block)
self.update_utxo(transactions_to_be_mined)
self.in_chain_transactions += transactions_to_be_mined
self.not_in_chain_transactions = []
def resolve_conflicts(self):
new_chain = None
new_in_chain_transactions = None
new_not_in_chain_transactions = None
new_utxo = None
max_length = len(self.chain)
for neighbour in self.neighbors:
url = "http://%s/chain" % neighbour[0]
r = requests.get(url)
if r.status_code != 200:
continue
r_json = r.json()
if r_json["length"] > max_length:
max_length = r_json["length"]
new_chain = r_json["chain"]
new_in_chain_transactions = r_json["in_chain_transactions"]
new_not_in_chain_transactions = r_json["not_in_chain_transactions"]
new_utxo = r_json["utxo"]
if new_chain:
self.chain = new_chain
self.in_chain_transactions = new_in_chain_transactions
self.not_in_chain_transactions = new_not_in_chain_transactions
self.utxo = new_utxo
return True
return False
def update_utxo(self, transactions_to_be_mined):
append_transactions_start_index = len(self.in_chain_transactions)
for i in range(len(transactions_to_be_mined)):
cur_transaction = transactions_to_be_mined[i]
for j in range(len(cur_transaction["input"])):
# tx_input.append((tx_id, output_index, cur_amount))
cur_input = cur_transaction["input"][j]
if cur_input == "coinbase":
continue
# 鍙互浼樺寲閫熷害
input_addr = self.in_chain_transactions[cur_input[0]]["output"][cur_input[1]][0]
self.utxo[input_addr].remove(cur_input)
tx_id = i + append_transactions_start_index
for j in range(len(cur_transaction["output"])):
cur_output = cur_transaction["output"][j]
output_index = j
recipient, cur_amount = cur_output
self.utxo[recipient].append((tx_id, output_index, cur_amount))
def assemble_transaction(self, sender, recipient, amount):
amount = int(amount)
tx_input = []
tx_output = []
if sender == '0':
tx_input = ["coinbase"]
tx_output = [(recipient, amount)]
return (
{
"input": tx_input,
"output": tx_output,
"time": time.time()
},
'submit success, but you MUST wait for 100 confirmations at least'
)
sender_utxo = self.utxo[sender]
sender_utxo = sorted(sender_utxo, key=lambda x: x[2], reverse=True)
remaining_need_amount = amount
for tx_id, output_index, cur_amount in sender_utxo:
tx_input.append((tx_id, output_index, cur_amount))
remaining_need_amount -= cur_amount
if remaining_need_amount == 0:
tx_output.append((recipient, amount))
break
elif remaining_need_amount < 0:
tx_output.append((recipient, amount))
tx_output.append((sender, -remaining_need_amount))
break
else:
continue
if remaining_need_amount > 0:
return (None, "transaction amount is beyond your balance")
return (
{
"input": tx_input,
"output": tx_output,
"time": time.time()
},
'submit success, but you MUST wait for 6 confirmations at least'
)
def new_transaction(self, sender, recipient, amount):
tx, msg = self.assemble_transaction(sender, recipient, amount)
if tx is None:
return msg
if sender == '0':
self.not_in_chain_transactions.insert(0, tx)
else:
self.not_in_chain_transactions.append(tx)
# self.broadcast_transaction()
return msg