介绍了一些关于比特币的概念与机制,为了加深理解,本文基于 JavaScript 来实现一个简单的区块链原型,后续再对其进行不断丰富。
1. 概述
如前所述区块链模型的组成部分,包括区块,区块构成的区块链,以及保存区块链的数据持久层等。一个超简单的 UML 类图如下:
由于我是前端的,业余看了这么久区块链的理论,还是手痒痒谢谢代码,把这个类用 JavaScript 实现一下。写完之后发现目前阶段,对于区块链原型来说还是太过简单,不过如果说用来做前端面试题,考察下面向对象和 Promise 等知识点倒是挺接洽。
2. 定义区块数据模型
摘取比特币区块的详情进行修改,去除所有多余信息,只留下能描述区块最基本的信息,声明区块类如下:
class Block {constructor(data) {
// 区块的属性值
this.hash = "";
this.height = 0;
this.body = data;
this.time = 0;
this.previousBlockHash = "";
}
}
module.exports.Block = Block;
3. 数据持久层
其实用数组实现区块链是最简单的原型方案,但每次重启数组都会被清空,数据并不持久。所以这里引入 levelDB
数据库作为持久层来保存数据,相关操作可参考 level。由于直接调用 API,对于应用层来说过于麻烦,所以在此声明一个数据操作类LevelSandbox
,该类不像传统的关系型数据库具有增、删、改、查等全部功能,由于区块链上数据的不可更改性,此类只包含增和查的操作。
3.1 根据 key 从数据库中获取数据
本文如下相关异步实现,都采用 Promise 的方式而非回调,其中好处作为前端工程师此处就不多介绍了,有需要了解的可异步 Promise 介绍,自行扩展阅读。
getLevelDBData(key) {
let self = this;
return new Promise(function(resolve, reject) {self.db.get(key)
.then(value => {console.log('Value =' + value);
resolve(value)
})
.catch(err => {console.log('Not found!');
reject(err)
})
});
}
3.2 将 key/value
数据插入数据库中
以 key/value
的方式在数据库中存储,其 key
值得选取,这里考虑使用区块类中声明的 height
字段,该字段标识一个区块在链中的位序,同时也具有唯一性,非常合适。
addLevelDBData(key, value) {
let self = this;
return new Promise(function(resolve, reject) {self.db.put(key, value)
.then(() => resolve())
.catch((err) => {console.log('Block' + key + 'submission failed');
reject(err)
})
});
}
3.3 获取数据库中区块总数
createReadStream()
方法创建一个读取数据库的流,这里的作用是为了遍历整库以获取存储的区块总数,另外此方法还可通过传参,设置遍历次序,详情可参阅文档。
getBlocksCount() {
let self = this;
return new Promise(function(resolve, reject){
let height = 0;
self.db.createReadStream()
.on('data', function () {height++;})
.on('error', function (error) {reject('Unable to read data stream!', error);
})
.on('close', function () {resolve(height);
});
});
}
4. 区块链类
该类主要负责将新创建的区块添加进区块链,并验证链中各个区块的数据完整性。这个过程中少不了对区块数据的哈希处理,为方便起见,采用第三方库 crypto-js 实现的 SHA256
方法。
构想该类中的主要方法包括:
- createGenesisBlock():生成起始区块
- getBlockHeight():获取区块链长度
- getBlock(height):获取指定区块
- addBlock(block):将一个新区块加入区块链中
- validateBlock(block):验证某个区块
- validateChain():验证区块链
如下便实现其中主要的几个方法:
4.1 增加新区块
各个区块通过 previousBlockHash
属性,依次指向前一个区块来连接成链的,除首区块该属性为空外。
addBlock(block) {return this.getBlockHeight()
.then(height => {
区块高度
block.height = height;
// UTC 时间戳
block.time = new Date().getTime().toString().slice(0, -3);
if (height > 0) {this.getBlock(height - 1)
.then(preBlock => {
// 前一个区块的哈希值
block.previousBlockHash = preBlock.hash;
// 对区块进行哈希处理
block.hash = SHA256(JSON.stringify(block)).toString();
// 将新区快存入库中
this.bd.addLevelDBData(height, JSON.stringify(block));
})
.catch(error => console.log(error));
} else {block.hash = SHA256(JSON.stringify(block)).toString();
this.bd.addLevelDBData(height, JSON.stringify(block));
}
})
.catch(error => console.log(error));
}
4.2 验证单个区块完整性
验证方法就是应用了 hash 算法的性质:相同的数据经过 hash 后会生成相同的 hash 值。
validateBlock(height) {
// 获取区块的值
return this.getBlock(height)
.then(block => {const objBlock = JSON.parse(block);
let blockHash = objBlock.hash;
objBlock.hash = '';
// 重新生成区块的哈希值
let validBlockHash = SHA256(JSON.stringify(objBlock)).toString();
objBlock.hash = blockHash;
// 比较以验证完整性
if (blockHash === validBlockHash) {return Promise.resolve({isValidBlock: true, block: objBlock});
} else {console.log('Block #'+blockHeight+'invalid hash:\n'+blockHash+'<>'+validBlockHash);
return Promise.resolve({isValidBlock: false, block: objBlock});
}
})
}
4.3 验证整个区块链
通过依次校验每个区块以验证整条链的完整性。
validateChain() {let errorLog = [];
let previousHash = '';
this.getBlockHeight()
.then(height => {for (let i = 0; i < height; i++) {this.getBlock(i)
.then(block => this.validateBlock(block.height))
.then(({isValidBlock, block}) => {if (!isValidBlock) errorLog.push(i);
if (block.previousBlockHash !== previousHash) errorLog.push(i);
previousHash = block.hash;
if (i === height - 1) {if (errorLog.length > 0) {console.log(`Block errors = ${errorLog.length}`)
console.log(`Blocks: ${errorLog}`)
} else {console.log('No errors detected')
}
}
})
}
})
}
5. 生成测试数据
(function theLoop (i) {setTimeout(function () {let blockTest = new Block.Block("Test Block -" + (i + 1));
myBlockChain.addBlock(blockTest).then((result) => {console.log(result);
i++;
if (i < 10) theLoop(i);
});
}, 10000);
})(0);
作为一个区块链原型的样子算是初见端倪,但就目前的功能来说还非常简陋,说是原型都算抬举了,不过后面慢慢再丰富吧。这里也只算是对之前的一个实践性的小节。
文中以列出主要代码片段,整体实现其实不难,没贴出所有代码主要是为了表述思路更清晰些,若有朋友实现过程中有问题,可文下留言交流。