本文通过JavaScript代码逐步演示如何构建一个简易区块链。内容涵盖区块与链的基础结构、哈希计算、工作量证明(POW)机制的引入以解决双花和垃圾数据问题,以及交易处理与挖矿奖励的实现。旨在帮助开发者直观理解区块链不可篡改性和去中心化记账的核心逻辑。
区块链基础与SavjeeCoin构建
比特币和以太坊等加密货币广为人知,但其底层技术常被忽视。本文将使用JavaScript构建一个名为SavjeeCoin的简易区块链,以直观演示其内部运作机制。
文章分为三个部分:
- 第一部分:实现基础区块链结构
- 第二部分:引入工作量证明(POW)
- 第三部分:处理交易与挖矿奖励
第一部分:实现基础区块链
区块链概述
区块链是一个由区块组成的公共数据库,任何人都可访问。其核心特性在于不可变性:一旦区块被添加,除非使后续所有区块失效,否则无法修改。
这正是加密货币依赖区块链的原因,确保交易完成后数据不被篡改。
创建区块
区块链由多个区块链接而成。通过特定机制,可以检测是否有人操纵了历史区块。
为确保数据完整性,每个区块包含基于其内容计算的哈希值,以及前一个区块的哈希值。
以下是用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表示区块在链中的位置,timestamp为时间戳,data存储数据,previousHash指向前一区块的哈希。
创建区块链
接下来在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()负责将新区块加入链。新区块包含前一区块的哈希,从而保持链的完整性。若修改最新区块内容,需重新计算其哈希,随后将新区块推入数组。
最后,创建isChainValid() 以验证区块链未被篡改。该方法遍历所有区块,检查每个区块的哈希是否正确,并通过比较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网络以与其他矿工交互。
但它证明了区块链的基本原理。许多人认为原理复杂,本文展示了基本概念易于理解和实现。
工作量证明(POW)机制实现
第二部分:实现POW(工作量证明)
在part1中我们用JavaScript创建了一个简单的区块链来演示区块链的工作原理。不过这个实现并不完整,很多人发现依旧可以篡改该系统。没错!我们的区块链需要另一种机制来抵御攻击。那么让我们来看看我们该如何做到这一点!
问题所在
当前创建和添加区块的速度过快,导致三个问题:
- 第一:人们可快速创建大量区块,塞满垃圾数据,导致区块链过载且无法使用。
- 第二:由于创建有效区块过于容易,攻击者可篡改链中某一区块,并重新计算后续所有区块的哈希。即使篡改,他们仍能以有效区块结束链。
- 第三:结合上述漏洞,攻击者可有效控制区块链。区块链由P2P网络驱动,节点将区块添加到最长链。攻击者篡改区块后计算所有其他区块,并添加任意数量的新区块,最终得到最长链,其他节点接受并继续添加。
显然需要一种解决方案:POW。
什么是POW
POW在首个区块链诞生前已存在。这是一种通过计算量防止滥用的简单技术。工作量是防止垃圾填充和篡改的关键。若需要大量算力,填充垃圾便不再划算。
比特币要求哈希以特定数量的0开头,这被称为难度。
但区块哈希如何改变?在比特币场景中,区块包含金融交易信息,我们不愿为获取正确哈希而混淆数据。
为此,区块链引入nonce值。Nonce用于寻找有效哈希的尝试次数。由于哈希函数输出不可预测,在获得满足难度条件的哈希前,只能大量组合尝试。寻找有效哈希(创建新区块)的过程称为挖矿。
在比特币场景中,POW确保每10分钟添加一个区块。想象垃圾填充者需要多大算力才能创造新区块,他们很难欺骗网络,更不用说篡改整个链。
实现POW
如何实施?首先修改区块类,在构造函数中添加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,直到获得有效哈希。这由难度决定,因此接收难度作为参数。
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计算哈希。
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(意味着区块哈希以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
总结
以上所述是小编给大家介绍的JavaScript实现区块链,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!