本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
大厂技术 坚持周更 精选好文
本文为纯粹区块链技术分享,没有任何投资建议。希望大家喜欢~
一、故事导读
开始分享之前,引用自网上一个段子来引导大家。
《小明的故事》
小明是谁?小明是一名前端工程师,也是一个足球迷。
他有一项神奇的技能:他对足球有很深的理解,能够在每届世界杯开赛之前准确预测出最终夺冠的球队
比如,在 2010 年的那届世界杯,小明就预测出了正确的结果。大赛闭幕,小明难掩兴奋之情,想在女朋友面前显摆一下。
女朋友很自然地提出质疑,而小明并没有证据证明自己,只能哑口无言。
小明痛定思痛,决定写一个网站来提前记录自己的预言。
小明自己设计了网页界面。
找小伙伴帮忙写了一个后端服务,提供两个接口。
小明基于这两个接口,写了一个纯前端渲染的网站。
最终网站看起来是这个样子的:
接下来,小明静静等待下一届世界杯的到来。
时间过得很快,转眼到了 2014 年。这一次,小明再次正确预测出了冠军得主。
有网站记录预言,小明心想,这次女朋友应该会相信自己了吧!
然而……
女朋友也是懂技术的,她这次仍然提出了一个合理的质疑。小明再次无言以对。
那么问题来了,该怎么办能够让女朋友相信自己呢?
如果现在还有没结论,可以继续向下看。
二、基础概念
区块链技术中有很多新的概念,对于一些并不深入这个领域的同学来说,相对不是很友好。本文先对一些技术的概念进行讲解。作为前置的知识。
区块链的概念
特殊的分布式数据库。
一种链表结构,链表中元素作为一个区块。而每个链表的结构包括:
timestamp: 区块产生时间戳
nonce: 与区块头的 hash 值共同证明计算量(工作量)
data: 区块链上存储的数据
previousHash: 上一个区块的 hash
hash: 本区块链的 hash,由上述几个属性进行哈希计算而得
暂时无法在飞书文档外展示此内容
一些特点
- 去中心化存储
分布式数据库很早之前就已经出现,但与之不同的是区块链是一个没有管理者的、无中心化的分布式数据库。其起初的设计目标就是防止出现位于中心地位的管理者当局。
那么下一个问题就来了,如果没有一个管理者进行数据的管理,如何保证这个分布式数据库中的数据是可信任的呢?这就要提到下一个不可修改的特性了。
- 不可篡改
区块链上的数据是不可篡改的,大家都这样说。但其实,数据是可以改的,只是说改了以后就你自己认,而且被修改数据所在区块之后的所有区块都会失效。区块链网络有一个同步逻辑,整个区块链网络总是保持所有节点使用最长的链,那么你修改完之后,一联网同步,修改的东西又会被覆盖。这是不可篡改的一个方面。
更有意思的是,区块链通过加密校验,保证了数据存取需要经过严格的验证,而这些验证几乎又是不可伪造的,所以也很难篡改。加密并不代表不可篡改,但不可篡改是通过加密以及经济学原理搭配实现的。这还有点玄学的味道,一个纯技术实现的东西,还要靠理论来维持。但事实就是这样。这就是传说中的挖矿。
挖矿过程其实是矿工争取创建一个区块的过程,一旦挖到矿,也就代表这个矿工有资格创建新区块。怎么算挖到矿呢?通过一系列复杂的加密算法,从 0 开始到∞,找到一个满足难度的 hash 值,得到这个值,就是挖到矿。这个算法过程被称为 “共识机制”,也就是通过什么形式来决定谁拥有记账权,共识机制有很多种,区块链采用哪种共识机制最佳,完全是由区块链的实际目的结合经济学道理来选择。
除了这些,区块链里面的加密比比皆是,这些加密规则和算法,使得整个区块链遵循一种规律,让篡改数据的成本特别高,以至于参与的人对篡改数据都没有兴趣,甚至忌惮。这又是玄学的地方。
针对这些不可篡改的特性,我们是不是能够解决一开始提出的问题呢。
用 js 来写一段区块链的代码,来解决小明的困惑。
三、【实战】用 JavaScript 来写一个基本的区块链 demo。
实现一个基本的区块链
- 创建区块
区块链是由许许多多的区块链接在一起的(这听上去好像没毛病..)。链上的区块通过某种方式允许我们检测到是否有人操纵了之前的任何区块。
那么我们如何确保数据的完整性呢?每个区块都包含一个基于其内容计算出来的 hash。同时也包含了前一个区块的 hash。
下面是一个区块类用 JavaScript 写出来大致的样子:采用构造函数初始化区块的属性。
在这里的哈希值是无法修改的。我们能够看到,哈希值是由多个元素组成的,一旦一个哈希值受到了修改,意味着previousHash被修改了,这个时候如果想要继续修改就要对下一个区块进行操作,否则修改的区块就不具有意义了。而哈希值的计算非常耗时,同时修改 51% 以上的节点基本不可能,所以,这种联动机制也就保证了其不可修改的特性。
const crypto = require('crypto');class Block { constructor(previousHash, timestamp, data) { this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.nonce = 0; this.hash = this.calculateHash(); } // 计算区块的哈希值 calculateHash() { return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex'); }}- 创建链
我们通过创建包含创世区块的数组来初始化整个链。这样一来,第一个区块是特殊的,因为他并没有指向前一个区块。并且添加了两个方法:
getLatestBlock()返回我们区块链上最新的区块。addBlock()负责将新的区块添加到我们的链上。为此,我们将前一个区块的 hash 添加到我们新的区块中。这样我们就可以保持整个链的完整性。因为只要我们变更了最新区块的内容,我们就需要重新计算它的 hash。当计算完成后,我将把这个区块推进链里(一个数组)。
最后,我创建一个isChainValid()来确保没有人篡改过区块链。它会遍历所有的区块来检查每个区块的 hash 是否正确。它会通过比较previousHash来检查每个区块是否指向正确的上一个区块。如果一切都没有问题它会返回true否则会返回false。
class Blockchain{ constructor() { this.chain = [this.createGenesisBlock()]; } // 创建当前时间下的区块(创世块) createGenesisBlock() { return new Block(0, "20/05/2022", "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); } // 验证区块链是否被篡改。 // 遍历每个区块的hash值是否正确&&每个区块的指向previousHash是否正确。 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; }}- 使用
我们的区块链类已经写完啦,可以真正的开始使用它了!
这里,我们创建了一个区块链的实例,并在其中添加区块。其中的数据就写成了小明对于世界杯冠军的预言。
let firstClain = new Blockchain();firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));// 检查是否有效(将会返回true)console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);// 现在尝试操作变更数据firstClain.chain[1].data = { champion: 'korea' }; // 再次检查是否有效 (将会返回false)console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);- 尝试修改数据
我会在一开始通过运行isChainValid()来验证整个链的完整性。我们操作过任何区块,所以它会返回 true。
之后我将链上的第一个(索引为 1)区块的数据进行了变更。之后我再次检查整个链的完整性,发现它返回了 false。我们的整个链不再有效了。
// 检查是否有效(将会返回true)console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);// 现在尝试操作变更数据firstClain.chain[1].data = { champion: 'korea' }; // 再次检查是否有效 (将会返回false)console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);POW(proof-of-work)工作量证明
POW 是在第一个区块链被创造之前就已经存在的一种机制。这是一项简单的技术,通过一定数量的计算来防止滥用。工作量是防止垃圾填充和篡改的关键。如果它需要大量的算力,那么填充垃圾就不再值得。
比特币通过要求 hash 以特定 0 的数目来实现 POW。这也被称之为难度
不过等一下!一个区块的 hash 怎么可以改变呢?在比特币的场景下,一个区块包含有各种金融交易信息。我们肯定不希望为了获取正确的 hash 而混淆了那些数据。
为了解决这个问题,区块链添加了一个nonce值。Nonce 是用来查找一个有效 Hash 的次数。而且,因为无法预测 hash 函数的输出,因此在获得满足难度条件的 hash 之前,只能大量组合尝试。寻找到一个有效的 hash(创建一个新的区块)在圈内称之为挖矿。
在比特币的场景下,POW 确保每 10 分钟只能添加一个区块。你可以想象垃圾填充者需要多大的算力来创造一个新区块,他们很难欺骗网络,更不要说篡改整个链。
暂时无法在飞书文档外展示此内容
我们该如何实现呢?我们先来修改我们区块类并在其构造函数中添加 Nonce 变量。我会初始化它并将其值设置为 0。
constructor(previousHash, timestamp, data) {
this.previousHash = previousHash;
this.timestamp = timestamp;
this.data = data;
// 工作量
this.nonce = 0;
this.hash = this.calculateHash();
}我们还需要一个新的方法来增加 Nonce,直到我们获得一个有效 hash。强调一下,这是由难度决定的。所以我们会收到作为参数的难度。
// 工作量计算 mineBlock(difficulty) { while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) { this.nonce++; this.hash = this.calculateHash(); }最后,我们还需要更改一下calculateHash()函数。因为目前他还没有使用 Nonce 来计算 hash。
// 计算区块的哈希值 calculateHash() { return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex'); }将它们结合在一起,你会得到如下所示的区块类:
class Block { constructor(previousHash, timestamp, data) { this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; // 工作量 this.nonce = 0; this.hash = this.calculateHash(); } // 计算区块的哈希值 calculateHash() { return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex'); } // 工作量计算 mineBlock(difficulty) { while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) { this.nonce++; this.hash = this.calculateHash(); } }}修改区块链
现在,我们的区块已经拥有 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 firstClain = new Blockchain();firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));// 检查是否有效(将会返回true)console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);// 现在尝试操作变更数据firstClain.chain[1].data = { champion: 'korea' }; // 再次检查是否有效 (将会返回false)console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);如果你运行了上面的代码,你会发现添加新区块依旧非常快。这是因为目前的难度只有 2(或者你的电脑性能非常好)。
如果你创建了一个难度为 5 的区块链实例,你会发现你的电脑会花费大概十秒钟来挖矿。随着难度的提升,你的防御攻击的保护程度越高。
实际的难度系数与 hash 值
上面计算 hash 的过程其实就是一个简略版本的挖矿过程,也就是计算机来计算出一个相应的 hash 值,但就像上面的所提及的并不是所有的 hash 都能够满足,这个条件比较苛刻,使得绝大多数的 hash 都不能够满足要求,需要重新计算。
在区块链的协议中,有一个标准的常量和一个目标值。只有小于目标值的 hash 才可以被使用。用常量除以难度系数,可以得到目标值,显然,难度系数越大,目标值越小。
target = const / diffculty否则,hash 无效只能重新计算,而 nonce 的大小就计算了相应的工作量证明。
整体代码贴在下方
const crypto = require('crypto');class Block { constructor(previousHash, timestamp, data) { this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; // 工作量 this.nonce = 0; this.hash = this.calculateHash(); } // 计算区块的哈希值 calculateHash() { return crypto.createHash('sha256').update(this.previousHash + this.timestamp + JSON.stringify(this.data) + this.nonce).digest('hex'); } // 工作量计算 mineBlock(difficulty) { while (this.hash.substring(0, difficulty) !== Array(difficulty + 1).join('0')) { this.nonce++; this.hash = this.calculateHash(); } }}class Blockchain{ constructor() { this.chain = [this.createGenesisBlock()]; this.difficulty = 5; } // 创建当前时间下的区块(创世块) createGenesisBlock() { return new Block(0, "20/05/2022", "Genesis block", "0"); } // 获得区块链上最新的区块 getLatestBlock() { return this.chain[this.chain.length - 1]; } // 将新的区块添加到链上 addBlock(newBlock) { newBlock.previousHash = this.getLatestBlock().hash; newBlock.mineBlock(this.difficulty); this.chain.push(newBlock); } // 验证区块链是否被篡改。 // 遍历每个区块的hash值是否正确&&每个区块的指向previousHash是否正确。 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; }} module.exports.Blockchain = Blockchain;module.exports.Block = Block;const { Block, Blockchain } = require('./block-chain');let firstClain = new Blockchain();firstClain.addBlock(new Block(0, "21/05/2022", { champion: 'Spain'}));firstClain.addBlock(new Block(1, "22/05/2022", { champion: 'China'}));// 检查是否有效(将会返回true)console.log('firstClain valid? ' + firstClain.isChainValid(), firstClain.chain);// 现在尝试操作变更数据firstClain.chain[1].data = { champion: 'korea' }; // 再次检查是否有效 (将会返回false)console.log("firstClain valid? " + firstClain.isChainValid(), firstClain.chain);四、总结
回到一开始的问题.
小明用 js 用区块链的形式在世界本的开始之前把预测的内容存储在了这里。并且成功预测.
这一次,终于没有之一,成功的在女朋友面前秀了一把。
本文从一个小故事引出区块链的相关内容,其作为一门新的技术和思路,提供了一些不可篡改,分布式数据库的观念,并用前端的 js 代码来写了一个小的 demo。
当然其作为一种无人管理的不可随意篡改的分布式数据库确实没有很大的问题,但也有一些弊端,首先是链表的结构与 hash 值计算的困难导致其写入是数据的效率并不高,需要一定的时间才能保证所有的节点同步。第二、区块的计算所需要的一些无意义的计算,也是较为消耗能源的。
最后本文作为纯技术分享,无任何投资建议。希望大家喜欢~
参考文章
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助 ^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab 团队 收货大厂一手好文章~
我们来自字节跳动,是旗下大力教育前端部门,负责字节跳动教育全线产品前端开发工作。
我们围绕产品品质提升、开发效率、创意与前沿技术等方向沉淀与传播专业知识及案例,为业界贡献经验价值。包括但不限于性能监控、组件库、多端技术、Serverless、可视化搭建、音视频、人工智能、产品设计与营销等内容。
欢迎感兴趣的同学在评论区或使用内推码内推到作者部门拍砖哦 🤪
字节跳动校 / 社招投递链接: https://job.toutiao.com/
内推码:3YNYJUT