Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

NodeJS实现简易区块链

r8kao8k8
134 0 0
NodeJS实现简易区块链
3 [: c8 J, w+ f: K& I: [之前由于课程要求,基于Nodejs做了一个实现简易区块链。要求非常简单,结构体记录区块结构,顺便能向链中插入新的区块即可。
; _( W6 I$ m# {7 v但是如果要支持多用户使用,就需要考虑“可信度”的问题。那么按照区块链要求,链上的数据不能被篡改,除非算力超过除了攻击者本身之外其余所以机器的算力。2 g% ?3 ?: ?- c, J
想了想,就动手做试试咯。
* P: Z  E! T8 j# T" t?查看全部教程 / 阅读原文?: z% b, W, C3 _& E
技术调研
. T6 D; F0 f* m" F! }6 M. D  U4 H在google上搜了搜,发现有个项目不错: https://github.com/lhartikk/naivechain 。大概只有200行,但是其中几十行都是关于搭建ws和http服务器,美中不足的是没有实现批量插入区块链和计算可信度。, J* W5 |) F9 D; H0 `; d
结合这个项目,基本上可以确定每个区块会封装成一个class(结构化表示),区块链也封装成一个class,再对外暴露接口。* ?& m7 Z4 r/ X) E1 b
区块定义
! n* K# ^2 G% L' i0 w) G" n为了方便表示区块,将其封装为一个class,它没有任何方法:
5 _* w" k7 p9 Q  }( h/**! `' ]# p5 x( @
* 区块信息的结构化定义
6 {5 P$ k+ U3 _ *// k5 W2 N/ A1 |6 _' c
class Block {+ @: A. @% I7 B! U( t
  /**5 J5 O! ~0 k! P' [
   * 构造函数; _* p' B! t5 U) s, M) M
   * @param {Number} index 6 D7 n, _- V* o6 L' T" T! j; c
   * @param {String} previousHash 3 H# f2 v8 ?% D
   * @param {Number} timestamp
6 t6 i( q7 b7 F& {   * @param {*} data 7 A* \- X* P7 @2 B
   * @param {String} hash . ^- u+ G- b3 i% b
   */
4 P7 P( ^, N8 a) b2 K. K2 i  constructor(index, previousHash, timestamp, data, hash) {& H! A7 r! ?' A7 P: f/ w- d
    this.index = index // 区块的位置* Z" w1 n' n2 r
    this.previousHash = previousHash + '' // 前一个区块的hash
! r" \% n- ]' Q& \    this.timestamp = timestamp // 生成区块时候的时间戳2 G5 E( c# R/ b  k
    this.data = data // 区块本身携带的数据
/ P" F7 i! O8 n! F0 H, U    this.hash = hash + '' // 区块根据自身信息和规则生成的hash
6 Y; ]$ z1 d( Y5 E' ?4 q2 ?( r  }' O- _+ m/ n9 E, ]' Z% |3 {& \
}' J" a( T$ M6 @
至于怎么生成hash,这里采用的规则比较简单:  T, c6 v& T7 Z7 P' \
拼接index、previouHash、timestamp和data,将其字符串化利用sha256算法,计算出的记过就是hash) A0 B% A* Y! l
. |: j7 x% T  ]. ?2 J
为了方便,会引入一个加密库:+ a, J2 x" `& ~, y- P
const CryptoJS = require('crypto-js')3 k; z, Y$ ]1 a8 |
链结构定义$ n. z* W8 q8 F
很多区块链接在一起,就组成了一条链。这条链,也用class来表示。并且其中实现了很多方法:) F# q) z6 t8 m: W& }
按照加密规则生成hash插入新块和检查操作批量插入块和检查操作以及可信度计算3 J6 c# z( H- j- P  w

( K4 ?. [7 X$ E5 Z1. 起源块
5 K  a4 T, n) A5 q  W& v# h. O5 Y9 Y起源块是“硬编码”,因为它前面没数据呀。并且规定它不能被篡改,即不能强制覆盖。我们在构造函数中,直接将生成的起源块放入链中。4 R. u7 y7 T6 ]4 b. p, |
class BlockChain {
' W  R- N" _3 C4 B+ \" [  constructor() {
3 i* p+ d# m8 R" F* J7 j* j# r    this.blocks = [this.getGenesisBlock()]% L9 y# W9 u3 T1 h  C5 Q
  }1 S: w' N6 ]5 _  F8 e8 L+ X0 y
  /**
" V6 K9 ]6 v7 Z, d/ j   * 创建区块链起源块, 此块是硬编码' b/ t8 N+ b1 q7 n1 K. t1 x
   */: \! \2 M8 A. u6 T+ U& r8 }: g0 y
  getGenesisBlock() {0 P' K( v2 T% v2 S1 L4 h5 x, f
    return new Block(0, '0', 1552801194452, 'genesis block', '810f9e854ade9bb8730d776ea02622b65c02b82ffa163ecfe4cb151a14412ed4')& X4 x! J$ D# Y/ [& ^3 z4 j
  }
( o, y# L9 B+ P( f4 n) l& O}
# J, z8 s+ r$ G* `4 C2. 计算下一个区块( u- \6 w7 N3 ?* W" P& N/ r
BlockChain对象可以根据当前链,自动计算下一个区块。并且与用户传来的区块信息比较,如果一样,说明合法,可以插入;否则,用户的区块就是非法的,不允许插入。
& E" j7 h3 q* p' M$ X5 ]// 方法都是BlockChain对象方法9 a8 t, U, Q% p8 R
  /**5 _9 E: q: T4 z0 Y, u- c
   * 根据信息计算hash值  T2 s# p! E  j$ S( N. Z* [
   *// q2 y9 R& A9 [5 o8 s
  calcuteHash(index, previousHash, timestamp, data) {: @3 [% l, m) Q9 c  Y
    return CryptoJS.SHA256(index + previousHash + timestamp + data) + ''
( N! N9 U9 i0 G6 ^, S' M  }8 E9 P# e! ^  M: H5 o
  /**) g! q4 [& ^' \4 o. m& m9 s; d
   * 得到区块链中最后一个块节点
- ]& L1 N% x; @   */6 V9 u! i/ B" f$ A& ]% F8 k( P
  getLatestBlock() {
( P+ o% x9 w, Z. |" H3 K    return this.blocks[this.blocks.length - 1]) D4 W) c( B% u8 n1 m
  }- a5 O  Z; R+ e
  /**9 A8 l) {  ?9 G% w4 y  R
   * 计算当前链表的下一个区块
- ], a( h5 k3 M& L0 [   * @param {*} blockData " I3 d# e- g+ s. e
   */8 ]% Z  f6 K" j; s; c: |6 |
  generateNextBlock(blockData) {8 X2 b' N: G2 m
    const previousBlock = this.getLatestBlock()
  n  b' [% _  e4 z( }3 @- }    const nextIndex = previousBlock.index + 1* d1 [5 o4 [$ ~4 N- K
    const nextTimeStamp = new Date().getTime()3 S( n# d- q) d2 |- ^1 f, a
    const nextHash = this.calcuteHash(nextIndex, previousBlock.hash, nextTimeStamp, blockData)
5 m0 E, Z) i: \    return new Block(nextIndex, previousBlock.hash, nextTimeStamp, blockData, nextHash)% Z5 O, Q8 {/ P4 [3 B
  }
$ M4 l) M# s  A5 F& A4 v) A3. 插入区块
( ~6 g9 w3 v0 h9 q% a" h插入区块的时候,需要检查当前块是否合法,如果合法,那么插入并且返回true;否则返回false。
/ ~3 W5 H! o- H6 r! _# M  /**
# i+ o7 n4 q# E0 ~8 r. t   * 向区块链添加新节点
& `) F$ k$ i* x, @/ {- j; N   * @param {Block} newBlock 6 o  ^' x) q% P, {7 u# J& Z$ ~
   */
; c, I" p3 r1 g7 S  addBlock(newBlock) {, @" k! C: k* u9 }5 v
    // 合法区块
0 Q" l- U& H+ e% w6 ]% d0 ^    if(this.isValidNewBlock(newBlock, this.getLatestBlock())) {; ?* Q' n. Y- R* A. ~/ b
      this.blocks.push(newBlock)2 k4 t. h1 ~: r: U9 l! K7 Z
      return true    G5 E' S. b2 O" o( g
    }
& U* I! j" }( |- ]$ T0 M- W( {    return false
2 M0 Z! u: _; n0 p& W2 `( b4 N  }
+ r6 h4 `0 r, D检查的逻辑就就放在了 isValidNewBlock 方法中, 它主要完成3件事情:
' u7 S0 G5 c6 c& @判断新区块的index是否是递增的判断previousHash是否和前一个区块的hash相等判断新区块的hash是否按约束好的规则生成; U& t0 K7 m* g

1 [7 _0 ^0 r, q1 P  /**' Y  B$ i# Q8 @3 ?6 \" e0 H
   * 判断新加入的块是否合法7 e' j& _# ~: Q. I' i
   * @param {Block} newBlock $ `5 W& y/ z8 K
   * @param {Block} previousBlock " n3 q1 b7 w* G
   */
, R6 H, H6 S$ L2 d9 X6 H  isValidNewBlock(newBlock, previousBlock) {
0 T5 b2 z6 ?; Z3 f: ?6 B    if(
! X8 O3 s9 x9 V: x2 I. J      !(newBlock instanceof Block) ||
, c8 E3 b: V0 S9 g8 m1 }! O  _      !(previousBlock instanceof Block)
: C; n3 Q  J, M- {    ) {8 v: o0 E) w" x$ a: N
      return false
6 D: \5 S# C- S    }
) U8 j; o" W6 V5 X! U    // 判断index
" b& \7 d6 I. V5 h    if(newBlock.index !== previousBlock.index + 1) { # _/ M2 A) z* ?, H- K; F+ ^4 c
      return false* \* R+ _6 O& p1 e
    }
( d/ g4 p' T9 k9 k$ O    // 判断hash值
5 O6 K2 |. l; T    if(newBlock.previousHash !== previousBlock.hash) { # O, B7 g3 E1 o
      return false) c6 D) A. e4 A) b; x, h
    }- i1 z4 |. U0 _9 b2 ?* ]; T: L
    // 计算新块的hash值是否符合规则: @/ u- O+ j8 Q$ ~2 v
    if(this.calcuteHash(newBlock.index, newBlock.previousHash, newBlock.timestamp, newBlock.data) !== newBlock.hash) { 0 B+ o" s) T7 i2 N% Z' f, v, }' w
      return false. g2 I) c1 p% R; l8 t
    }6 x3 X1 R* p% g1 T8 k4 b6 O
    return true0 X/ {& Z' _' ?" }! l+ _5 S: M
  }
* w# N; h) I* H* B( T7 _6 \7 a1 I4. 批量插入" W6 ^  v6 N2 R0 |/ O
批量插入的逻辑比较复杂,比如当前链上有4个区块的下标是:0->1->2->3。除了起源块0不能被覆盖,当插入一条新的下标为“1->2->3->4”的链时候,就可以替换原来的区块。最终结果是:0->1->2->3->4。5 {- ]! G7 O- Y+ q+ d
在下标index的处理上,假设还是上面的情况,如果传入的链的下标是从大于4的整数开始,显然无法拼接原来的区块链的下标,直接扔掉。% N" ^& N( l8 a# Y0 g
但是如何保证可信度呢?就是当新链(B链)替换原来的链(A链)后,生成新的链(C链)。如果 length© > length(A),那么即可覆盖要替换的部分。 这就保证了,只有在算力超过所有算力50%的时候,才能篡改这条链
$ L( I4 J; v% W# A% ?8 C插入新链的方法如下:. i" c0 U+ J$ A/ S( i% ]
  /**
, F+ a9 L4 p) S7 c   * 插入新链表: e# [+ ]8 v: @) \# h
   * @param {Array} newChain   p! g: A5 p* P# n
   */
, N& h2 P0 y; a" L: c  addChain(newChain) {- e4 ~- P0 c( K
    if(this.isValidNewChain(newChain)) {
7 S% u/ f1 m, v4 J      const index = newChain[0].index0 z$ x+ y% H( m. |
      this.blocks.splice(index)
: Q3 a+ [) v$ \; E      this.blocks = this.blocks.concat(newChain)
1 i; ]7 ^0 W  g) @) f      return true
' F$ h( W$ N8 k. {( E0 M0 a3 X    }
: o. ~8 `7 j% e    return false' ]1 D7 F, a1 Z  D5 J
  }
3 n8 F, u% d! J: h$ [实现上面所述逻辑的方法如下:  L9 U* b1 {. B6 y" ?1 `
  /**/ r7 b$ l$ O1 V3 \4 t/ @2 Z" Y
   * 判断新插入的区块链是否合法而且可以覆盖原来的节点5 o( j8 j, }2 P0 s( m& C" |% y0 W
   * @param {Array} newChain
$ w# G+ X0 ~; B  l; q4 j: d/ D& i: g   */: G( L$ {( h- J4 O# W; e4 y
  isValidNewChain(newChain) {! u+ n9 `4 p6 {; v
    if(Array.isArray(newChain) === false || newChain.length === 0) {) h/ S; S+ i4 t* x2 t& h- ], ^  ^
      return false
& \: d, b! h: Z    }* R, K! \2 M7 b8 m6 B; z1 G
    let newChainLength = newChain.length,
- v( F  S! N5 H, ]- D  b      firstBlock = newChain[0]
& b  R# r3 T$ k    // 硬编码的起源块不能改变
3 H2 [% s4 \, [8 \* R    if(firstBlock.index === 0) {* j& T2 M1 @  o/ u. W
      return false- U/ A. s! W' x9 ~6 ^
    }, c8 C* e; V' Q# k! x6 o
    // 移植新的链的长度
5 M2 x& k5 S: ~7 f' D+ h0 y5. 为什么需要批量插入?5 G- @0 A: Z) _: |/ M+ B8 s8 i6 m
我当时很奇怪,为什么需要“批量插入”这个方法。后来想明白了(希望没想错)。假设服务器S,以及两个用户A与B。3 Y% j, `' P  b* X7 R5 G8 J
A与B同时拉取到已知链的数据,然后各自生成。A网速较快,但是算力低,就生成了1个区块,放入了S上。注意:此时S上的区块已经更新。2 A& S3 y5 `9 w- m1 a8 T3 l
而B比较惨了,它在本地生成了2个区块,但是受限于网速,只能等网速恢复了传入区块。这时候,按照规则,它是可以覆盖的(算力高嘛)。所以这种情况下,服务器S接受到B的2个区块,更新后的链长度是3(算上起源块),并且A的那个区块已经被覆盖了。3 X, Q: S, j3 w5 n$ X
效果测试3 W2 H" y8 [) U# P
虽然没有写服务器,但是还是模拟了上面讲述的第5种情况。代码在 test.js 文件中,直接run即可。看下效果截图吧:7 B! i. D7 X' O
) s/ s' Q: {1 r7 A( s
红线上面就是先算出来的,红线下面就是被算力更高的客户端篡改后的区块链。具体模拟过程可以看代码,这里不再冗赘了。1 {8 j1 M0 Y0 M% e  I$ e& ~
全部代码在都放在: https://github.com/dongyuanxin/node-blockchain
标签: NodeJS
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

r8kao8k8 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    1