【原创】区块链开发:区块链基本原型 #C03


#1

从本文开始,我们将开发一个区块链原型,并陆续实现各项功能,使用的语言为Node.js。


区块链作为一个去中心化的数据库,通常由区块、共识、P2P网络、存储、交易等多个部分组成。而一个完整的区块链,例如比特币或者以太坊,各个功能的代码都比较复杂,我们也没有必要再造轮子实现一遍这样规模的区块链。但是我们可以参考其结构设计,实现一个简易版本的区块链,这里将各模块尽量解耦,每个模块单独的实现、演示,以便大家更好地理解区块链。

基本结构

区块链中的区块是其核心结构,每个节点将一部分交易打包,并加上一定的元信息,就构成了区块。区块被创建之后还要被广播出去,其他节点接收后会进行验证,如果验证通过,那么就加入当前区块链并写入数据库。从以上信息可知,区块分为两个部分,区块体就是打包的交易信息,一般用merkle书进行存储,而区块头中包含了识别区块的关键信息。

// block.js

class Block extends EventEmitter {
    constructor(data, consensus) {
        super();
        // body
        this.transactions_ = data ? data.transactions : [];
        // header
        this.version_ = 0;
        this.height_ = data ? data.previous_block.height + 1 : -1;
        this.previous_hash_ = data ? data.previous_block.hash : null;
        this.timestamp_ = (new Date()).getTime();
        this.merkle_hash_ = data ? this.calc_merkle_hash(data.transactions) : null;
        this.generator_publickey_ = data ? data.keypair.publicKey.toString('hex') : null;
        this.hash_ = null;
        this.block_signature_ = null;
        // header extension
        this.consensus_data_ = {};

        if (consensus) {
            let self = this;
            setImmediate(() => {
                self.make_proof(consensus, data.keypair);
            });
        }

    }
}

Block类表示生成的区块结构,这里参考比特币,各成员变量有如下含义:

  • version_:区块版本
  • height_:当前区块高度
  • previous_hash_:前一个区块的哈希值
  • timestamp_:当前区块生成时的时间戳
  • merkle_hash_:默克尔树根,由打包的交易计算出
  • generator_publickey_:生成区块节点的公钥
  • hash_:当前区块的哈希值
  • block_signature_:生成区块节点对当前区块哈希值的签名
  • consensus_data_:共识数据,依据公式算法不同而不同
  • transactions_:打包的交易数据

其中trasnscations_区块体中的内容,剩余的都是区块头的内容。所有交易构成一颗merkle树,树根哈希值包含在区块头中。

generator_publickey_配合block_signature_使用,用于验证区块签名是否正确。

构建区块时,需要传入两个参数,data表示构建区块所用到的数据,包含keypair(节点的密钥对)、previous_block(上一个区块)和transactions(交易数据);而consensus表示共识方法实例,这在后面会讲到。

创建区块后会进行共识操作,而不同的共识方法其过程耗费的时间不同,所以这里通过setImmediate将其设置为异步过程,共识完成后会发射相应的信号。

如下是一些常用函数:

// block.js

    get_version() { return this.version_; }
    get_height() { return this.height_; }
    get_hash() { return this.hash_; }
    get_previous_hash() { return this.previous_hash_; }
    get_timestamp() { return this.timestamp_; }
    get_signature() { return this.block_signature_; }
    get_publickey() { return this.generator_publickey_; }
    get_transactions() { return this.transactions_; }
    get_consensus_data() { return this.consensus_data_; }
    set_consensus_data(data) { this.consensus_data_ = data; }
    toObject() {
        let block = {
            "version": this.version_,
            "height": this.height_,
            "previous_hash": this.previous_hash_,
            "timestamp": this.timestamp_,
            "merkle_hash": this.merkle_hash_,
            "generator_publickey": this.generator_publickey_,
            "hash": this.hash_,
            "block_signature": this.block_signature_,
            "consensus_data": this.consensus_data_,
            "transactions": this.transactions_
        };
        return block;
    }

    calc_hash(data) {
        return crypto.createHash('sha256').update(data).digest().toString('hex');
    }
    calc_merkle_hash() {
        // calc merkle root hash according to the transactions in the block
        var hashes = [];
        for (var i = 0; i < this.transactions_.length; ++i) {
            hashes.push(this.calc_hash(this.transactions_.toString('utf-8')));
        }
        while (hashes.length > 1) {
            var tmp = [];
            for (var i = 0; i < hashes.length / 2; ++i) {
                let data = hashes[i * 2] + hashes[i * 2 + 1];
                tmp.push(this.calc_hash(data));
            }
            if (hashes.length % 2 === 1) {
                tmp.push(hashes[hashes.length - 1]);
            }
            hashes = tmp;
        }
        return hashes[0] ? hashes[0] : null;
    }

