Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

NodeJS实现简易区块链

r8kao8k8
123 0 0
NodeJS实现简易区块链" h) }4 C2 }6 s& R+ m
之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。
" t4 X& h) F& b0 o. a但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。- d& w& m8 ?, {# v4 F8 r
想了想,就动手做试试咯。7 q* k. |  h1 q8 T; w3 N
?查看全部教程 / 阅读原文?- g! n: S( V# d3 A2 Y( B- e
技术调研1 W; I' h# l2 f
在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。5 S% C7 }% P  G& r' o: O
结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。+ L. m7 x2 P% ~7 L2 j
区块定义
1 M0 v3 p" u0 r为了方便表示区块,将其封装为一个class,它没有任何方法:
5 N. i! v/ u: V4 M" \/ ~$ c9 p, j/**
3 F! i2 w. O0 P/ y$ H$ `0 o: { * 区块信息的结构化定义
4 v0 c$ U; u7 a  w% L, o */5 y# d7 M( M: _/ G9 B. V
class Block {- `8 ?8 R/ A( J8 x' ~$ ?
  /**
3 d  m# z6 _9 m   * 构造函数
" y4 X: `4 y9 i  @  @+ \   * @param {Number} index ' p' q% @3 C+ d) M( X& u
   * @param {String} previousHash
1 |0 r+ v* x0 w# H! q   * @param {Number} timestamp 2 Q' J- M+ r: \& _4 T* H
   * @param {*} data
+ U/ k: f. B, M6 B- Z   * @param {String} hash , X, R5 j) [' b' T% u( }  N; Y) r  s
   *// Z1 K0 e: L5 Z
  constructor(index, previousHash, timestamp, data, hash) {% X% g0 p: l$ G5 Q' j
    this.index = index // 区块的位置2 q# a+ Y0 [5 v5 F8 z1 @+ U1 _
    this.previousHash = previousHash + '' // 前一个区块的hash
- O3 W) e% Q+ |; b1 j    this.timestamp = timestamp // 生成区块时候的时间戳
( _- P+ ]* t4 x) P& K8 l" B$ {    this.data = data // 区块本身携带的数据" l1 J8 k9 _) n5 `7 X
    this.hash = hash + '' // 区块根据自身信息和规则生成的hash% z# Y+ G! b- `8 s  G# G5 M
  }
