原文:Writing a tiny blockchain in JavaScriptgit
做者:Savjee.begithub
译者:JeLewine数据库
几乎每一个人都据说过像比特币和以太币这样的加密货币,可是只有极少数人懂得隐藏在它们背后的技术。在这篇博客中,我将会用JavaScript来建立一个简单的区块链来演示它们的内部到底是如何工做的。我将会称之为SavjeeCoin!数组
全文分为三个部分:bash
区块链是由一个个任何人均可以访问的区块构成的公共数据库。这好像没什么特别的,不过它们有一个有趣的属性:它们是不可变的。一旦一个区块被添加到区块链中,除非让剩余的其他区块失效,不然它是不会再被改变的。markdown
这就是为何加密货币是基于区块链的缘由。你确定不但愿人们在交易完成后再变动交易!网络
区块链是由许许多多的区块连接在一块儿的(这听上去好像没毛病..)。链上的区块经过某种方式容许咱们检测到是否有人操纵了以前的任何区块。函数
那么咱们如何确保数据的完整性呢?每一个区块都包含一个基于其内容计算出来的hash。同时也包含了前一个区块的hash。oop
下面是一个区块类用JavaScript写出来大体的样子:性能
const SHA256 = require("crypto-js/sha256"); class Block { constructor(index, timestamp, data, previousHash = '') { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = this.calculateHash(); } calculateHash() { return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data)).toString(); } } 复制代码
由于JavaScript中并不支持sha256因此我引入了crypto-js库。而后我定义了一个构造函数来初始化我区块的属性。每个区块上都被赋予了index
属性来告知咱们这个区块在整个链上的位置。咱们同时也生成了一个时间戳,以及须要在区块里存储的一些数据。最后是前一个区块的hash。
如今咱们能够在Blockchain类中将区块连接起来了!下面是用JavaScript实现的代码:
class Blockchain{ constructor() { this.chain = [this.createGenesisBlock()]; } createGenesisBlock() { return new Block(0, "01/01/2017", "Genesis block", "0"); } 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; } } 复制代码
在构造函数里,我经过建立一个包含创世块的数组来初始化整个链。第一个区块是特殊的,由于它不能指向前一个区块。我还添加了下面两个方法:
getLatestBlock()
返回咱们区块链上最新的区块。addBlock()
负责将新的区块添加到咱们的链上。为此,咱们将前一个区块的hash添加到咱们新的区块中。这样咱们就能够保持整个链的完整性。由于只要咱们变动了最新区块的内容,咱们就须要从新计算它的hash。当计算完成后,我将把这个区块推动链里(一个数组)。最后,我建立一个isChainValid()
来确保没有人篡改过区块链。它会遍历全部的区块来检查每一个区块的hash是否正确。它会经过比较previousHash
来检查每一个区块是否指向正确的上一个区块。若是一切都没有问题它会返回true
不然会返回false
。
咱们的区块链类已经写完啦,能够真正的开始使用它了!
let savjeeCoin = new Blockchain(); savjeeCoin.addBlock(new Block(1, "20/07/2017", { amount: 4 })); savjeeCoin.addBlock(new Block(2, "20/07/2017", { amount: 8 })); 复制代码
在这里我仅仅是建立了一个区块链的实例,而且命名它为SavjeeCoin!以后我在链上添加了一些区块。区块里能够包含任何你想要放的数据,不过在上面的代码里,我选择添加了一个带有amount
属性的对象。
在介绍里我曾说过区块链是不可变的。一旦添加,区块就不可能再变动了。让咱们试一下!
// 检查是否有效(将会返回true) console.log('Blockchain valid? ' + savjeeCoin.isChainValid()); // 如今尝试操做变动数据 savjeeCoin.chain[1].data = { amount: 100 }; // 再次检查是否有效 (将会返回false) console.log("Blockchain valid? " + savjeeCoin.isChainValid()); 复制代码
我会在一开始经过运行isChainValid()
来验证整个链的完整性。咱们操做过任何区块,因此它会返回true。
以后我将链上的第一个(索引为1)区块的数据进行了变动。以后我再次检查整个链的完整性,发现它返回了false。咱们的整个链再也不有效了。
这个小栗子还远未达到完成的程度。它尚未实现POW(工做量证实机制)或P2P网络来与其它矿工来进行交流。
但他确实证实了区块链的工做原理。许多人认为原理会很是复杂,但这篇文章证实了区块链的基本概念是很是容易理解和实现的。
在part1中咱们用JavaScript建立了一个简单的区块链来演示区块链的工做原理。不过这个实现并不完整,不少人发现依旧能够篡改该系统。没错!咱们的区块链须要另外一种机制来抵御攻击。那么让咱们来看看咱们该如何作到这一点!
如今咱们能够很快的创造区块而后很是迅速的将它们添加进咱们的区块链中。不过这致使了三个问题:
显然咱们须要一个方案来解决这些问题:POW。
POW是在第一个区块链被创造以前就已经存在的一种机制。这是一项简单的技术,经过必定数量的计算来防止滥用。工做量是防止垃圾填充和篡改的关键。若是它须要大量的算力,那么填充垃圾就再也不值得。
比特币经过要求hash以特定0的数目来实现POW。这也被称之为难度
不过等一下!一个区块的hash怎么能够改变呢?在比特币的场景下,一个区块包含有各类金融交易信息。咱们确定不但愿为了获取正确的hash而混淆了那些数据。
为了解决这个问题,区块链添加了一个nonce
值。Nonce是用来查找一个有效Hash的次数。并且,由于没法预测hash函数的输出,所以在得到知足难度条件的hash以前,只能大量组合尝试。寻找到一个有效的hash(建立一个新的区块)在圈内称之为挖矿。
在比特币的场景下,POW确保每10分钟只能添加一个区块。你能够想象垃圾填充者须要多大的算力来创造一个新区块,他们很难欺骗网络,更不要说篡改整个链。
咱们该如何实现呢?咱们先来修改咱们区块类并在其构造函数中添加Nonce变量。我会初始化它并将其值设置为0。
constructor(index, timestamp, data, previousHash = '') { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = this.calculateHash(); this.nonce = 0; } 复制代码
咱们还须要一个新的方法来增长Nonce,直到咱们得到一个有效hash。强调一下,这是由难度决定的。因此咱们会收到做为参数的难度。
mineBlock(difficulty) { while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) { this.nonce++; this.hash = this.calculateHash(); } console.log("BLOCK MINED: " + this.hash); } 复制代码
最后,咱们还须要更改一下calculateHash()
函数。由于目前他尚未使用Nonce来计算hash。
calculateHash() { return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce ).toString(); } 复制代码
将它们结合在一块儿,你会获得以下所示的区块类:
class Block { constructor(index, timestamp, data, previousHash = '') { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = this.calculateHash(); this.nonce = 0; } calculateHash() { return SHA256(this.index + this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).toString(); } mineBlock(difficulty) { while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")) { this.nonce++; this.hash = this.calculateHash(); } console.log("BLOCK MINED: " + this.hash); } } 复制代码
如今,咱们的区块已经拥有Nonce而且能够被开采了,咱们还须要确保咱们的区块链支持这种新的行为。让咱们先在区块链中添加一个新的属性来跟踪整条链的难度。我会将它设置为2(这意味着区块的hash必须以2个0开头)。
constructor() { this.chain = [this.createGenesisBlock()]; this.difficulty = 2; } 复制代码
如今剩下要作的就是改变addBlock()
方法,以便在将其添加到链中以前确保实际挖到该区块。下面咱们将难度传给区块。
addBlock(newBlock) {
newBlock.previousHash = this.getLatestBlock().hash;
newBlock.mineBlock(this.difficulty);
this.chain.push(newBlock);
}
复制代码
大功告成!咱们的区块链如今拥有了POW来抵御攻击了。
如今让咱们来测试一下咱们的区块链,看看在POW下添加一个新区块会有什么效果。我将会使用以前的代码。咱们将建立一个新的区块链实例而后往里添加2个区块。
let savjeeCoin = new Blockchain(); console.log('Mining block 1'); savjeeCoin.addBlock(new Block(1, "20/07/2017", { amount: 4 })); console.log('Mining block 2'); savjeeCoin.addBlock(new Block(2, "20/07/2017", { amount: 8 })); 复制代码
若是你运行了上面的代码,你会发现添加新区块依旧很是快。这是由于目前的难度只有2(或者你的电脑性能很是好)。
若是你建立了一个难度为5的区块链实例,你会发现你的电脑会花费大概十秒钟来挖矿。随着难度的提高,你的防护攻击的保护程度越高。
就像以前说的:这毫不是一个完整的区块链。它仍然缺乏不少功能(像P2P网路)。这只是为了说明区块链的工做原理。
而且:因为单线程的缘由,用JavaScript来挖矿并不快。
在前面两部分咱们建立了一个简单的区块链,而且加入了POW来抵御攻击。然而咱们在途中也偷了懒:咱们的区块链只能在一个区块中存储一笔交易,并且矿工没有奖励。如今,让咱们解决这个问题!
如今一个区块拥有index
,previousHash
,timestamp
,data
,hash
和nonce
属性。这个index
属性并非颇有用,事实上我甚至不知道为何开始我要将它添加进去。因此我把它移除了,同时将data
更名为transactions
来更语义化。
class Block{ constructor(timestamp, transactions, previousHash = '') { this.previousHash = previousHash; this.timestamp = timestamp; this.transactions = transactions; this.hash = this.calculateHash(); this.nonce = 0; } } 复制代码
当咱们改变区块类时,咱们也必须更改calculateHash()
函数。如今它还在使用老旧的index
和data
属性。
calculateHash() { return SHA256(this.previousHash + this.timestamp + JSON.stringify(this.transactions) + this.nonce).toString(); } 复制代码
在区块内,咱们将能够存储多笔交易。所以咱们还须要定义一个交易类,一边咱们能够锁定交易应当具备的属性:
class Transaction{
constructor(fromAddress, toAddress, amount){
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.amount = amount;
}
}
复制代码
这个交易例子很是的简单,仅仅包含了发起方(fromAddress
)和接受方(toAddress
)以及数量。若是有需求,你也能够在里面加入更多字段,不过这个只是为了最小实现。
当前的最大任务:调整咱们的区块链来适应这些新变化。咱们须要作的第一件事就是存储待处理交易的地方。
正如你所知道的,因为POW,区块链能够稳定的建立区块。在比特币的场景下,难度被设置成大约每10分钟建立一个新区块。可是,是能够在创造两个区块之间提交新的交易。
为了作到这一点,首先须要改变咱们区块链的构造函数,以便他能够存储待处理的交易。咱们还将创造一个新的属性,用于定义矿工得到多少钱做为奖励:
class Blockchain{ constructor() { this.chain = [this.createGenesisBlock()]; this.difficulty = 5; // 在区块产生之间存储交易的地方 this.pendingTransactions = []; // 挖矿回报 this.miningReward = 100; } } 复制代码
下一步,咱们将调整咱们的addBlock()
方法。不过个人调整是指删掉并重写它!咱们将再也不容许人们直接为链上添加区块。相反,他们必须将交易添加至下一个区块中。并且咱们将addBlock()
改名为createTransaction()
,这看起来更语义化:
createTransaction(transaction) {
// 这里应该有一些校验!
// 推入待处理交易数组
this.pendingTransactions.push(transaction);
}
复制代码
人们如今能够将新的交易添加到待处理交易的列表中。但不管如何,咱们须要将他们清理掉并移入实际的区块中。为此,咱们来建立一个minePendingTransactions()
方法。这个方法不只会挖掘全部待交易的新区块,并且还会向采矿者发送奖励。
minePendingTransactions(miningRewardAddress) { // 用全部待交易来建立新的区块而且开挖.. let block = new Block(Date.now(), this.pendingTransactions); block.mineBlock(this.difficulty); // 将新挖的看矿加入到链上 this.chain.push(block); // 重置待处理交易列表而且发送奖励 this.pendingTransactions = [ new Transaction(null, miningRewardAddress, this.miningReward) ]; } 复制代码
请注意,该方法采用了参数miningRewardAddress
。若是你开始挖矿,你能够将你的钱包地址传递给此方法。一旦成功挖到矿,系统将建立一个新的交易来给你挖矿奖励(在这个栗子里是100枚币)。
有一点须要注意的是,在这个栗子中,咱们将全部待处理交易一并添加到一个区块中。但实际上,因为区块的大小是有限制的,因此这是行不通的。在比特币里,一个区块的大小大概是2Mb。若是有更多的交易可以挤进一个区块,那么矿工能够选择哪些交易达成哪些交易不达成(一般状况下费用更高的交易容易获胜)。
在测试咱们的代码钱让咱们再作一件事!若是可以检查咱们区块链上地址的余额将会更好。
getBalanceOfAddress(address){ let balance = 0; // you start at zero! // 遍历每一个区块以及每一个区块内的交易 for(const block of this.chain){ for(const trans of block.transactions){ // 若是地址是发起方 -> 减小余额 if(trans.fromAddress === address){ balance -= trans.amount; } // 若是地址是接收方 -> 增长余额 if(trans.toAddress === address){ balance += trans.amount; } } } return balance; } 复制代码
好吧,咱们已经完成并能够最终一切是否能够正常工做!为此,咱们建立了一些交易:
let savjeeCoin = new Blockchain(); console.log('Creating some transactions...'); savjeeCoin.createTransaction(new Transaction('address1', 'address2', 100)); savjeeCoin.createTransaction(new Transaction('address2', 'address1', 50)); 复制代码
这些交易目前都处于等待状态,为了让他们获得证明,咱们必须开始挖矿:
console.log('Starting the miner...'); savjeeCoin.minePendingTransactions('xaviers-address'); 复制代码
当咱们开始挖矿,咱们也会传递一个咱们想要得到挖矿奖励的地址。在这种状况下,个人地址是xaviers-address
(很是复杂!)。
以后,让咱们检查一下xaviers-address
的帐户余额:
console.log('Balance of Xaviers address is', savjeeCoin.getBalanceOfAddress('xaviers-address')); // 输出: 0 复制代码
个人帐户输出居然是0?!等等,为何?难道我不该该获得个人挖矿奖励么?那么,若是你仔细观察代码,你会看到系统会建立一个交易,而后将您的挖矿奖励添加为新的待处理交易。这笔交易将会包含在下一个区块中。因此若是咱们再次开始挖矿,咱们将收到咱们的100枚硬币奖励!
console.log('Starting the miner again!'); savjeeCoin.minePendingTransactions("xaviers-address"); console.log('Balance of Xaviers address is', savjeeCoin.getBalanceOfAddress('xaviers-address')); // 输出: 100 复制代码
如今咱们的区块链已经能够在一个区块上存储多笔交易,而且能够为矿工带来回报。
不过,仍是有一些不足:发送货币是,咱们不检查发起人是否有足够的余额来实际进行交易。然而,这实际上是一件容易解决的事情。咱们也没有建立一个新的钱包和签名交易(传统上用公钥/私钥加密完成)。
我想指出的是,这毫不是一个完整的区块链实现!它仍然缺乏不少功能。这只是为了验证一些概念来帮助您来了解区块链的工做原理。
该项目的源代码就放在个人GitHub