Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
107 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。
' p( X6 M: f) h- b% o5 t! K. a1 c. g% I) P- A- T
7 a6 [* U" B  x" y$ j7 H
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。% ^, H; K, D5 h' n$ }4 |& z

. U% K' Y% l  h3 k# @! O0 P! [2 n  o5 v) e5 d* k0 ?! g& ?
  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
3 @4 w* n0 f7 d: i  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。: O* o5 ?4 J/ g* K0 t
; `$ ^/ L; |& y, d

1 Y% X0 U4 s, V. i9 ]  区块链网络3 c2 e) M! Z0 V
  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
$ \# k% r$ Q$ p  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
/ j- R+ t; k7 |2 ]# Q3 g. i  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。
5 |+ |. a2 i* Q/ A6 \' H
) @; Q# q6 [# L7 J7 e
5 r+ X8 ]$ }" u. f1 k! H- F  节点角色
# V" N3 j. u/ z+ e& |; }  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:4 a8 M3 o* g3 ?3 M
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
6 K! x4 b) k* c  h; c, Z  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。$ O, f" w9 J/ \8 V9 b, _+ I+ I
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
& {% `# h: ^- u" U7 W& t  X. `' W
+ _6 I* S( n/ R: C9 e* \7 ~1 H1 c. e( n
  网络简化. X0 |2 _# x. A" t7 ^7 V
  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
  b5 d3 z& v. I( U! j  E8 O
0 D% U( U  }: |, v  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
7 v. k* N$ `" J4 i  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。5 h% P% W% \; _$ Z, ]0 [
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
5 f# w: U4 X$ L, r$ H
) R- V" V( J8 ^- i8 B
! b' d5 r( \- C& q4 G; X( P  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
4 ^3 |8 L& S7 L" ^2 c; @- @% T1 {
, W) u2 {# g( I0 P
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?2 P$ d0 L* [3 B/ J" y
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。0 N# }5 T9 X* g5 k7 ?3 T% {( [
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
8 \- s  j/ M6 L' {! B* ]8 u1 N  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。) w- h! p6 u: d# x, S2 d+ g
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。8 U3 _$ e+ C2 F3 ~% [9 S5 @/ s
  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
9 u/ V: ?& {1 _7 D3 G  场景9 s4 Y% L1 Y+ n, ~% l6 U- b+ b
  本文的目标是实现如下场景:
3 h0 A1 J1 {" G' q5 x" P: O  中心节点创建一个区块链。
$ l2 w: c6 H5 c( [- C- c9 d  一个其他(钱包)节点连接到中心节点并下载区块链。5 G9 C) E3 Q7 T/ K& q
  另一个(矿工)节点连接到中心节点并下载区块链。9 P+ y$ k$ s' r. M
  钱包节点创建一笔交易。
; g/ R0 j% P, d: ]  矿工节点接收交易,并将交易保存到内存池中。+ v, z9 {: K* f1 V2 O' `
  当内存池中有足够的交易时,矿工开始挖一个新块。
2 D6 G) E" n# _, @- K! ]  当挖出一个新块后,将其发送到中心节点。
- G! M4 q# e+ E, A  `; R( _* c2 E4 Q  钱包节点与中心节点进行同步。" p# w1 q# o7 C  E3 r
  钱包节点的用户检查他们的支付是否成功。
. O  C& b" r5 J4 V# k  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。' _( v" F! n5 ]4 _9 ^2 m
  版本
0 r# u  R4 }3 @# g7 n) w4 y  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:+ R2 D5 T# h: t9 I) O- }, E. l
  1.   type version struct {; u. g6 j6 O; T1 A2 w+ N; T
  2.   Version int
    ) M/ T7 w! |) m$ w# U
  3.   BestHeight int
    : f" }! e* W& @. E  s
  4.   AddrFrom string}
复制代码

6 M% o- y- j& x2 X- g' n' u  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。& i' U4 P% \" P
  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。* Q0 D$ M2 t! X* V$ t
  为了接收消息,我们需要一个服务器:
