区块链的目的之一是让我们所需要的“有价值”的信息得以保存且不可更改,这些信息都储存在一个叫做“区块(block)”的结构中。以比特币为例,被认为是有价值的信息是“交易”,所有的交易储存在区块中,通过区块的hash、时间戳等实现信息的可追溯以及不可更改性。
我们这里首先实现的是一个简易的区块链,并不是像比特币这样成熟的区块链,暂不涉及交易结构、验证以及UTXO。
首先,创建一个区块(Block)类。我们的区块仅包含了一部分关键信息,它的结构如下:
class Block{
constructor(data){
this.hash = "", // 当前区块的Hash值
this.height = 0, // 当前区块的高度
this.body = data, // 区块实际存储的有效信息
this.time = 0, // 时间戳,即区块创建的时间
this.previousBlockHash = ""。// 前一个区块的Hash值
}
}
这里,hash、height、time和previousBlockHash在比特币规范中属于区块头(header),区块头是一个单独的数据结构,而body部分用于储存交易信息或者其他数据。
首先,我们要把区块存在哪里?我们可以把区块直接放进一个数组(let blockchain = arr[]),但是这显然不是我们想要的。这里我们选择leveldb作为底层的数据储存方案。
const level = require('level');
const chainDB = './.data/blockchain';
const db = level(chainDB);
然后,我们要思考一下,一个blockchain实例需要具有哪些功能?
1、getBlockHeight():获取当前区块的高度,即该区块链的高度;
2、getBlockByHeight(height):通过区块高度来获取区块;
3、getBlockByHash(hash):通过区块哈希来获取区块;
4、validateBlock(height):验证某高度的区块是否有效;
5、validateChain():验证整条链的每个区块是否均有效;
通过上面几个函数,我们定义了对一个区块链(blockchain)的基本描述,我们通过当前区块的高度获取当前区块的hash和前一个区块的hash,进而可以向前回溯出整个区块链。
下面我们看看怎么实现?
首先,Blockchain要包括如下的方法:
class Blockchain {
addBlock();
getBlockHeight();
getBlockByHeight();
getBlockByHash();
validateBlock();
validateChain();
}
我们要保证系统中只有一个Blockchain实例,可以使用ES6语法class的static关键词来设定一个静态方法来储存Blockchain实例。在ES6的class中,加了static关键词的方法不会被实例所继承,只能通过class来调用,如Blockchain.getInstance()。
static getInstance() {
if (!Blockchain.instance) {
Blockchain.instance = new Blockchain();
return Blockchain.instance.getBlockHeight()
.then((height) => {
if (height === 0) {
const initialBlock = new Block('First block of the blockchain');
return Blockchain.instance.addBlock(initialBlock);
}
})
.then(() => Blockchain.instance)
}
return Promise.resolve(Blockchain.instance);
}
getBlockHeight()
通过递归调用count函数,从创始区块一直计数到最后一个区块,得到待增新区块的高度。
getBlockHeight() {
let count = function (key) {
return db.get(key)
.then(() => count(key + 1))
.catch(() => key);
};
return count(0);
}
getBlockByHeight()
getBlockByHeight(){
return db.get(height).then((value) => JSON.parse(value));
}
addBlock(newBlock)
新区块的previousBlockHash应等于前一个区块的Hash,这样就将两个区块链接到了一起。
addBlock(newBlock) {
return this.getBlockHeight()
.then((height) => {
let PrevBlock;
newBlock.height = height; // 新区块的高度
newBlock.time = new Date().getTime().toString().slice(0, -3); // 新区块时间戳
// 得到新区块的previousBlockHash
if (height > 0) {
PrevBlock = this.getBlock(height - 1)
.then((previousBlock) => {
newBlock.previousBlockHash = previousBlock.hash;
});
}
return Promise.all([PrevBlock])
.then(() => {
newBlock.hash = SHA256(JSON.stringify(newBlock)).toString();
return db.put(height, JSON.stringify(newBlock));
});
})
.then(() => Blockchain.instance);
}
getBlockByHash()
可以通过类似于getBlockHeight()的思路进行递归,比较该区块的hash于输入的参数hash是否相等。然而,我们还有另外一种选择。leveldb给我提供了db.createReadStream()方法来逐个读取数据库中的条目。
getBlockByHash(hash){
return new Promise((resolve, reject) => {
let block;
db.createReadStream()
.on("data", (data) => {
if(data.key != 'height'){
let value = JSON.parse(data.value);
let blockHash = value.hash;
if (blockHash == hash) {
block = value;
}
}
})
.on("error", (err) => {
reject(err);
})
.on("close", ()=>{
resolve(block); // 如果没有满足条件的,则block值为undefined,在下一步就会抛出错误
})
})
}
validateBlock(blockHeight)
验证一个区块,就是要重新生成该区块的hash,比较该hash与区块本身hash属性的值是否相等。
validateBlock(blockHeight){
return db.get(blockHeight).then(function(value){
let block = JSON.parse(value);
let blockHash = block.hash;
// remove block hash to test block integrity
block.hash = '';
// generate block hash
let validBlockHash = SHA256(JSON.stringify(block)).toString();
// Compare
if (blockHash === validBlockHash) {
return true
} else {
console.log('Block #'+blockHeight+' invalid hash:\n'+blockHash+'<>'+validBlockHash);
return false
}
}).catch(function(err){
console.log('Not found!', err);
})
}
validateChain()
验证整个链的所有区块,这就肯定要用到for循环。
validateChain(){
let errorLog = [];
this.getBlockHeight().then(height =>{
for (var i = 0; i <= height; i++) {
this.getBlock(i).then(block => {
// validate block
let h = block.height
this.validateBlock(h).then(val => {
if(!val) errorLog.push(h)
})
// compare blocks hash link
let hash = block.hash;
let n = block.height + 1;
if( n <= value ) {
db.get(n, (err, val) => {
let nextBlock = JSON.parse(val)
let preHash = nextBlock.previousBlockHash
if(hash !== preHash) {
errorLog.push(n-1);
}
if(n == value) {
if(errorLog.length>0){
console.log('\n-------Errors Detected!-------\n')
console.log('Block errors = ' + errorLog.length);
console.log('Blocks: '+ errorLog);
} else {
console.log('No errors detected');
}
}
})
}
})
}
}).catch(function(err){
console.log('Not found!', err);
})
}
通过上面简单的代码,我们实现了一个简单的区块链结构,这是一个private chain,没有p2p网络,没有挖矿需要的共识算法。我们关注的是基本的区块链的储存结构,即有效数据储存在block中,每一个block通过hash值关联成一条链,我们通过区块的高度即可从数据库leveldb中读取区块的数据。