NodeJS实现简易区块链
r8kao8k8
发表于 2023-1-2 20:25:20
191
0
0
之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。9 }( x; |2 p) i$ H0 e: \
但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。
想了想,就动手做试试咯。
?查看全部教程 / 阅读原文?
技术调研2 _2 F3 }( o% J7 C9 N
在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。
结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。+ T. U! ^6 @4 l; V% r! Y; @: {: f7 X
区块定义
为了方便表示区块,将其封装为一个class,它没有任何方法:
/**
* 区块信息的结构化定义5 }+ j7 I! ^' f, [# Q
*/
class Block {( t* v+ }: t% F' ]9 c
/**
* 构造函数
* @param {Number} index
* @param {String} previousHash
* @param {Number} timestamp
* @param {*} data c ~% G( ~( f$ Y4 \
* @param {String} hash 4 h7 Y2 W& W2 E$ S& |2 b& I
*/0 H! \. C/ @6 Y1 X: G* B, \( j' k
constructor(index, previousHash, timestamp, data, hash) {
this.index = index // 区块的位置3 L# K+ s5 |: R4 a6 ?
this.previousHash = previousHash + '' // 前一个区块的hash$ d' V- ?9 I8 n7 x. M- h# ?# s
this.timestamp = timestamp // 生成区块时候的时间戳
this.data = data // 区块本身携带的数据
this.hash = hash + '' // 区块根据自身信息和规则生成的hash
}
}1 }' p: `5 m5 b8 w: `0 h
至于怎么生成hash,这里采用的规则比较简单:
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash
" |1 ~4 G& e0 C3 @1 j* h: t
为了方便,会引入一个加密库:* I- D7 d) s- o: h
const CryptoJS = require('crypto-js')0 c8 x' \0 b5 R3 K0 D) e8 s
链结构定义
很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:. E7 W* Y d4 h
按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算0 F) X% S3 L' Y3 v
1. 起源块
起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。0 \- m1 e; \8 D9 j
class BlockChain {
constructor() {- ?6 w( ^. Q O% L, [( \
this.blocks = [this.getGenesisBlock()]$ e6 \/ A [1 Y' P; ^! i h
}
/**! M9 K' L# s9 E* ?7 i
* 创建区块链起源块, 此块是硬编码
*/
getGenesisBlock() {! x3 D& N' q8 |% H( a, E
return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')
}$ e! l/ g; w [/ }% u7 _
}
2. 计算下一个区块3 R2 {6 G* T$ z+ x+ d- A S
BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。
// 方法都是BlockChain对象方法
/**
* 根据信息计算hash值8 X3 a. m# }% K8 Y3 j
*/4 d4 C% u* ^' _; B( j
calcuteHash(index, previousHash, timestamp, data) {
return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''0 C9 K$ o- X5 S; Q7 Y, |3 L
}
/**% G1 q6 f0 R( i* |
* 得到区块链中最后一个块节点
*/
getLatestBlock() {
return this.blocks[this.blocks.length - 1]+ R7 W4 K7 u. k3 S! e
}) @# k; Q6 a+ V7 C5 e$ e( A
/**
* 计算当前链表的下一个区块( V3 d1 C2 `6 g0 } y
* @param {*} blockData
*/& I# k6 P) q% N2 r
generateNextBlock(blockData) {
const previousBlock = this.getLatestBlock()- x& }+ p4 v$ {2 D
const nextIndex = previousBlock.index + 1
const nextTimeStamp = new Date().getTime()
const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
}3 M" L. W6 q8 J7 _" U
3. 插入区块
插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。
/**
* 向区块链添加新节点/ \: g+ Z& o& l0 a
* @param {Block} newBlock
*/
addBlock(newBlock) {
// 合法区块
if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {+ A, |1 T5 f) q5 V$ d
this.blocks.push(newBlock)
return true
}
return false$ U5 C% J# L: V, x ~. G
}% ^( Q: U u. n, R* ^8 u
检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:' N6 m* d, V F; d& H
判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成& Y2 e) t3 P0 Q' J- v$ c* n
5 D4 r) b( K/ m2 H
/**
* 判断新加入的块是否合法7 a: P. ?: q: t( d2 V- C& [( D; f6 H
* @param {Block} newBlock ( ?# \& w2 g( z+ C
* @param {Block} previousBlock
*/& }$ K/ u- S: l1 I' E
isValidNewBlock(newBlock, previousBlock) {
if(
!(newBlock instanceof Block) || I& L8 I. a# z) S1 g7 ^5 p& H% O* w; }
!(previousBlock instanceof Block)
) {0 H) L7 `! I. k+ @( M
return false/ O1 w- ?2 J$ ?
}
// 判断index
if(newBlock.index !== previousBlock.index + 1) {
return false
}
// 判断hash值
if(newBlock.previousHash !== previousBlock.hash) {
return false; r2 A5 R" z( j* S% B5 R; O e
}0 U2 u7 n- M4 a5 m5 m6 }
// 计算新块的hash值是否符合规则5 `* ^# g( L( L$ O" O( G1 H) K
if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) {
return false
}
return true
}$ J" r' y* L0 b9 a7 t
4. 批量插入$ ^0 |( c- T, ?3 H
批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。 t) B% ]1 v3 k) q5 p
在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。3 x* }% h7 D0 F0 _) ?" t
但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链 。4 x9 n, F0 [4 S+ q
插入新链的方法如下:, R! O7 V% d, ]3 p& e" n+ O
/**( \' t) r5 \3 \9 ^/ N% n0 ?
* 插入新链表
* @param {Array} newChain M7 O$ n1 f5 p5 [: E
*/
addChain(newChain) {
if(this.isValidNewChain(newChain)) {6 e1 P& q% h3 f; R3 d w7 V' Q9 [5 ?
const index = newChain[0].index) `5 Y. J, X6 K2 l
this.blocks.splice(index); R2 q/ ~" N. I, C
this.blocks = this.blocks.concat(newChain)2 Q; ?( x# G. a4 [8 g( {% m0 U, G
return true% c/ O1 v0 L$ ^1 X8 a# R
}0 a) u8 ~5 A6 u) E0 w
return false
}3 B6 r) \) W8 _; G, B9 e% W: o
实现上面所述逻辑的方法如下:% v/ ]4 m5 s e) y. |8 c
/**
* 判断新插入的区块链是否合法而且可以覆盖原来的节点
* @param {Array} newChain
*/2 n+ Z. k. ^2 X! y/ A
isValidNewChain(newChain) {
if(Array.isArray(newChain) === false || newChain.length === 0) {
return false
}: ?8 x; v9 S$ Q* X0 u2 ]0 I8 h
let newChainLength = newChain.length,
firstBlock = newChain[0]; Z, F! \+ e% S. h9 j8 ^ ^
// 硬编码的起源块不能改变
if(firstBlock.index === 0) {8 v i1 c( i% m) `. u# U
return false5 }/ g. b* t& n# j7 x1 n# W
} u" n! }9 a j L
// 移植新的链的长度 & C" G! V9 `" ?- ?
5. 为什么需要批量插入?! n: E/ T4 z" v( o( f/ o& r, D
我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。
A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。6 K- A" O& I7 ^3 Y; X
而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。0 G3 Z/ a( e# }+ M& }7 ?# c
效果测试
虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:; A9 s6 I* u5 t8 h4 _
5 b. `# u/ k1 a, N! w! u* s
红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
成为第一个吐槽的人