NodeJS实现简易区块链
r8kao8k8
发表于 2023-1-2 20:25:20
190
0
0
之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。
但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。
想了想,就动手做试试咯。: L0 R5 ]( {/ _3 q
?查看全部教程 / 阅读原文?
技术调研
在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。0 N% I$ ^( ~+ A, Z0 n
结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。
区块定义
为了方便表示区块,将其封装为一个class,它没有任何方法:
/**
* 区块信息的结构化定义7 E F9 x' [1 C+ }% M
*/
class Block {
/**4 T, G" S1 }( C
* 构造函数& M8 j+ d$ T7 `6 M5 P& ]
* @param {Number} index 2 B7 C+ T' U4 L3 L: \/ X( `3 ]' H
* @param {String} previousHash 3 Z/ }5 h3 ~0 I& H. i' x
* @param {Number} timestamp
* @param {*} data
* @param {String} hash
*/6 c* x' h: J4 O' y8 S
constructor(index, previousHash, timestamp, data, hash) {
this.index = index // 区块的位置
this.previousHash = previousHash + '' // 前一个区块的hash1 z6 n$ D# t6 `0 V% n- ]% C( `
this.timestamp = timestamp // 生成区块时候的时间戳
this.data = data // 区块本身携带的数据) o& t) R+ Y7 B
this.hash = hash + '' // 区块根据自身信息和规则生成的hash
}
}" z6 B) s9 v f B( O5 [
至于怎么生成hash,这里采用的规则比较简单:
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash& G# }* J4 E: ]" ]& Y
! b" R; R4 Z! k5 x/ E; ?
为了方便,会引入一个加密库:" j0 w' X# d9 U
const CryptoJS = require('crypto-js')' m) u( ?% b* m0 M
链结构定义
很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:, q$ {! x1 E7 C
按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算
1. 起源块: }7 P( O0 n7 V. A
起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。! X2 z. V! ` I0 _
class BlockChain {
constructor() {
this.blocks = [this.getGenesisBlock()]
}
/**
* 创建区块链起源块, 此块是硬编码* N1 q: _0 z# F
*/
getGenesisBlock() {9 R+ S4 `$ _2 z" l$ Z/ D* z
return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4'). O. h) m- z7 h' A
}
}% K M& m5 c7 [9 H" r9 @
2. 计算下一个区块! E2 J: K* q/ i8 s( T
BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。" P3 W0 C9 O' x8 G& K! _5 w0 R
// 方法都是BlockChain对象方法& {- e3 q8 M" F
/**- E# w% ^; o5 ^! S; {9 J
* 根据信息计算hash值
*// J+ D, q; g5 D4 d) c3 k
calcuteHash(index, previousHash, timestamp, data) {; p; N' ]/ w+ S" g$ W4 F% G1 }6 n
return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''- V" f/ l$ C1 }% G
}
/**9 m* p$ D4 `0 c; J
* 得到区块链中最后一个块节点3 s% b, w* E9 a3 D5 I
*/- T# F$ Y# V6 L' u/ ?' ~
getLatestBlock() {" |, S% R' C5 w7 ~1 s' u
return this.blocks[this.blocks.length - 1]/ u: i/ n$ I0 u' v. O( R$ B
}
/**
* 计算当前链表的下一个区块
* @param {*} blockData " I9 |7 }$ `9 g, i# h2 Q
*/6 m: k2 m# k5 }' l/ l, y1 S
generateNextBlock(blockData) {
const previousBlock = this.getLatestBlock()
const nextIndex = previousBlock.index + 1
const nextTimeStamp = new Date().getTime(). R4 A" ~2 Y5 q4 s# h
const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)& l" v. L( d4 M5 \* e
}
3. 插入区块4 c J7 d% U) P4 z9 Z
插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。' O8 _7 O8 g0 G4 T' q, O
/**
* 向区块链添加新节点, S" j# s0 @7 h# N# c( i
* @param {Block} newBlock
*/
addBlock(newBlock) {
// 合法区块
if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {& {6 T3 e/ b7 g+ D L
this.blocks.push(newBlock)# M* g+ { `8 f, V- `
return true
}
return false
}3 @- h9 M6 b8 @6 L% M [5 Z) i% j0 ]
检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:, P- A. ]4 H0 `8 s3 ~
判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成
/**
* 判断新加入的块是否合法
* @param {Block} newBlock 4 ?( O4 Q& ]- [; e
* @param {Block} previousBlock
*/
isValidNewBlock(newBlock, previousBlock) {1 P6 ^% @6 v* _$ t. J; M
if(
!(newBlock instanceof Block) ||6 ], O( C. M8 V, Y+ E
!(previousBlock instanceof Block)* T: Y8 [8 S) r
) {
return false% J9 E" m8 I2 E) b9 f
}
// 判断index' Z' x! A ~ ~3 ^: R# h
if(newBlock.index !== previousBlock.index + 1) {
return false- R' P) W- k/ v: q/ \0 L9 E) A
}% P7 J1 Q7 D& n8 Q( b* o5 e
// 判断hash值+ A6 l( w2 u# `9 J5 n
if(newBlock.previousHash !== previousBlock.hash) {
return false- Q+ w$ G" _0 n0 e; D, N
}4 ^2 d# K- o4 T5 f) C6 J
// 计算新块的hash值是否符合规则
if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) {
return false
}
return true
}
4. 批量插入
批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。
在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。. i% c) b) H4 _% K
但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链 。
插入新链的方法如下:* i9 q$ t/ ?% G9 x8 _
/**
* 插入新链表
* @param {Array} newChain
*/0 x: z/ m& |% c" T3 C4 O" i
addChain(newChain) {
if(this.isValidNewChain(newChain)) {2 u' Z z4 l4 q v$ q# v8 V
const index = newChain[0].index
this.blocks.splice(index)
this.blocks = this.blocks.concat(newChain)
return true8 w7 \+ A1 [8 _5 {: y/ V$ C
}
return false
}
实现上面所述逻辑的方法如下:
/**
* 判断新插入的区块链是否合法而且可以覆盖原来的节点, I) m; C R9 i! |
* @param {Array} newChain
*/7 F) B2 m8 f- x$ G- E1 G
isValidNewChain(newChain) {
if(Array.isArray(newChain) === false || newChain.length === 0) {
return false
}4 ?) L% K: e/ V' G% d7 Y! Z
let newChainLength = newChain.length,
firstBlock = newChain[0]
// 硬编码的起源块不能改变
if(firstBlock.index === 0) {: X8 k5 j$ N% u( K/ i
return false
}/ C2 E9 A t* ?$ _7 ]! `
// 移植新的链的长度 & h r! W6 `) X; e3 w! w
5. 为什么需要批量插入?: h9 B+ [* s; ?
我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。
A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。; M& N" m: F% J8 Z2 j
而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。$ A2 S$ k5 S9 O$ T5 L' z0 |
效果测试9 N* C/ s" k1 Y; U% W; }: M
虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:
红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
成为第一个吐槽的人