9 G7 a& b7 g% Q}
$ C' N& c0 i1 m至于怎么生成hash,这里采用的规则比较简单:2 H2 p* `0 ]& Q1 Z
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash
- j! r0 _# X- F
# u4 T& Y' Z5 J' t' u7 r/ w为了方便,会引入一个加密库:
& I6 P' Q  ~+ O: Econst CryptoJS = require('crypto-js')
) L( K0 y( I5 Z# ]) ~链结构定义
+ `( Q' m1 R5 Z很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:6 o( Z# I1 ?/ ~: K4 v; `
按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算
% f$ k# ]* S1 b+ {: T" y3 \: d
) Y* O! d& d5 P+ K; z1. 起源块
" N# s/ j: w  q: g0 {起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。( r* Z6 d7 ]7 X" y) `5 ~
class BlockChain {% L/ C0 p( D+ O3 _. c, f  J
  constructor() {" B- a* X1 F8 P, H8 F' y
    this.blocks = [this.getGenesisBlock()]
4 @0 {) Z+ @% D  }6 @5 c- G* v! v& B: @( G, s" Q" l
  /**
* S# ]( U( r0 y( b6 t2 y+ x  B( S' S   * 创建区块链起源块, 此块是硬编码, s, p: n1 T6 z
   */8 _( ?: Z- D% z1 q* G' O9 P( n, v
  getGenesisBlock() {% j9 Y. ?! z. |: p: Z
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')( b. d# l. L$ P' j" Y1 D
  }
5 {$ n2 C# ^; P0 R9 B: Y}$ |" _0 \  u- t2 U6 U
2. 计算下一个区块
' j% ^; ]$ O5 p: h- ^, [' ]BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。
/ O5 z. X$ a( y. {/ @// 方法都是BlockChain对象方法
5 L$ k" @- A9 G! y. {  /**2 v! \! G6 {2 W1 O1 |
   * 根据信息计算hash值  x: k% }' C% U$ Q# U4 \! [
   */2 c2 q: s1 u0 \+ B
  calcuteHash(index, previousHash, timestamp, data) {; g+ O( w1 J! f! r9 E1 L& W+ I
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''' I0 g  a7 x! `
  }& L! @5 x2 [  @* F  O
  /**# A$ V% Q! I6 g6 c
   * 得到区块链中最后一个块节点
0 M8 q; g, B' s( i   */
0 Q7 e8 T0 b+ ]: z* J& e6 |6 J  getLatestBlock() {
/ w6 c) c2 p9 ]1 l    return this.blocks[this.blocks.length - 1]
4 |$ n+ x0 |7 K/ ~  }
6 e- G) `3 V4 f1 B+ Y  /**
9 N% u1 ?4 r. `   * 计算当前链表的下一个区块
0 a- F0 w$ j5 P8 n   * @param {*} blockData
" r0 Z6 M, ~- Y/ E; T" Y   */3 `; K* A8 }* t7 g' P
  generateNextBlock(blockData) {$ S& L9 q/ b" e! |6 A5 G, Y4 U
    const previousBlock = this.getLatestBlock(): c8 ~8 B: [9 H( w
    const nextIndex = previousBlock.index + 15 {+ X" T% N4 i8 Y9 \7 g( U
    const nextTimeStamp = new Date().getTime()1 R  k+ ?. B8 o, [4 m  b" ^
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)9 m1 \' s. O! Q
    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
( @) |7 x& }& {0 z! m6 H; p3 r  }
' h. c" U! Q* G" r# P3. 插入区块
  S6 e- N8 q5 {插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。
, k0 f! L6 b- O) S4 E2 r% v, j  /**  f( d8 ]9 o- F( h* {
   * 向区块链添加新节点8 z1 [. @, V3 ~5 R7 F
   * @param {Block} newBlock
9 v5 E  C- }3 q& H( s, a6 r( X   */
6 p0 N. J) @/ R4 _5 e  addBlock(newBlock) {" d" A- [" e% D
    // 合法区块  s" A, H# H- P6 a+ S; X
    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {& ?0 L7 Q, ^- G: q! ?7 J, I$ |$ J
      this.blocks.push(newBlock)
' y* r; z! [8 e' w- y: H$ }      return true  
8 }9 m6 x3 k7 b5 d7 n. J    }. J  Y: m4 g. y9 ]* w; q2 t
    return false
9 }% J& l9 b0 |- ?7 [  }4 G3 y, K7 b( K0 n% Z
检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:
& e+ j- Y1 b% G判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成% c. J% U) m: Y9 y) D! H- W
& x0 @: N0 j- b* p& K
  /**
, m. p: S6 ?/ a4 Z% K   * 判断新加入的块是否合法
2 q. F& z; N" ], }   * @param {Block} newBlock   @; i* |1 I/ U: Y
   * @param {Block} previousBlock 4 O4 o/ F" A% k& j- p( {# ^* X5 x
   */
* Y8 `; u7 P" `  K. p* K$ i3 d  isValidNewBlock(newBlock, previousBlock) {& C$ S: P+ y9 j! ~
    if(/ ~  ^; z* \* {1 L, h$ U- _8 r
      !(newBlock instanceof Block) ||" ^8 b* U$ I; `" ^
      !(previousBlock instanceof Block)
- ]( F) R* N! ~& F2 ^( h. `    ) {
" j8 `- t* s$ ?3 N9 k8 f      return false3 z; o% g0 B9 B3 J: u
    }
, ]. b' E& c* f- J% I2 [    // 判断index1 ?' I2 b5 R, G* P- g( R0 r7 B) @
    if(newBlock.index !== previousBlock.index + 1) {
& ~+ W8 i& q9 m0 d$ [      return false. v8 ~, B7 j/ @8 \' l+ j
    }" T" i, h, @/ v
    // 判断hash值; F8 i3 v# {1 X" a
    if(newBlock.previousHash !== previousBlock.hash) { , b# M; j% _  `* S1 D/ K' g* _
      return false9 I# g9 h, O  H3 x2 T/ r  O! U6 [
    }2 f3 v7 N- L/ N) K$ w# I) N) t
    // 计算新块的hash值是否符合规则
