Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

NodeJS实现简易区块链

r8kao8k8
88 0 0
NodeJS实现简易区块链
$ e+ O4 U+ k* j4 P2 O! h: U0 c( N之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。$ \6 p& N$ E" {3 Y. x, R" o
但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。
$ a3 L" r+ a, O; L* U& Q想了想,就动手做试试咯。
* K  k8 v% z, V: d) m?查看全部教程 / 阅读原文?
3 G/ Z" r7 Q, `* e技术调研+ F1 g- k7 X6 W6 K. \  c
在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。* ^# C! }; _- s7 j
结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。  \; V1 q  I1 F. S
区块定义
# C% J9 x* L+ ^0 ]  F% ~为了方便表示区块,将其封装为一个class,它没有任何方法:
/ m5 i$ {1 C1 \, {, n+ w) W/**
/ }; G4 [3 N5 i2 d; U! z# w * 区块信息的结构化定义" S) h6 d* _$ z6 Z
*/, a  b7 ]' J) R$ r- o
class Block {
% z) M+ f' r$ K2 S  /**
* L9 t: A4 I3 ~3 m   * 构造函数8 t) t4 N* z4 w
   * @param {Number} index 4 ]5 V  C9 H- g6 D2 y3 ^+ v+ l
   * @param {String} previousHash & I- S% `7 f* b6 S0 M7 c3 x
   * @param {Number} timestamp 0 J1 ~- @, Q+ A
   * @param {*} data
6 M8 F% L/ l6 P' H9 z; [, D; J   * @param {String} hash
3 x$ z& j( P' D2 ]  E9 T   */
: W3 o. q; t$ W( }  constructor(index, previousHash, timestamp, data, hash) {
1 h+ V! W, I7 M  `; m    this.index = index // 区块的位置. z8 ^3 G7 n5 R3 L
    this.previousHash = previousHash + '' // 前一个区块的hash
