概览
区块链的基础概念很是简单, 说白了就是一个维护着一个持续增加的有序数据记录列表的这么一个分布式数据库。在此章节中咱们将实现一个简单的玩具版的区块链。此章节结束时,咱们的区块链将实现如下功能:html
- 实现区块和区块链结构定义
- 实现能够将包含任意数据的新区块写入到区块链的方法
- 实现能够与其余节点进行点到点沟通和同步区块链数据的运行节点
- 操做单个运行节点的简单HTTP(Restful) API
区块数据结构
咱们首先会从区块数据结构的定义开始。在当前阶段,简单起见,咱们只会给每一个区块定义最关键的属性。node
- index: 区块在区块链中的高度(即序号),由于每加一个区块,该index就会加1,因此币圈将其称之为高度。
- data: 任何须要包括在此区块中的数据。本章节中能够是任何数据,到后面章节咱们会用来记帐用。
- timestamp: 时间戳。本章节中也是能够是任何数据,日后咱们须要保证这个字段是正确的时间戳数据,用来防止攻击等用。
- hash: 根据区块内容计算的哈希值(SHA256)。
- previousHash: 前一个区块的哈希值。经过这个属性,咱们能很方便回溯前面的区块。
相应代码大体以下:git
class Block { public index: number; public hash: string; public previousHash: string; public timestamp: number; public data: string; constructor(index: number, hash: string, previousHash: string, timestamp: number, data: string) { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = hash; } }
区块哈希
区块哈希值是区块中最重要的属性之一。哈希值根据区块中的全部数据计算而得,这意味着若是区块中任何数据发生变化,原有的哈希值就再也不有效。区块哈希值也能被当作区块的惟一性标识。好比说,两我的同时挖矿成功,那就有可能出现两个高度一致的区块,可是由于要经过其余属性值一块儿算哈希(日后咱们会看到data属性会存放交易数据,交易数据,特别是id,确定不能重复),因此绝对不会出现同样的哈希值。 根据如下的代码来计算哈希值:github
const calculateHash = (index: number, previousHash: string, timestamp: number, data: string): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString();
须要注意的是,在这个阶段,区块的哈希值与挖矿没有任何关系,由于还未有 POW(工做量证实) 问题须要解决。咱们使用区块哈希值来保证区块的完整性,同时也使用它来回溯前一个区块。web
由以上对 hash 和 previousHash 属性的处理机制,很容易得出区块链的一个重要特性:区块的内容不能被修改,除非同时修改它后续的全部区块内容。typescript
如下的例子描述了这个特性。若是将第44区块的数据从“DESERT”修改为“STREET”,全部后续区块的哈希值也必须被修改。这是因为区块的哈希值是经过对区块的内容计算哈希获得的,而内容中包含了 previousHash 这个表明了前一个区块的哈希的值。shell
这个特性在咱们后面章节中引入的工做量证实机制来讲尤为重要。一个区块在区块链中的位置越深(即越靠前),要修改它的难度就越大,由于须要同时修改它自己以及它后续的全部区块。数据库
创世块
创世块是区块链中的第一个区块。它是惟一一个没有 previousHash 的区块,由于这个区块比较特别,咱们在代码里会将创世区块进行硬编码处理:express
const genesisBlock: Block = new Block( 0, '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7', null, 1465154705, 'my genesis block!!' );
建立区块
建立一个新的区块,须要得到上一个区块的哈希值,并建立其余必须的内容( index, hash, data 和 timestamp)。区块的数据(data字段)由用户提供,其余的参数使用如下代码生成:npm
const generateNextBlock = (blockData: string) => { const previousBlock: Block = getLatestBlock(); const nextIndex: number = previousBlock.index + 1; const nextTimestamp: number = new Date().getTime() / 1000; const nextHash: string = calculateHash(nextIndex, previousBlock.hash, nextTimestamp, blockData); const newBlock: Block = new Block(nextIndex, nextHash, previousBlock.hash, nextTimestamp, blockData); return newBlock; };
保存区块链
目前咱们使用 JavaScript 的数组,将区块链保存在程序的运行内存中。这意味着当一个运行节点中止时,该节点上的区块链数据不会被持久化。
const blockchain: Block[] = [genesisBlock];
验证区块完整性
为确保数据完整性,咱们应想办法作到可随时对一个区块,或者一条区块链上的区块进行有效性验证。特别是当咱们的节点从其余运行节点中接收到广播过来的新区块时,咱们就须要验证区块的有效性,以便决定是否接受这些区块。
验证区块的有效性,须要知足如下全部条件:
- 区块的 index 须要比上一个区块大1;
- 区块的 previousHash 属性须要与上一个区块的 hash 属性一致;
- 区块自身的 hash 值须要有效。
如下代码描述了整个验证过程:
const isValidNewBlock = (newBlock: Block, previousBlock: Block) => { if (previousBlock.index + 1 !== newBlock.index) { console.log('invalid index'); return false; } else if (previousBlock.hash !== newBlock.previousHash) { console.log('invalid previoushash'); return false; } else if (calculateHashForBlock(newBlock) !== newBlock.hash) { console.log(typeof (newBlock.hash) + ' ' + typeof calculateHashForBlock(newBlock)); console.log('invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash); return false; } return true; };
同时咱们还必须验证该区块的结构是否正确,以免其余节点广播过来的带有不正确格式的数据致使程序崩溃。
const isValidBlockStructure = (block: Block): boolean => { return typeof block.index === 'number' && typeof block.hash === 'string' && typeof block.previousHash === 'string' && typeof block.timestamp === 'number' && typeof block.data === 'string'; };
既然咱们如今可以验证单个区块的有效性,咱们就能够进一步的对整个区块链进行有效性验证了。首先验证链中的第一个区块为创世区块。而后,咱们使用以上的方式来依次校验链中的下一个区块,如下为实现代码:
const isValidChain = (blockchainToValidate: Block[]): boolean => { const isValidGenesis = (block: Block): boolean => { return JSON.stringify(block) === JSON.stringify(genesisBlock); }; if (!isValidGenesis(blockchainToValidate[0])) { return false; } for (let i = 1; i < blockchainToValidate.length; i++) { if (!isValidNewBlock( blockchainToValidate[i], blockchainToValidate[i - 1])) { return false; } } return true; };
选择最长链
在任什么时候候,在区块链系统中都应该只存在一条正确的链,但冲突仍是在所不免的,咱们须要有一个你们都认同的共识机制来确保冲突得以解决。在冲突发生的状况下(好比:主链在71这个块的时候发生分叉,而后我紧邻的节点在某一条链的基础上挖出了第73个块),则从中选择包含更长区块的链(好比个人节点启动时会和其余节点请求区块链状态,发现有最后块为72和73的两条链,那么咱们的节点将会在73这个链的基础上继续贡献资源进行挖矿)。在如下的例子中,因为被更长的区块链复写,第72区块: a350235b00 中的数据将不会被包括在区块链中。
代码实现以下:
const replaceChain = (newBlocks: Block[]) => { if (isValidChain(newBlocks) && newBlocks.length > getBlockchain().length) { console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); blockchain = newBlocks; broadcastLatest(); } else { console.log('Received blockchain invalid'); } };
节点间通讯
每一个运行节点都必须能和其余节点广播和同步区块链数据。咱们经过如下规则保证节点间能正确有效的同步:
- 当一个节点生成新区块时,该节点会将此区块广播至区块链网络中
- 当一个节点和另一个节点创建点对点链接时,该节点将会向另外一个节点请求最新的区块链信息
- 当一个节点发现从其余节点过来的一个区块的 index 比该节点中保留的区块链的最后一个区块的 index 大,根据两个index之间相差的大小,该节点会有两个选择:若是只相差1,则将此区块加到自身的区块链中; 若是超过1,则须要向其余节点请求整条区块链。
咱们将会使用 WebSocket 技术来实现各个节点的点对点通讯。各个节点的 socket 列表将保存在 const sockets: WebSocket[] 变量中。咱们并无实现节点发现机制,因此新增长一个节点后,须要手动添加须要创建点对点链接的目标节点的地址。
操做节点
用户需可以以某种方式来操做节点。咱们将经过实现相应的http服务端接口来提供相应功能。
const initHttpServer = ( myHttpPort: number ) => { const app = express(); app.use(bodyParser.json()); app.get('/blocks', (req, res) => { res.send(getBlockchain()); }); app.post('/mineBlock', (req, res) => { const newBlock: Block = generateNextBlock(req.body.data); res.send(newBlock); }); app.get('/peers', (req, res) => { res.send(getSockets().map(( s: any ) => s._socket.remoteAddress + ':' + s._socket.remotePort)); }); app.post('/addPeer', (req, res) => { connectToPeers(req.body.peer); res.send(); }); app.listen(myHttpPort, () => { console.log('Listening http on port: ' + myHttpPort); }); };
根据以上代码暴露出来的HTTP接口,用户能够发送请求到节点进行如下操做:
- 列出全部区块
- 由用户指定相应内容来建立一个新区块
- 列出链接过来的节点的地址
- 经过websocket url链接到指定节点
您能够经过Curl工具来对节点进行操做,固然您也能够经过postman等工具来操做:
#get all blocks from the node > curl http://localhost:3001/blocks
架构
每一个节点都对外暴露两个web 服务: 一个是用户来给用户对节点进行操做(HTTP Server),一个是用来实现节点间的点对点通讯(Websocket HTTP server)。
运行测试
安装
npm install
运行
打开一个终端运行节点1. 节点1的http服务端端口为3001, p2p端口为6001。
npm run node1
建议打开另一个终端运行节点2,以便能经过输出查看两个区块链节点是怎么通讯的。 节点1的http服务端端口为3002, p2p端口为6002。
npm run node2
ps: 节点2运行后,便可以经过addPeer这个api和节点1进行websocket链接。
生成一个区块
curl -H "Content-type:application/json" --data '{"data" : "Some data to the first block"}' http://localhost:3001/mineBlock
返回结果示例:
{ "index": 1, "previousHash": "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", "timestamp": 1561025398.834, "data": "Some data to the first block", "hash": "979335f8383fa058c0abf5d342a232d345de51ea644756d3522eca5637e97a17" }
获取区块链
curl http://localhost:3001/blocks
返回示例:
[ { "index": 0, "previousHash": "", "timestamp": 1465154705, "data": "my genesis block!!", "hash": "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7" }, { "index": 1, "previousHash": "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7", "timestamp": 1561025398.834, "data": "Some data to the first block", "hash": "979335f8383fa058c0abf5d342a232d345de51ea644756d3522eca5637e97a17" } ]
链接到一个节点
curl -H "Content-type:application/json" --data '{"peer" : "ws://localhost:6001"}' http://localhost:3002/addPeer
查询链接的节点列表
curl http://localhost:3001/peers
返回示例:
["::ffff:127.0.0.1:54261"]
小结
到如今为止,咱们实现了一个简单的玩具版的区块链。此外,本章节还为咱们展现了如何用简单扼要的方法来实现区块链的一些基本原理。下一章节中咱们将为naivecoin 加入工做量证实机制。
本章节的代码请查看这里
本文由天地会珠海分舵编译,转载需受权,喜欢点个赞,吐槽请评论,如能给Github上的项目给个星,将不胜感激。
原文出处:https://www.cnblogs.com/techgogogo/p/11072536.html