使用node.js来实现一个简单的区块链系统。
运行效果图后续会放上。
一、区块和区块链的创建
区块链是一串使用哈希指针链接起来的区块,区块是它的最基本单位,所以先写一个简单的区块。
1、创建区块
一个区块最少要包含下面的信息:
index:区块在区块链中的位置。
timestamp:区块产生的时间。
transactions:区块包含的交易。
previousHash:前一个区块的Hash值。
hash:当前区块的Hash值。
据此创建Block类:
const SHA256 = require('crypto-js/sha256');
class Block {
// 构造函数
constructor(index, timestamp) {
this.index = index;
this.timestamp = timestamp;
this.transactions = [];
this.previousHash = '';
this.hash = this.calculateHash();
}
// 计算区块的哈希值
calculateHash() {
return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();
}
// 添加新的交易到当前区块
addNewTransaction(sender, recipient, amount) {
this.Transactions.push({
sender,
recipient,
amount
})
}
// 查看当前区块里的交易信息
getTransactions() {
return this.transactions;
}
在上面的Block类的实现中,使用了crypto-js中的SHA256来作为区块的哈希算法,这和bitcoin的选择一致。transactions是一系列交易对象的列表,内容包括发送人、接收人和数额。
类中的三个方法,分别用于计算本区块的哈希值、增加新交易到当前区块、获取当前区块的所有交易。
有了区块,下一步应该将它组装成链。
2、创建区块链
一个区块链就是一个链表,链表上的每个元素都是一个区块。并且区块链需要一个创世区块来初始化,同时创世区块也是区块链的第一个区块。
代码如下:
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
}
// 创建创始区块
createGenesisBlock() {
const genesisBlock = new Block(0, "2019/11/18");
genesisBlock.previousHash = '0';
genesisBlock.transactions.push({
sender: 'Shi',
recipient: 'Niu',
amount: 100
});
return genesisBlock;
}
// 获取最新区块
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// 添加区块到区块链
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
// 验证当前区块链是否有效
isChainValid() {
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
// 验证当前区块的 hash 是否正确
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
// 验证当前区块的 previousHash 是否等于上一个区块的 hash
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
}
3、对区块链进行测试
一个最最基本的区块链系统有了,现在来测试一下它是否能够运行。
通过向区块链中添加区块,并尝试修改区块内容,来测试区块链不可篡改的特性。
先创建一个名字叫testCoin的区块链。
const testCoin = new Blockchain();
console.log(JSON.stringify(testCoin.chain, undefined, 2));
运行后发现链上只有一个创世区块。
新建两个区块,每个区块包含一笔交易,然后将这两个区块添加到testCoin链上。
let block1 = new Block('1', '2019/11/19');
block1.addNewTransaction('Wang', 'Niu', 200);
testCoin.addBlock(block1);
let block2 = new Block('2', '2019/11/20');
block2.addNewTransaction('Niu', 'Zhao', 500);
testCoin.addBlock(block2);
console.log(JSON.stringify(testCoin.chain, undefined, 2));
运行后发现两个区块已经正常添加到区块上了。
仔细观察结果,可以发现现在链上有三个区块,每个区块的previousHash指向前一个区块的Hash。
调用isChainValid方法(console.log(testCoin.isChainValid())
)验证区块有效性,发现此时返回true。
这个时候,我们来修改区块信息。在第一个区块(创世区块的index为0)中,Wang向Niu转账了200元,但是他又反悔了,他只想给100元,于是现在他修改交易信息:
block1.transactions[0].amount = 100;
console.log(block1.getTransactions())
控制台返回信息显示,信息已经修改成功了。但是Niu发现自己接收的数额不对,于是调用isChainValid方法来验证,发现返回结果是false,说明信息肯定被修改了。为什么返回false?很简单,因为通过哈希函数计算出的哈希值和区块内容是一一对应的,修改后计算出的哈希值和原本的哈希值不同,所以返回false。那么如果Wang修改数额,并且修改hash值呢?依然是false,因为下一个区块存储了上一个区块的哈希值。那么干脆点,Wang把下一个区块的previousHash也修改不就行了吗?现实中这种事情是不可能发生的,就是因为POW的存在。
4、工作量证明(POW)
比特币系统中,中本聪设计了一个工作量证明的机制,解决了系统里的经济激励和双重支付问题(这两个问题可以百度百科或者维基百科),下面我们看看工作量证明的原理和实现:
1.工作量证明算法
一个健康的区块链系统随时都会产生交易,以比特币为例,比特币每隔十分钟把这段时间产生的交易打包到一个区块,然后经过共识添加到现有区块。那么如何达成共识,确定到底是哪个节点来完成打包呢(获得记账权,并且得到coinbase激励)?这就是工作量证明算法解决的问题。
一个健康运行的区块链系统随时会产生交易,我们需要由服务器进行一下工作:定时把一个时间段(比特币是十分钟)的交易打包到一个区块,并且添加到现有的区块链中。但是一个区块链系统可能有很多个服务器(节点),究竟是以哪台服务器打包的区块为准呢?为了解决这个问题,比特币采用了一种叫做工作量证明的算法来决定采用哪一台服务器打包的区块并且给予奖励。
工作量证明算法可以简单的描述为:在一个时间段内同时有多台服务器对这一时间段内的交易进行打包,打包完成后连带区块Header信息一起经过SHA256算法进行哈希运算(这里面还有一个默克树结构,有兴趣可以了解),区块结构如图:
在区块头一级奖励交易coinbase里各有一个变量nonce,如果运算的结果不符合难度值(稍后解释),那么就调整nonce值并继续运算区块hash值。如果有某台服务器率先计算出了符合难度值的区块,那么它可以广播这个区块。其他服务器验证没问题后就可以添加到现有区块链上,然后大家再一起竞争下一个区块。这个过程也称为挖矿。
比特币中的工作量证明采用了基于secp256k1椭圆曲线的ECDSA算法的哈希算法SHA256,具体可以见我另一篇笔记。这种算法的特点是难以通过运算得到特定的结果,但是一旦计算出来合适的结果后则很容易验证,也就是说寻找和验证的难度是不对称的。在比特币系统中,找到一个符合要求难度的nonce值的期望时间为10分钟,而验证它是否正确却只要一瞬间。
上面阐述了什么是工作量证明的原理,那么如何将它转化为代码形式呢?
任何一个数据经过SHA256运算后都会的得到一个长度为256位的二进制数值,我们可以通过调整最开始的部分连续0的个数作为“难度值”。比如我们要求最后的区块经过SHA256运算后第一位为0,那么平均每两次运算就会得到一个这样的结果。但是如果我们要求连续10位都是0,那么就需要平均计算次才能够得到一个这样的结果。系统通过调整计算结果里连续0的个数来达成调整难度的目标。这也是为什么从2009年到2019年的十年间,算力遵循摩尔定律提升如此迅速,而比特币区块的挖矿时间依然可以维持在10分钟的原因。
2.代码实现
我们在区块的头信息中添加一个nonce,通过不停调节nonce值来寻找符合要求的哈希值,知道结果满足要求。
区块的代码:
class Block {
// 构造函数
constructor(index, timestamp) {
this.index = index;
this.timestamp = timestamp;
this.transactions = [];
this.previousHash = '';
this.hash = this.calculateHash();
this.nonce = 0;
}
// 计算区块的哈希值
calculateHash() {
return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString();
}
// 查看当前区块里的交易信息
getTransactions() {
return this.transactions;
}
// 挖矿
mineBlock(difficulty) {
console.log(`Mining block ${this.index}`);
while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) {
this.nonce++;
this.hash = this.calculateHash();
}
console.log("BLOCK MINED: " + this.hash);
}
}
不难发现,区块类中多了一个nonce属性,多了一个mineBlock方法,mineBlock方法就是我们说的挖矿,它负责寻找到指定难度的nonce值,找到了这个nonce值才可以提交区块。其中,difficulty指的是结果里从开头连续为0的个数。如果计算出来的哈希值不符合要求,那么nonce加1,然后重新计算区块的哈希值。可以看到这完全是暴力破解的方式,所以我们说POW的一大缺点就是严重浪费算力。
下面我们来看Blockchain类的代码:
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
this.difficulty = 3;
this.currentTransactions = [];
}
// 添加新的交易到当前区块
addNewTransaction(sender, recipient, amount) {
this.currentTransactions.push({
sender,
recipient,
amount
})
}
// 创建创始区块
createGenesisBlock() {
const genesisBlock = new Block(0, "01/10/2017");
genesisBlock.previousHash = '0';
genesisBlock.transactions.push({
sender: 'Leo',
recipient: 'Janice',
amount: 520
});
return genesisBlock;
}
// 获取最新区块
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// 添加区块到区块链
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
// 验证当前区块链是否有效
isChainValid() {
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
// 验证当前区块的 hash 是否正确
if (currentBlock.hash !== currentBlock.calculateHash()) {
return false;
}
// 验证当前区块的 previousHash 是否等于上一个区块的 hash
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
}
改动主要集中的关于挖矿、添加区块的代码中。
3.测试
添加一个区块:
const testCoin = =new Blockchain();
let block1 = new Block('1', '2019/11/19');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);
console.log(block1);
结果展示:
图
可以看到其中nonce值和hash值相对应。nonce越大表明运算越困难,算力浪费越大。我们设置的难度值为2,期望计算的次数是次(hash中一个字符代表4位)。
5、提供和区块进行交互的API
1.挖矿奖励
上面介绍了挖矿的原理并且实现了POW,可是服务器为什么愿意贡献自己的算力资源去挖矿呢?答案就是挖矿时有一个奖励机制。矿工在打包一个时间段的交易后,会在区块的第一笔交易的位置创建一笔新的交易。这笔交易没有发送人,接收人可以设置为任何人(应该没有矿工不设置为自己吧?),奖励的数额是多少呢?比特币初试阶段的奖励为50BTC(bitcoin),目前已经减少两次至12.5BTC。这笔奖励交易就是coinbase,由系统保证有效,并且可以通过任何一个其他节点的验证。
这里面有一个问题:奖励金额如果矿工随意改动怎么办?矿工可以这么干,但是不符合系统规定数额的coinbase无法通过其他节点验证,将丢弃该区块,最终无法通过共识的区块不会被添加到链上。