+ p- H2 q& ~& h- [% E3 b" v    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) {
3 ~7 q# H/ G. Z9 M      return false. r9 R$ H3 f1 |2 L+ b
    }
6 G7 M# A) r1 m1 `. |& ?5 J    return true& x7 K4 z# e, t* `0 Z
  }
& t8 ^2 y$ U- H: V/ ~4 m* ^4 o4. 批量插入8 o9 Z: O' E, h* X: W
批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。- }3 c9 x% Q. |7 R/ m. L' l) B
在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。
1 i# G  |9 v9 w7 x8 X& c6 B: ?2 [9 D但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链1 Y& t  d, M4 _8 `- e  G
插入新链的方法如下:
4 u% Y! \: w% q/ t  /**7 r3 E  o1 u! }8 E$ r  a2 s
   * 插入新链表, n1 }. A6 S* F# D, ?$ U1 V
   * @param {Array} newChain 2 E+ J3 `* m+ i- T9 c+ r: {3 ~
   */
5 k2 y  ?9 g, U# R  addChain(newChain) {
# C$ x! l5 a9 }9 \$ l    if(this.isValidNewChain(newChain)) {
- x+ u& {# O4 u      const index = newChain[0].index  K2 `$ [5 c# @; ?3 N
      this.blocks.splice(index)
9 _: M, w8 N7 d$ r; b2 m      this.blocks = this.blocks.concat(newChain)! Y* |+ @0 P* A4 W" b
      return true, H( ]9 K" _0 |" Q/ N
    }
7 U; k* K$ Z2 p+ Z! i    return false$ N2 @4 \9 @& @" N6 n, f" A
  }
, h2 f5 h& y2 g- k8 i  N6 A实现上面所述逻辑的方法如下:# ?7 A: B& n7 ^! J4 k" L8 s
  /**( ~/ t, X6 B- B0 [3 K' ]$ J& @. T
   * 判断新插入的区块链是否合法而且可以覆盖原来的节点6 A7 S- f& g; x1 |4 z
   * @param {Array} newChain : l! k) @8 G- u$ t. M8 \& O0 t
   */
  x; m; {% S  v& f" h  isValidNewChain(newChain) {
& @# o5 Q$ I" L7 x( O& r8 ~" t! `+ ~    if(Array.isArray(newChain) === false || newChain.length === 0) {; O/ J6 B" S; L' T6 m7 F
      return false
! Y1 V  N5 J+ }8 G* O    }8 V& t+ V# ^% p% c) A$ Q+ }; z
    let newChainLength = newChain.length,
) _1 B. L# x2 I/ o; @$ I# f      firstBlock = newChain[0]
- q% ?2 K% i# h    // 硬编码的起源块不能改变
' c/ H/ j; }' d6 \# h    if(firstBlock.index === 0) {! F; D! l9 o, q& T5 h1 w
      return false; J/ w6 Z* x4 D, V. D
    }
6 Y# {; e0 d5 W! j" |    // 移植新的链的长度 3 g$ r- [1 c" N8 H
5. 为什么需要批量插入?
. R' ?9 P$ ?; ^* N我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。
1 ?1 H3 a4 P" K! P/ dA与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。  \% h- W0 R- _* n( ?; K# f
而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。
, y4 |( F: T0 i3 O% a8 C效果测试  e& ?7 ?* T, s( o6 K$ P9 r
虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:
; J6 {: Y7 W. p1 E
: N  Z0 p: {7 w, X红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。6 f. Q2 w/ {5 z& \# R6 ^- s( U
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
标签: NodeJS
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

r8kao8k8 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    1