也许你已经读过许多区块链的资料,浏览过很多项目的开发文档,却依然对区块链的运行原理感觉云里雾里,无法进行区块链的相关开发。伟大前辈陆游教导我们“纸上得来终觉浅,绝知此事要躬行”,自己动手从头构建一个区块链系统是探索区块链运行原理的非常好的途径。
这次《从零开始构建一个区块链》将会有一系列文章,从一开始构建一个最简单的可运行的区块链,到后面逐步加入POW/共识/API/UTXO…等等许多比特币中已有的特性。 预计节奏是每周至少更新一篇,总共应该会有十篇左右。
本系列中所有代码均采用JavaScript语言实现,项目地址见:liangpeili/testcoin
一、区块(Block)
区块是构建区块链的基本单位。一个区块至少要包含以下信息:
- index: 区块在区块链中的位置;
- timestamp: 区块产生的时间;
- transactions: 区块包含的交易;
- previousHash: 前一个区块的Hash值;
- hash: 当前区块的Hash值;
其中最后两个属性 previousHash 和 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)).toString();
}
addNewTransaction(sender, recipient, amount) {
this.transactions.push({
sender,
recipient,
amount
})
}
getTransactions() {
return this.transactions;
}
}
在上面的 Block 类的实现中,我们实用了crypto-js里的SHA256来作为区块的 Hash 算法,这也是比特币中实用的算法。transactions为一系列交易对象的列表。每笔交易的格式为:
{
sender: sender,
recipient: recipient,
amount: amount
}
calculateHash方法根据当前区块的信息计算出Hash值。
二、 区块链(Blockchain)
区块构建完成后,下一步就是如何把区块组装成一个区块链了。我们可以这样设计:一个区块链就是一个列表,列表中的每个元素都是一个区块。区块链需要一个创世区块(Genesis Block),它是区块链的第一个区块,需要手工生成。
区块链可以通过以下代码实现:
class Blockchain {
constructor() {
this.chain = [this.createGenesisBlock()];
}
createGenesisBlock() {
const genesisBlock = new Block(0, "01/10/2017");
genesisBlock.previousHash = '0';
genesisBlock.addNewTransaction('Leo', 'Janice', 520);
return genesisBlock;
}
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.hash = newBlock.calculateHash();
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];
if(currentBlock.hash !== currentBlock.calculateHash()){
return false;
}
if(currentBlock.previousHash !== previousBlock.hash){
return false;
}
}
return true;
}
}
在Blockchain 这个类中,我们实现了一个创建创世区块的方法。在创世区块中,由于它并没有前一个区块,因此previousHash设置为0。而假定这一天是Leo和Janice的结婚纪念日,Leo给Janice发了一个520元的红包,产生了一笔交易并记录到创世区块中。最后我们把这个创世区块添加到构造函数中,这样区块链就包含一个创世区块了。getLatestBlock和addBlock含义比较明显,这里不再解释。最后一个isChainValid方法用于检测区块链是否有效。如果已经添加到区块链的区块被篡改,那么该方法返回为false。我们会在下一部分试验这个场景。
三、测试区块链
现在我们已经完成一个最简单的区块链啦!是不是很不可思议?在这一部分,我们会创建一个区块链,向该区块链中添加两个完整的区块,并通过修改区块的信息来展示区块链的不可篡改的特性。Here we go!
我们将要创建一个名字叫做testCoin的区块链。首先使用Blockchain类新建一个对象,此时它应该只包含创世区块。
const testCoin = new Blockchain();
console.log(JSON.stringify(testCoin.chain, undefined, 2));
运行该程序,结果为:
[
{
"index": 0,
"timestamp": "01/10/2017",
"transactions": [
{
"sender": "Leo",
"recipient": "Janice",
"amount": "520"
}
],
"previousHash": "0",
"hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
}
]
我们新建两个区块,依此添加到该区块链中。
block1 = new Block('1', '02/10/2017');
block1.addNewTransaction('Alice', 'Bob', 500);
testCoin.addBlock(block1);
block2 = new Block('2', '03/10/2017');
block2.addNewTransaction('Jack', 'David', 1000);
testCoin.addBlock(block2);
console.log(JSON.stringify(testCoin.chain, undefined, 2));
可以得到以下结果:
[
{
"index": 0,
"timestamp": "01/10/2017",
"transactions": [
{
"sender": "Leo",
"recipient": "Janice",
"amount": 520
}
],
"previousHash": "0",
"hash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e"
},
{
"index": "1",
"timestamp": "02/10/2017",
"transactions": [
{
"sender": "Alice",
"recipient": "Bob",
"amount": 500
}
],
"previousHash": "23975e8996cd37311c7fd0907f9b2511c3bf23cf9c9147cca329dec76d7b544e",
"hash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1"
},
{
"index": "2",
"timestamp": "03/10/2017",
"transactions": [
{
"sender": "Jack",
"recipient": "David",
"amount": 1000
}
],
"previousHash": "32b96fa0bba9a7353e67498d822fb0c1f89c307098295c288459cb44dbc5d0f1",
"hash": "3a0b9a0471bb474f7560968f2f05ff93306cfc26be7f854a36dc4fea92018db2"
}
]
testCoin现在包含三个区块,除了一个创世区块以外,剩下的两个区块是我们刚刚添加的。注意每一个区块的previousHash属性是否正确的指向了前一个区块的Hash值。
此时我们使用isChainValid方法可以验证该区块链的有效性。
console.log(testCoin.isChainValid())的返回结果为true。
区块链的防篡改性体现在哪里呢?我们先来修改第一个区块的交易。在一个区块中,Alice向Bob转账500元,假设Alice后悔了,她只想付100元给Bob,于是修改交易信息:
block1.transactions[0].amount = 100;
console.log(block1.getTransactions())
Alice查看区块链的交易信息,发现已经改成了100元,心满意得的走了。
Bob看到后,发现交易遭到了篡改,于是开始收集证据,他怎么证明block1的那笔交易不是最初的交易呢?首先,Bob可以调用isChainValid方法,来证明目前的testCoin是无效的因为testCoin.isChainValid()返回值为false。但是testCoin.isChainValid()为什么会返回false呢?这就要从它的实现方式上来看了:首先Alice修改了交易的内容,这个时候block1的Hash值肯定和通过之前交易计算出的Hash值是不同的。这两个值的不同会触发isChainValid返回为false,也就是
if(currentBlock.hash !== currentBlock.calculateHash()){
return false;
}
这部分代码实现的功能。
那既然如此,Alice在修改交易内容的同时修改block1的hash不就可以了吗?于是Alice多了一个步骤:
block1.transactions[0].amount = 100;
block1.hash = block1.calculateHash();
console.log(testCoin.isChainValid())
怎么最后还是返回false?还是没有通过验证?原因是因为这段代码:
if(currentBlock.previousHash !== previousBlock.hash){
return false;
}
每一个区块都存储了上一个区块的Hash值,只修改一个区块是不够的,还需要修改下一个区块存储的previousHash。如果我们事先安全存储了block2的hash值,那无论如何Alice都是不可能在不被发现的情况下篡改已有数据的。在真实的区块链项目中,修改一个区块必须修改接下来该区块之后的所有区块,这也是无法办到的事情。区块链的这个“哈希指针”的特性,保证了区块链数据的不可篡改性。
一个简单的区块链完成了,如何把工作量证明(Proof-Of-Work)添加进去呢? 敬请关注下一篇更新。
参考:
Writing a tiny blockchain in JavaScript
https://hackernoon.com/learn-blockchains-by-building-one-117428612f46