创世区块

区块链网络由诸多区块链节点构成,每一个节点的类型和职责也不相同,例如一个完整的比特币节点具有路由、区块链数据库、挖矿、钱包服务的职能。

而在这里主要设计节点的区块链职能,也就是处理产生(挖矿)和收到的区块,将验证过的区块写入数据库,连接成链。那么首先解决的一个问题就是如何产生创世区块。

// blcokchain.js

var Block = require("./block");
const genesis_block = require("./genesis_block.json");
var Node = require("./network");
var Account = require("./account");
var Transaction = require("./transaction");
var Msg = require("./message");
var MessageType = require("./message").type;

class BlockChain {
    constructor(Consensus, keypair, id) {
        // todo
        this.pending_block_ = {};
        this.chain_ = [];

        // ///////////////////////////////////////
        this.genesis_block_ = genesis_block;
        this.last_block_ = genesis_block;
        this.save_last_block();

        this.account_ = new Account(keypair, id);
        this.consensus_ = new Consensus(this);
        this.node_ = null;
    }

    start() {
        this.node_ = new Node(this.get_account_id());
        this.node_.on("message", this.on_data.bind(this));
        this.node_.start();
        // start loop
        var self = this;
        setTimeout(function next_loop() {
            self.loop(function () {
                setTimeout(next_loop, 1000);
            });
        }, 5000);
    }
    loop(cb) {
        let self = this;
        if (this.consensus_.prepared()) {
            this.generate_block(this.get_account_keypair(), () => {
                // broadcast block
                let block = self.get_last_block();
                console.log(`node: ${self.get_account_id()} generate block! block height: ${block.height} hash: ${block.hash}`);
            });
        }
        cb();
    }

    save_last_block() {
        // query from db via hash
        // if not exist, write into db, else do nothing
        // todo(tx is also need to store?)
        if (this.pending_block_[this.last_block_.hash]) {
            delete this.pending_block_[this.last_block_.hash];
        }
        this.chain_.push(this.last_block_);
    }
    on_data(msg) {
        switch (msg.type) {
            case MessageType.Block:
                {
                    let block = msg.data;
                    // console.log(`node: ${this.get_account_id()} receive block: height ${block.height}`);
                    // check if exist
                    if (this.pending_block_[block.hash] || this.get_block(block.hash))
                        return;
                    // verify
                    if (!this.verify(block))
                        return;

                    this.pending_block_[block.hash] = block;

                    // add to chain
                    if (block.height == this.last_block_.height + 1) {
                        // console.log("on block data");
                        this.commit_block(block);
                        // console.log("----------add block");
                    } else {
                        // fork or store into tmp
                        // console.log('fork');
                        // todo
                    }
                    // broadcast
                    this.broadcast(msg);
                }
                break;
            case MessageType.Transaction:
                {
                    // check if exist(pending or in chain) verify, store(into pending) and broadcast
                }
                break;
            default:
                break;
        }
    }
}

BlockChain主要包含两个成员,分别是链头(创世区块)和链尾(最新区块),创世区块是硬编码到区块结构中的,这样做就能保证每个节点的区块根都是安全可靠的。

account_consensus_node_分别代表当前节点的账户、共识方法和网络传输模块。其中keypairid可以在创建BlockChain时显式传入Account模块,从外部指定当前节点的id和密钥。也可以不传入,那么Account模块就会从本地配置文件中读取相应的信息来构建账户,由于这里Account模块尚未实现,所以这里测试代码中多用显式传参。

区块一旦加入链中就应该持久化到本地数据库,但是存储模块也尚未实现,所以这里用chain_pending_block_两个成员来存储当前主链和剩余区块,后续应将其替换。

let block_chain= new BlockChain(Consensus);
block_chain.start();

每次节点启动时,会直接加载创世区块,然后写入本地数据库,如果本地数据库中已经存在,那么就不操作。

