Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
156 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。& Y) |/ Q" Y& d$ U$ |
! Q, ]6 m- L! d! P
  E! l! g" e8 J, K7 I$ P
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。
2 B/ X8 b+ K% H0 V: j, Q5 D
% ]" X% u& q9 W) j( _* A2 f7 F; H# c  P: x* d+ ?" D) C
  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
0 N* E* Z  w3 A9 [, }  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。
/ a$ v  w5 c# I$ j
  B# D5 a1 m3 Y! G/ |' x
0 ?( _7 e3 U5 Y  区块链网络' S) ?4 g% R5 @6 [. x' ]' O) I3 v7 \
  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。3 Z0 P9 N' a& m- s
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:  x4 s; j3 D. C
  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。% d5 I" {+ P6 \9 {# S

2 A6 X* B: n3 c% i; {( A" l" k$ t9 O; Z6 M1 h9 U
  节点角色: x- }: {2 H& ~: n  D4 n. B' m0 X
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:( a" ~. g8 ^6 U( d+ ^2 ~% M1 F
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。) p0 ~( o3 y4 v5 A
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
. i# x# D9 e- d) l& J  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
; m! z( H, w+ h3 c9 e" s
# p6 w- c5 _- ?& S7 r4 U9 V1 |* U; e& B0 x! Z, S( j: ^) Q- r
  网络简化4 Q5 j9 Y* B( Y4 R# Y, I# i: v
  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
9 k, C) ]( N$ I* C
9 I+ w4 B3 K- |  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
( H6 s' g$ [& |5 P  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。, n7 e( N9 {# y! P  z
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。6 x) ?5 A0 r. q, X

- [' }* t! d7 J, L/ ?4 k2 T% {) V/ @3 C" `/ y( a/ \! v* l
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。. Q2 J) _, h* |( K! d# n

- j" x0 ?* v! M7 c# Z* e; k
; F. s! u2 q- X- U' k3 N  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?+ W- j+ D8 E; d9 h) G: Z) Y
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。# i1 a* e% s  q3 D
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
0 L( _$ J# p. p! q2 G5 x  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。' e9 h4 \- [8 N& W
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
% `$ b4 G7 o2 I9 f3 T  y  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。8 Y3 N, R. _- K6 G- D' r8 K. g
  场景
3 E3 k) J% S) Z0 U! t  本文的目标是实现如下场景:" c$ x: {# }, J
  中心节点创建一个区块链。, E" z! o4 h8 {7 Y/ M- \' E$ S
  一个其他(钱包)节点连接到中心节点并下载区块链。
; X8 p7 D" e  @1 u& g& ~  另一个(矿工)节点连接到中心节点并下载区块链。
8 \1 f  W. G5 p* f6 X  钱包节点创建一笔交易。/ Q2 T% i! [( f( I0 n' M6 X4 R
  矿工节点接收交易,并将交易保存到内存池中。
$ V; m1 G; U) Q* |- g) v- g0 w7 s  当内存池中有足够的交易时,矿工开始挖一个新块。. f5 L) [3 M3 \- w* q
  当挖出一个新块后,将其发送到中心节点。
: T2 _- d. D1 H, i; b" R* r  V  钱包节点与中心节点进行同步。4 V8 q; N/ s) C3 Y8 U/ P4 {
  钱包节点的用户检查他们的支付是否成功。9 y! g1 k# i8 `  U
  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。
7 J/ ]& o& p# K- S8 V  版本
5 y1 V1 C! w4 g, q4 ^1 ~  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:; \) j* O' ^% U$ M
  1.   type version struct {8 E7 O& n3 M' {* ?8 c4 O3 @
  2.   Version int
    ( t- Y9 {/ e' b+ i1 s4 p6 Y. C
  3.   BestHeight int- Z! I+ F/ z7 I+ N% R8 \
  4.   AddrFrom string}
复制代码
5 ^4 ]  M3 w; M
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
* H4 O2 m; v. E& e  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。* f. B( E1 s$ `2 F: {
  为了接收消息,我们需要一个服务器:
) i" b: I7 ~0 I0 n1 y& _7 a  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    + s, V" N* z7 a" Y
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    " l. |/ i7 k1 i6 z6 s; W
  3.   miningAddress = minerAddress
    0 \. E; t4 k5 C( |3 k! {" |1 ?
  4.   ln, err := net.Listen(protocol, nodeAddress)
    5 m+ w( V9 B- D; c
  5.   defer ln.Close()
    + [2 j7 t1 y6 T: C7 s' T
  6.   bc := NewBlockchain(nodeID)
    ; t+ k/ W: T! `3 R  i/ v
  7.   if nodeAddress != knownNodes[0] {
    + V6 G( U1 @7 e0 N) a# h" W, _8 N
  8.   sendVersion(knownNodes[0], bc). y) |; O9 X) R8 G! ^  [, e
  9.   }. T. ^& y$ j! c- ?; Y2 l% m' n1 o
  10.   for {  [. m# U9 C% j. k% a; q2 f
  11.   conn, err := ln.Accept()  `! t6 @; F+ I7 \1 p" q, K! T
  12.   go handleConnection(conn, bc)
    + s, L4 e/ e3 Q7 f! e& v7 P
  13.   }}
复制代码

) J: n: ^1 ?3 s+ }+ |3 \  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:( \) i: }( x; L
 
  1.  if nodeAddress != knownNodes[0] {
    ) V" c8 X2 I7 t# Q8 S+ F( z, A
  2.   sendVersion(knownNodes[0], bc)}
复制代码
. z7 w7 O; Q/ j
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。5 i. w& t' S3 `, I( @
 
  1.  func sendVersion(addr string, bc *Blockchain) {$ Z8 e1 l( I; B
  2.   bestHeight := bc.GetBestHeight()
    - o! B' y" [8 i9 X& a  B! N* J$ \' S
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})) ~$ Y- z6 p* W4 g4 x+ s% H. D
  4.   request := append(commandToBytes("version"), payload...)0 g2 r& Z6 i$ j$ F! @
  5.   sendData(addr, request)}
复制代码

. J' ?% V8 Y8 L! J  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:$ X& I9 j' R9 s8 I' w
 
  1.  func commandToBytes(command string) []byte {
    6 \9 r2 d! X1 A
  2.   var bytes [commandLength]byte+ O; R/ o: T. c7 [6 a+ @: ?% |* h* Q
  3.   for i, c := range command {4 d" I: L5 G. h. ]! D
  4.   bytes = byte(c)
    : S- n6 M6 }) b
  5.   }
复制代码

* C  \' t; @  |, m6 A3 h& |  return bytes[:]}
1 Y5 {2 z: v: s  ~! \  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
8 u* F# M- Y+ h. e  
  1. func bytesToCommand(bytes []byte) string {
    & I; ~8 d' G& V* t: U0 ~/ o6 ~
  2.   var command []byte0 s  K7 Y: a+ i6 y  o. r
  3.   for _, b := range bytes {
    1 `- E# ]+ [$ o5 B9 i! S
  4.   if b != 0x0 {& s4 I; B' o- y( V5 K8 m
  5.   command = append(command, b)
    ' x! D' Q- i/ D( c  X+ r" H
  6.   }
    1 V3 C1 |% Z4 u* B
  7.   }* K. P2 k) ]+ p% `; V$ y
  8.   return fmt.Sprintf("%s", command)}
复制代码
# D, X1 i& H/ Q% \7 W3 J
  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:1 l& o. ?( _+ L
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    ! C. Q# x$ G$ s& b5 l& g& E2 C
  2.   request, err := ioutil.ReadAll(conn)
    5 |7 ~" d6 [2 N% [+ G
  3.   command := bytesToCommand(request[:commandLength])
    ; `3 U; K# H' F: r
  4.   fmt.Printf("Received %s command\n", command)
    / D& A' s8 K+ S4 j* c1 K* j3 v
  5.   switch command {  _7 D) v  G0 `# i3 Q, M7 z
  6.   ...( R. J% s; [6 T2 l! Y
  7.   case "version":
    # l( `0 r; ?, Q# e% \. ?
  8.   handleVersion(request, bc)6 x9 u& r: V# G# e
  9.   default:
      s4 D% \  r. N/ G% K' s4 x
  10.   fmt.Println("Unknown command!")
    9 k/ B4 ]5 Y# L8 z/ Z8 c9 }
  11.   }$ v9 r/ P8 q$ F; ]
  12.   conn.Close()}
复制代码

6 F6 k, c: U( h! K1 T% t  下面是 version 命令处理器:" ~& j0 v3 h; O! S6 @. `( {
  
  1. func handleVersion(request []byte, bc *Blockchain) {; p* d: \! ^  B
  2.   var buff bytes.Buffer var payload verzion
    , w. j8 f! c  r+ Y9 _" ^
  3.   buff.Write(request[commandLength:])8 z, I9 P% ]" N4 O. O: r; d
  4.   dec := gob.NewDecoder(&buff)# q6 S+ p3 ~/ f9 }+ J9 u6 I& m
  5.   err := dec.Decode(&payload)/ D% i" s. i9 e8 f+ @2 |
  6.   myBestHeight := bc.GetBestHeight()
    . w4 @: d+ U; H
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {3 Q3 w. O+ h5 W- D, o0 l. p
  8.   sendVersion(payload.AddrFrom, bc)8 p+ k' e4 n# ]* v: F. F6 x
  9.   }9 S* a1 T5 N8 y, x
  10.   if !nodeIsKnown(payload.AddrFrom) {" ]8 E9 R$ ]% ?
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    + }& E* Z- n. {' ^- E
  12.   }}
复制代码
$ j9 s- q$ u5 I) \' z3 @
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。) X$ o0 g, f+ e( M; E7 V
5 y1 ~8 O0 X- {0 W- V) u. b
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
* A, ~5 ]( q0 ^: r- p; a6 V
& h1 |; G/ P' ]- M2 k& o' ?( y7 ]  m3 f1 X
) k7 C  M( Q" \& F; w$ X
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10