Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

NodeJS实现简易区块链

r8kao8k8
192 0 0
NodeJS实现简易区块链2 n5 [- }# v* Q" A$ y& D
之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。
3 a8 C- N' `# g8 K但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。
$ D! j% G7 G& ]想了想,就动手做试试咯。
1 R* I7 n  I: ~0 \& o?查看全部教程 / 阅读原文?" I0 ]& L" n4 ~
技术调研0 e$ K% M* m( Q& U$ E' C
在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。
  P9 v* h* v- J结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。
6 ~* Y, }: R/ g1 u5 ?区块定义
) u: R) ^, F, u- P为了方便表示区块,将其封装为一个class,它没有任何方法:7 M: L- {' k; w+ w' F4 F. y4 G  E
/**
) b; h) T! x7 k7 g; z * 区块信息的结构化定义1 T! ~$ j0 f3 y8 C* W9 v
*/3 q0 `$ M! |' t! i
class Block {
( I! D& I8 h  Y- x, X  /**
* d8 v0 A9 w9 X4 e4 i0 R3 k   * 构造函数) j; g( X2 k6 j. N
   * @param {Number} index
9 G7 g# z5 a6 ?0 [% ?" I) E$ \   * @param {String} previousHash
/ \6 _5 I9 t% f$ F' z1 l; a   * @param {Number} timestamp ' s% ~# {. n% o2 \6 _
   * @param {*} data
" a5 d0 P3 l8 ?( q; d' q1 I   * @param {String} hash
0 r) H2 F/ j8 b! }* U' a1 ^   *// G$ _1 E! N- A& R
  constructor(index, previousHash, timestamp, data, hash) {6 {1 C/ V1 @* N0 X& `
    this.index = index // 区块的位置
; s: N8 [: y3 S! g    this.previousHash = previousHash + '' // 前一个区块的hash
6 j2 |$ W9 r# A- o: w+ _1 Z    this.timestamp = timestamp // 生成区块时候的时间戳8 A! v5 X0 D: \/ F+ b* ?
    this.data = data // 区块本身携带的数据
. i+ I( g0 y4 _    this.hash = hash + '' // 区块根据自身信息和规则生成的hash/ Z0 s- _+ G# W1 q
  }
3 s3 L; J$ ?) G( `}
1 e3 @2 k5 [& a' ^; A至于怎么生成hash,这里采用的规则比较简单:8 I% G1 {8 r/ k
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash
+ |" V2 y) o2 z/ C+ L3 r7 s" c9 P) I7 L" \- a/ I- Z4 `
为了方便,会引入一个加密库:
  |+ j' W9 w! Y# F2 |2 D8 _const CryptoJS = require('crypto-js')9 |3 _; a# V- \1 g5 o, j  C! J
链结构定义
" S" v( v" T0 ~+ e. E9 l很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:7 E8 ]5 n' L4 k/ S5 O' r! i
按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算
8 _8 M5 Z8 M. Y' |; Z2 [# Q/ f3 f# g( X+ S
1. 起源块
$ W+ {+ i& B' ~) v; O. J1 a起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。' ]2 P2 t0 [# g5 u" {
class BlockChain {
) _( K' v  K7 m2 y4 Z7 v/ I  constructor() {
. P4 u" \) c' D6 T+ S* L    this.blocks = [this.getGenesisBlock()]
  l4 X2 E3 A9 b9 p6 y  }  Q, M3 N: U: G, p
  /**
. L. u% t2 Z, ^! g8 h. y( M   * 创建区块链起源块, 此块是硬编码
, x; A8 Z' M, i: ~4 v2 w   */* }4 _! @4 ]# ~6 `! ^
  getGenesisBlock() {9 i1 `! k- @7 H: O
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')- g: ~) |+ b8 P( o, `0 h8 m
  }6 M( L1 N0 c* Y/ x0 b
}  I3 p& O- S$ Q3 e/ X# X- h
2. 计算下一个区块  j: H) E1 @0 ^( H
BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。
8 o9 A' ]' F; M3 S// 方法都是BlockChain对象方法
% L  e1 N, _! K0 M6 R5 L# s; q  /**
' v- b. A7 Y. T1 |   * 根据信息计算hash值& k/ i2 w6 k, ^! q* [6 z
   */6 N9 ]$ }, L/ y6 T
  calcuteHash(index, previousHash, timestamp, data) {& x* Z+ V6 f$ E4 o( n; C4 Q
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''
4 \& H) O; v: ]+ z7 l$ D2 c" I/ Z+ q& K# E  }
3 @3 O  K  F2 w3 k  ?) q  /**
8 S& k9 U" g$ ~' @. I2 s+ X% h   * 得到区块链中最后一个块节点$ ~' b( `& T" q% }
   */