4 w- y' [* b  l8 J/ U; Y    this.timestamp = timestamp // 生成区块时候的时间戳
! A7 O0 |, [- X& w. p3 z/ J) G) z1 j    this.data = data // 区块本身携带的数据3 Y* C' w5 R% w# F0 f
    this.hash = hash + '' // 区块根据自身信息和规则生成的hash9 U- I: M/ g+ {4 B  ^7 M. _- g
  }* t8 k( B2 N) z6 L
}6 r5 O  `; L7 G; H
至于怎么生成hash,这里采用的规则比较简单:2 [- f+ E: s% v% \( L. ?
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash
. Q1 \8 s( I* P7 c# r7 E
, w7 u/ |0 \1 ?7 ^- n& t; V为了方便,会引入一个加密库:( H* d+ m  j  j% F3 y
const CryptoJS = require('crypto-js')1 d+ i' T7 N- C: k  \
链结构定义# ?# s9 a( M& V: I' ?- c
很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:
$ _. j( g  x' y+ E' n! V2 f$ S" }按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算
! k. l1 y0 F5 ~) _
/ a6 o' i9 L, d- V: T* K1. 起源块: V5 K' f9 h2 ]5 p2 m  N" C; l; B
起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。
0 D  `# y/ n3 s( u+ r: Fclass BlockChain {
% t% ^( P; a/ a& d1 h$ v  constructor() {
  U4 s8 U. R- C+ k0 N2 p/ i' s    this.blocks = [this.getGenesisBlock()]5 r: m2 p& u+ p. p  f, E7 |
  }! _4 f' O& z- A
  /**' G5 y- T5 I  o* \0 R4 ^
   * 创建区块链起源块, 此块是硬编码" l: \* Y! X! q* z8 o
   */
: H9 s, t; B3 G9 @9 G' T4 I, x+ [5 P  getGenesisBlock() {0 i! M3 m" h: C
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')
+ ]' F* ^' z; T4 |% M+ Z5 V  }
% [/ Q- q" v; q" f- _; E}. Q' P; s8 r" y* b$ X1 E' Q6 A
2. 计算下一个区块
' f% I5 Y8 `6 ~; z4 O* Q( _, LBlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。# C, \! y1 |4 l6 ~
// 方法都是BlockChain对象方法
8 f- x$ H$ N6 q/ p7 Z$ \  /**
$ s( r9 E' U3 h   * 根据信息计算hash值, H" R  `, n( X8 j
   */' l5 A, d: e; i5 c- k. Z5 ^5 `9 W
  calcuteHash(index, previousHash, timestamp, data) {
- u; t: ]+ p5 t* L: a5 z& T3 k    return CryptoJS.SHA256(index + previousHash + timestamp + data) + '') a2 D3 ~2 a( {  i0 M( d$ E% i: u
  }
: K7 b, V: g: ]4 q$ m" }  /**
  {  j( f% E/ u8 H' l   * 得到区块链中最后一个块节点
- @6 {7 r4 Q, @) `. g4 c: R   */
  `$ ^: [9 W- F  getLatestBlock() {
% v2 u) k+ y- i( D" E9 Z    return this.blocks[this.blocks.length - 1]
$ Q& @7 F/ Q6 I  }7 g- R6 ]* ~* Y
  /**' l* W" I! r* J! m8 W; o7 V& ]' B
   * 计算当前链表的下一个区块) x# L6 S: ~- s6 z
   * @param {*} blockData + M8 r3 ^$ W& r* n
   */% r1 {/ y8 ]  `3 n( w
  generateNextBlock(blockData) {/ K: b* B; H, d
    const previousBlock = this.getLatestBlock()/ m* ^' w  {8 P7 o
    const nextIndex = previousBlock.index + 1' L( z( R6 K; x: f+ S
    const nextTimeStamp = new Date().getTime()
2 Q7 a/ v, Z! w# \8 U    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
4 Q6 E/ B. r, X) @* a9 [6 A; ?    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)
: b4 e( v6 W* q# u' G  }
  i$ s& o# c" a1 J1 W3. 插入区块
+ ~4 j  a" y  `0 m. ?插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。
7 R% s2 X9 @) F6 {  /**7 S8 K7 x% P& r. a0 P; S3 j. T
   * 向区块链添加新节点8 l; j9 n9 x9 x, F: {6 @: b
   * @param {Block} newBlock * p7 T* w3 E* V2 B
   */
* e' Q1 N, B; y# A$ j5 F  addBlock(newBlock) {; X2 x2 Z  g' K0 {% _
    // 合法区块
) j9 c- S' U/ o% v  X) G    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {( c( A7 S5 r5 k& |
      this.blocks.push(newBlock)
- F( M; [5 z) {      return true  
2 G1 k+ L2 ]" s/ c5 ?    }
2 G) f0 ]% O: A7 B4 m: A7 A    return false! b6 P% i* b% c) W- `
  }
" J! R& [* Z/ U- L8 R2 n5 M% B* M; ^检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:
3 [  u8 x6 I  S" }( z判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成
& u4 }4 V% f0 @: \" h6 W$ m% G% n1 I/ Q& K9 e2 n6 p$ m
  /**
/ D+ i+ c2 w2 y: Y   * 判断新加入的块是否合法
+ d6 Q& h+ Z9 y  V8 A9 T5 G   * @param {Block} newBlock & W; D4 ]+ T  D  p. r: E8 y& C
   * @param {Block} previousBlock
2 E5 V+ p8 \6 K- l) o1 S   */
4 ]7 b. k6 T% q) h: B) r  isValidNewBlock(newBlock, previousBlock) {
  n; L/ `" P  [1 S/ J    if(
1 X" C# Z% m5 A. p4 F+ B+ F% z      !(newBlock instanceof Block) ||6 D! u7 _% W7 C4 E- D' h; l. X
      !(previousBlock instanceof Block)
+ [: d5 v+ J0 u! ]9 m" a    ) {5 c1 V1 s! B3 ~1 a
      return false
  L; ~. C! ?: F% j$ `    }; p! H' |0 u9 O$ y
    // 判断index
5 h3 }: o" g$ C0 V1 w' b    if(newBlock.index !== previousBlock.index + 1) { - u: K' P$ G+ a
      return false/ ~4 a. S  v8 w! N8 z1 s/ ?8 [+ J! G
    }
8 H& |1 S* l$ a4 P8 W; X" l    // 判断hash值* k+ C1 U4 U6 {( y: b+ c7 f7 s3 _
    if(newBlock.previousHash !== previousBlock.hash) {
8 j; u1 b2 X1 E4 O/ @$ Z% M      return false
- z2 y6 s+ |+ d: V9 d3 c5 }    }& `' O$ y& t. o; ^. x( O& Z0 n
    // 计算新块的hash值是否符合规则) x4 ~( P5 @  |" J% s9 m# f. Y6 j
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) {
6 U  S& |! C/ ]9 k/ S      return false
- ]& c! @% f3 c$ q6 x- n8 }    }
: J& Y- x) r( `    return true  ~: @8 y$ `& k8 `. W6 ~+ X0 o; A
  }
* L1 ?7 J- H( o; m) a; x4. 批量插入
' R  p  s2 h1 z* b" e. @, m批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。; z1 X/ ?8 b! K' a2 s
在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。9 c7 v, N! ~3 y  J$ F5 [- E
但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链
/ }* \2 }3 |2 s8 C插入新链的方法如下:% i5 l+ ^9 d5 T9 b& Z% o  v
  /**
  s% P  C5 ~. A& r+ c   * 插入新链表0 c  x" j* n4 e7 D8 K- I
   * @param {Array} newChain
3 n% z( `; h  ]0 T/ r   */
3 Y) \! m! k2 E' P: C) g+ m  addChain(newChain) {1 S, H; ~9 D+ o" k; y# X  w  s0 e
    if(this.isValidNewChain(newChain)) {8 o3 I- b4 [- E) P$ O5 Z
      const index = newChain[0].index
( o, |$ [9 ~+ J) C) |      this.blocks.splice(index)
! A/ b8 ]3 `; n, X) \2 S      this.blocks = this.blocks.concat(newChain)2 ?# ?3 _9 f2 s1 y
      return true
( H# f7 Q% V$ v$ [1 A) r) K' M& x    }; P) I! a8 F/ v+ r; y( w
    return false6 S  @3 A/ E9 _, [  e
  }
1 c7 ^/ d0 ]  q; l7 {: F, K3 a实现上面所述逻辑的方法如下:
0 W; M$ a* R* p( r: u- ]$ O  /**
3 Z" z8 c1 b: q1 A  k7 p$ f( {   * 判断新插入的区块链是否合法而且可以覆盖原来的节点
$ [) R& o# b' R, ~. V1 z   * @param {Array} newChain
  J9 V5 v# Q" w: l- G' I   */: j9 L8 j3 U" t* w8 J0 P
  isValidNewChain(newChain) {
- I8 |# \% v/ t2 p1 R7 S! i1 f    if(Array.isArray(newChain) === false || newChain.length === 0) {
# w& b- A4 ~* [* Y: J6 P( d& A      return false, n  }% I: Y8 B1 ]: X
    }# |1 b2 X# ^' B+ ]: G4 ~
    let newChainLength = newChain.length,
1 p9 p9 U* n9 j$ s1 w: k  P% ~      firstBlock = newChain[0]7 M. ]* h7 |" a  l' M! N
    // 硬编码的起源块不能改变- j( {& ~, z! ~4 _' {1 y! o
    if(firstBlock.index === 0) {4 N4 T* M0 L: Z! ~; m/ s
      return false6 m8 M+ L8 ^- k$ p' @$ V  b- M- B
    }$ h7 x; b( u( |9 T/ |
    // 移植新的链的长度 $ M) R' @3 m$ r- d7 s% |, S: H
5. 为什么需要批量插入?
! I' M# a, G1 [" m6 Q1 C7 S我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。
& ]" G+ q+ F! `6 YA与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。
) N* X3 `* t1 h# W) b0 v而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。2 @5 l* ]5 d1 p: r# ?8 W
效果测试
0 Y+ j- V: M1 {  `1 V% x/ {/ l/ }虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:
7 ]/ R" ]/ h+ {0 I4 ~9 R' G* [. z3 j' y) {* B8 w* c! I
红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。  r( V! o+ ?. d
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
标签: NodeJS
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

r8kao8k8 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    1