比特币 (11):300行代码实现一个简化版比特币系统(一)

By | 2018-05-07
1,081 views | 0 Comment

本文受启发于Learn Blockchains by Building One ,但是做了大量改进,使之更符合于真实的比特币系统。

一、准备工作

在linux下确保已经安装Python, pip , Flask, requests

一个方便查看http结果的插件,比如chrome下插件restlet client

二、开始构建Blockchain

1. 首先创建一个Blockchain类

放在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做索引的。
提供创建新交易的函数,以及挖矿函数。

2. 交易结构

根据《精通比特币》第五章的介绍,我们的交易结构,保留关键属性的前提下简化为:

{
	"input": [
		[0, 0, 50 ],
		[1, 0, 50 ],
	],
	"output": [
		["cf5c82d91ae3f4826f0ea80924d02843", 10 ],
		["af5c8924d028432d91ae3f4826f0ea80", 40 ]
	],
	"time": 1525705520.58631
}

有多个输入和多个输出。
输入是指向之前的某个输出下面的某个UTXO,第一项为:之前的某个交易在in_chain_transactions中的下标;第二项为:该UTXO在交易中的第几个;第三项为:输出金额。
输出是指此次输出的接收地址和收到的金额。注意为了简化,没有加上锁定脚本和解锁脚本,也就是没有私钥的签名和公钥的验证过程。
还有当前时间戳。

3. 区块结构

{
	"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。

4. 现在就来加入交易吧

生成一个交易,接收的参数是发送方地址和接收方地址以及此次发送的金额(忽略签名)。我们需要根据发送方之前的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负责这块。

5. 创建新块(挖矿)

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遍历,永远都是第一次就挖的区块。挖矿函数到此结束。

6. 创世区块

首先需要一个节点,首先挖出一个创世区块,也就是比特币中,在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)
 

至此,区块链的结构到此结束,下一篇介绍,如何搭建起一个运行区块链的节点和网络。完整代码为:block.py