& H$ o; L7 c: u  t. G5 m  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {% a! F& A( U4 a1 P, a$ _
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    $ a9 L& y) d$ G" |: k* Z: r; {6 _5 k
  3.   miningAddress = minerAddress
    - _" O4 h  `3 Z2 h$ m' h3 J
  4.   ln, err := net.Listen(protocol, nodeAddress)
    ) Y6 I; B0 ^, F% \3 H8 ?& E
  5.   defer ln.Close()
    : m. {8 v3 m% w; c' X
  6.   bc := NewBlockchain(nodeID)6 `" e4 a7 }. P( ?. S3 {& q
  7.   if nodeAddress != knownNodes[0] {
      U- q7 ?/ r- t3 o0 ?( u! a. |/ y
  8.   sendVersion(knownNodes[0], bc)1 D) {- |- n0 o; S
  9.   }" u5 v: m- ^% _# [
  10.   for {+ X; j' D* N( H# L1 i
  11.   conn, err := ln.Accept()
    % Q. G. ?! |3 ]1 ^( G
  12.   go handleConnection(conn, bc)- F4 N- N9 G2 @
  13.   }}
复制代码

' s5 H7 A7 J  n& h% B  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:* p7 Z- b4 v, K& y: y9 V
 
  1.  if nodeAddress != knownNodes[0] {
    ; Q+ ^& c  o( p9 |
  2.   sendVersion(knownNodes[0], bc)}
复制代码
  l( D3 w$ D4 u& E4 W# |
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
; l; ^8 I$ z+ J8 R9 ]9 `% c 
  1.  func sendVersion(addr string, bc *Blockchain) {# A/ ~0 }; I3 Z# h# ^
  2.   bestHeight := bc.GetBestHeight()
    ( \. |- {* E% O/ k4 f1 ]
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})! z. g: F( ^' N. B7 p
  4.   request := append(commandToBytes("version"), payload...)3 t+ ?- K+ Q! v
  5.   sendData(addr, request)}
复制代码

2 c  ?8 Z# W% r  J3 D- @8 w  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:5 t" c  I. k& `  A
 
  1.  func commandToBytes(command string) []byte {
    1 m6 G$ k' h2 j! A% n4 S
  2.   var bytes [commandLength]byte
    ' w; z  ?# \3 M  ^
  3.   for i, c := range command {0 `3 ~/ f: h, {) v, s
  4.   bytes = byte(c)
    6 U$ l! w  Z( R! h( n7 ]
  5.   }
复制代码
7 M) W* E1 B  I2 m$ q
  return bytes[:]}# p; E$ A8 I, N& }
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
% I& b+ H  G' u: f- S  
  1. func bytesToCommand(bytes []byte) string {- p9 K4 g) s$ b6 {: f. e
  2.   var command []byte2 ?% k: j% K( o4 B9 a
  3.   for _, b := range bytes {8 I% O' S4 j. h! q0 @+ n, c
  4.   if b != 0x0 {
    , d$ q/ F0 p9 W3 K: _+ E
  5.   command = append(command, b)" [5 v3 \& E: ~
  6.   }
    % R. `0 C$ O; i; t
  7.   }: x- m6 H9 }: t1 v6 G) q+ k5 v* T
  8.   return fmt.Sprintf("%s", command)}
复制代码

" y4 ]( p2 g2 Z! n) X9 {0 m7 h  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
4 G! b- G4 h; z4 _+ i. T. D  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    5 K5 _# p( `; R% b8 E9 L
  2.   request, err := ioutil.ReadAll(conn)- P6 k' h/ D+ S2 A: A
  3.   command := bytesToCommand(request[:commandLength])
    8 N6 p* o0 |9 G- P8 I6 B
  4.   fmt.Printf("Received %s command\n", command)3 S) V: l7 K, V) O& x, K
  5.   switch command {8 C* O( P' f% S! a8 O" ^
  6.   ...- ^  F- o1 ~" T  [7 o
  7.   case "version":
    : G+ E2 T& d$ [
  8.   handleVersion(request, bc)
    1 k: ]% U) z/ g- y' R; [
  9.   default:
    $ m" r# U' M6 [" j  _! R
  10.   fmt.Println("Unknown command!")! v: t9 g- y7 Q& q
  11.   }
    # m: R9 Q6 q3 z2 D/ M1 Y
  12.   conn.Close()}
复制代码

1 |# L; ^/ U7 L7 m9 R: I/ g0 O  下面是 version 命令处理器:
  C" w7 u$ |3 ]5 m  j% `  
  1. func handleVersion(request []byte, bc *Blockchain) {& g4 Y2 Z( y4 C* r. I$ Z! I4 z
  2.   var buff bytes.Buffer var payload verzion& y( f: i" e" T' A7 Y4 d* _
  3.   buff.Write(request[commandLength:])
    & f" g, G2 |% o& y, Z
  4.   dec := gob.NewDecoder(&buff)( d/ }) t2 u2 A3 ^$ h  G
  5.   err := dec.Decode(&payload)
    6 S& F* Z' J! G+ ~* O5 ^/ `8 S
  6.   myBestHeight := bc.GetBestHeight()
    + c/ z' W6 `: C6 i- W: X
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {
    , y2 ^! G% o1 D/ i5 p  k* s7 r7 Y+ Y
  8.   sendVersion(payload.AddrFrom, bc)
    1 v+ O2 u+ A5 t, Z7 ?
  9.   }: X. z5 e8 `# H) x; t  \1 D
  10.   if !nodeIsKnown(payload.AddrFrom) {: s+ z; _, \' [. `- C  {6 i
  11.   knownNodes = append(knownNodes, payload.AddrFrom)# s' O8 I3 Z0 L; q: ?
  12.   }}
复制代码

6 d. q( b: R9 Y: g/ o8 j  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
( s5 F4 G, \( Q& B; @% ~5 j- o/ K& r+ y$ ]
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。0 B. T% M' H! I4 r. o* ^* e

6 V% @1 ~7 q/ g" H" Q, }" l+ N% b% v0 g2 m' y
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10