调用start函数启动节点后,首先创建启动网络模块,网络模块设置在这里启动是因为需要通过Account模块获取当前节点的id,而Account模块的加载也是个异步过程。

网络模块启动之后,会建立一个p2p网络,用于发送和接收消息,并每隔1s执行loop函数,loop函数会执行挖矿过程。这里设定一次只产生一个区块,不会并行对多个区块进行挖矿,所以在loop函数中先检测当前是否正在挖矿或者需要挖矿,如果当前节点空闲,那么就进行挖矿。

网络模块收到消息后会进行相应的处理,如果收到的是新区块,会先判断区块是否已经处理过,如果没有处理过,那么先验证其有效性,然后记录并添加到主链或者分叉;最后会将该消息广播到其他节点。

创建区块

BlockChain的一个重要功能就是创建区块,也就是所谓的挖矿。当然创建区块只是挖矿的一个部分,分为以下几个步骤:

  • 从交易池中加载交易
  • 创建区块实例
  • 工作量证明
  • 签名区块
  • 存入本地数据库
  • 广播区块
// blockchain.js

    generate_block(keypair, cb) {
        // load transactions
        var tx = [];
        // create block
        let block = new Block({
            "keypair": keypair,
            "previous_block": this.last_block_,
            "transactions": tx
        }, this.consensus_);
        // make proof of the block/mine
        let self = this;
        block.on('block completed', (data) => {
            if (data.height == self.last_block_.height + 1) {
                console.log("block completed");
                self.commit_block(data);

                self.broadcast(Msg.block(data));

                if (cb) cb();
            } else {
                // fork or store into tmp
                console.log('fork');
                // todo
                self.pending_block_[data.hash] = data;

            }
        });
    }

创建区块后,共识模块会进行挖矿操作,完成共识后会发射'consensus completed'信号,然后当make_proof函数监听到该信号后会对产生的区块进行签名,再发射'block completed'信号。回到generate_block函数,对产生的区块再次进行检验,如果产生的区块高度为主链高度加1,那么就加入主链,并广播出去,否则加入分叉。由于共识过程是一个异步过程,在此过程中可能会收到别的节点发来的区块,如果验证合格就可加入主链,这时候自己新产生的区块就会被加入分叉。

// block.js

    prepare_data() {
        let tx = "";
        for (var i = 0; i < this.transactions_.length; ++i) {
            tx += this.transactions_[i].toString('utf-8');
        }
        let data = this.version_.toString()
            + this.height_.toString()
            + this.previous_hash_
            + this.timestamp_.toString()
            + this.merkle_hash_
            + this.generator_publickey_
            + JSON.stringify(this.consensus_data_)
            + tx;

        return data;
    }
    // calc the hash of the block
    calc_block_hash() {
        return this.calc_hash(this.prepare_data());
    }
    sign(keypair) {
        var hash = this.calc_block_hash();
        return ed.Sign(Buffer.from(hash, 'utf-8'), keypair).toString('hex');
    }
    make_proof(consensus, keypair) {
        let self = this;
        this.on('consensus completed', () => {
            self.hash_ = self.calc_block_hash();
            self.block_signature_ = self.sign(keypair);
            self.emit('block completed', self.toObject());
        });

        consensus.make_consensus(this);
    }

    static verify_signature(block) {
        var hash = block.hash;
        var res = ed.Verify(Buffer.from(hash, 'utf8'), Buffer.from(block.block_signature, 'hex'), Buffer.from(block.generator_publickey, 'hex'));
        return res;

    }

验证区块

当节点接收到广播来的区块时,要对其合法性进行验证,验证通过后才能将其写入本地数据链。验证过程如下:

  • 验证区块签名
  • 验证共识过程
  • 验证打包的交易是否正确
// blockchain.js

    verify(block) {
        // verify the block signature
        if (!Block.verify_signature(block))
            return false;
        // verify consensus
        if (!this.consensus_.verify(block))
            return false;
        // verify transactions
        let tx = block.transactions;
        for (var i = 0; i < tx.length; ++i) {
            // todo (check tx is exist and valid)
            if (!Transaction.verify(tx[i]))
                return false;
        }
        return true;
    }

这里仅仅是对区块的打包信息进行验证,验证通过之后并不代表区块是合格的,还要根据其他规则来判断是加入主链还是分叉或者丢弃,这部分内容在后续部分会讲解。