6 L! m2 t7 a! G6 O8 l/ t* C# v' g: F  getLatestBlock() {
' K% }; j0 @4 P. P, k8 ?    return this.blocks[this.blocks.length - 1]7 f/ T0 \/ w5 ^3 O+ W
  }
. e3 @3 V; S4 \' {2 z  /**
5 J' w* k$ o" E2 `6 h1 s   * 计算当前链表的下一个区块
: G6 ?3 f0 D0 L   * @param {*} blockData
# k( j. w( S8 f7 m   */. u- X$ v: o5 Y9 m
  generateNextBlock(blockData) {
  ~' D& v3 X9 l* M6 r+ _, j* Q) b    const previousBlock = this.getLatestBlock()
) `8 O4 X1 F5 v3 P    const nextIndex = previousBlock.index + 1) n; P4 P$ e1 I7 q6 x
    const nextTimeStamp = new Date().getTime()" h' ~/ P' L; D0 D
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)5 b7 f3 D  h4 V- u: V, ]
    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
/ N: o+ @& K* ^+ U, B: ?! t" w) V  }
7 f5 k; q% Z/ c3. 插入区块* I. a$ j6 V: Y6 p, V: Y
插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。
0 i+ U$ n8 ]8 k  /**/ D$ E! G% M6 E* h
   * 向区块链添加新节点/ x& x; w! z; b$ d
   * @param {Block} newBlock
* y. X* ^, o  E6 P3 e/ D   */0 }! K( A- Z: D0 s  ^6 D
  addBlock(newBlock) {
- H; V  {% ^; T0 a! R; v! N    // 合法区块
6 B, e* `# @3 U8 S0 `    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {/ A# x( I/ h. B" R! G, m% c
      this.blocks.push(newBlock)* l6 i; F" X0 t9 d7 F8 q  e
      return true  
$ A, n2 X+ ?' C! c9 ?" C! F  U    }4 i. Z; o5 i. N. b" \  A3 d
    return false
. A, }% H" b2 F+ I# ]$ n  }
( H" O  F5 @" e- y* S7 W9 Q检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:. ]4 c# |1 {; F' ^. K- }% W
判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成/ u2 L, D- y( [9 J# c/ l

" i* `+ ?/ Y7 [3 q- K, l  /**1 L+ @+ r! X3 _! t9 u2 ?
   * 判断新加入的块是否合法+ L0 {9 g! D% O8 e
   * @param {Block} newBlock
5 k3 S/ x) w8 [5 K   * @param {Block} previousBlock + w+ D- j; ]! g/ P; g7 u5 ?; |7 R1 L
   */2 F2 a; m: D6 X9 _& A  A. O( R
  isValidNewBlock(newBlock, previousBlock) {
1 e+ [# ?/ u3 T' w& c4 b/ b    if(' q+ U% C! J' T6 W0 X
      !(newBlock instanceof Block) ||
& i* H# V* u8 G  S% s      !(previousBlock instanceof Block)
+ i! x3 G0 u6 x: X- j$ V$ V/ Q    ) {) n0 k  k5 E; H' ], M8 M: q2 @8 F
      return false
. O! i& m7 d! r: u& X5 v) y: t    }
$ z7 k/ N8 F5 V  l2 o    // 判断index+ W7 `+ N2 z% g: T6 `8 B' x$ v3 g
    if(newBlock.index !== previousBlock.index + 1) {
: ^% a" u% }* _) t6 U4 k      return false
' q8 ]6 n6 K4 [! u    }
" w; B0 J* m0 l0 b    // 判断hash值3 F9 `& T8 i' T8 Z/ S  J. K1 @; A
    if(newBlock.previousHash !== previousBlock.hash) {
1 W5 V$ Z, ]) v; e      return false8 m3 A" w' v8 Y  g. k
    }: E8 N6 p8 c3 U" ~
    // 计算新块的hash值是否符合规则9 y. K7 ?& A0 B' e
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) {
3 J2 w: N9 `0 _  p      return false4 z* z: p& _6 _
    }
' Z  ]% |& B1 \  _    return true  {9 Q" t' o8 Y, ^. P0 f' K6 n
  }
; _+ Y6 ~$ m+ U& B, x/ Q  P% j4. 批量插入
# {2 w! F  y5 ]5 D0 j8 |% T批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。
. [# U) M0 P* U0 y4 W在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。* v% I: y& `0 ^3 }
但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链: f& m3 D5 m- ^) M, |" [0 {* x5 r
插入新链的方法如下:" v0 i% q. h/ y1 C7 ?2 y7 |  f0 L
  /**& K( M, H7 |: G6 z- c. K  `
   * 插入新链表
9 q) B6 o$ k+ Z* y, V- B   * @param {Array} newChain ) m2 N7 T0 D" Y$ }1 J
   */
