从本文开始,我们将开发一个区块链原型,并陆续实现各项功能,使用的语言为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_
分别代表当前节点的账户、共识方法和网络传输模块。其中keypair
和id
可以在创建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;
}
这里仅仅是对区块的打包信息进行验证,验证通过之后并不代表区块是合格的,还要根据其他规则来判断是加入主链还是分叉或者丢弃,这部分内容在后续部分会讲解。
网友评论