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

本文受启发于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)

 

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

# -*- 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

 

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
其大力 1人赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线