6 d; b5 E# `4 V! j& z& g  addChain(newChain) {
6 O$ i3 |; ^/ m7 m! x" t4 n9 O    if(this.isValidNewChain(newChain)) {! p( j5 Y0 O5 L7 b6 X( n8 `( m
      const index = newChain[0].index. R* ?5 x% }2 P6 L! n" {
      this.blocks.splice(index)
% d! \4 g# A3 k: k, R7 I+ Y      this.blocks = this.blocks.concat(newChain)3 `0 D# @8 w: g" Y7 f2 y
      return true) D0 C) i- g. C& v
    }& E) E+ A7 C* ^, U) J+ E
    return false) Y! _0 J, W- t- C0 a) r5 ]
  }
2 W1 @9 v: z+ d. E  N8 I4 e  ^实现上面所述逻辑的方法如下:
* h) K. q& V2 y  /**
0 r( m# m6 k$ `/ G- s7 p# T& f   * 判断新插入的区块链是否合法而且可以覆盖原来的节点! |& z+ F- g3 D1 |- q
   * @param {Array} newChain
" x" |% K* k; v: @   */
* z! d  |. x, y5 W- N" Z9 x: {' Z/ p  isValidNewChain(newChain) {9 I" w9 M* ^4 d/ h7 D' j. T: W6 q
    if(Array.isArray(newChain) === false || newChain.length === 0) {
6 b* y5 r' J# i4 L% A% L      return false% l5 e) l( o7 l6 u; t8 k
    }: h4 d: f7 b, j1 ^3 J
    let newChainLength = newChain.length,4 q6 `! E3 C  ]3 u1 o4 Y# T
      firstBlock = newChain[0]
, Y: J  z% _3 {    // 硬编码的起源块不能改变. @/ h3 d, w1 s
    if(firstBlock.index === 0) {) N: ^$ f/ {2 H1 t! |
      return false: H) Z$ @: v- g7 n& O, v% G: S
    }
7 w; E2 y0 R# o, p+ w1 d& T, U    // 移植新的链的长度 0 U6 T" j9 F8 G0 g* Q& d5 z' G4 L/ l
5. 为什么需要批量插入?; ^7 o* V) w9 m
我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。/ S* L4 B' s5 s2 C% K  r, D) m
A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。
2 B6 j' a7 p9 Y. X4 i- {- a" H3 a而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。
$ Y* q* H: \  y' p! a( s效果测试1 Z; L( m6 t* b) z  k5 X
虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:6 v/ V0 C. `2 C* A- k. p

1 `9 X& ^: D5 ~$ a/ y红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。- L4 M8 [6 ?/ `  U1 j- u- R
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
标签: NodeJS
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

r8kao8k8 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    1