Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
109 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。# W: V1 I' X0 p' G! k
3 S% }+ O* a% r! S
3 A7 g4 ]  L& }
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。, U8 _8 l+ U0 W# P' ~
* I) ?2 d4 U# o5 k( M- M

! ]/ s: N1 V* a4 p  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
' c* d2 ^- c5 z, t) h" |+ G$ {- N1 x  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。
, [6 D% Z/ ?* B2 v" T8 B
8 z+ N" h9 H- Y: {% c- j2 E  A! o
  区块链网络
: K5 W' q( y5 ^9 U7 o  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。. }. M9 V# p& x1 Y* [8 H& t1 n
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
! N0 `/ A& S. F5 {+ j' c) ]  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。/ B' `# [5 {. C/ n

0 {: _, Y3 e1 t1 H/ _* P6 w* s9 d& K2 D+ A: y. F
  节点角色
2 Y" C. b4 v9 T  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:2 y4 L( |# }4 c
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。& Y+ m7 [6 H& d. N7 v+ B
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
0 {9 N) u- h/ x" B1 ]  U! s  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。$ L+ D+ P; d, r+ i/ `
- X: K4 d6 t! }+ s2 H4 ?) {5 \9 O

& ]7 Z! O" ^! f: h2 q1 M  网络简化
1 w' c; f% N6 V7 F  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。0 @- R; N) X9 {; j- F; T" d
. Z; C1 W0 y; g* ~' O9 r3 K& v
  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
3 M; y) D9 i/ s+ K! w  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。7 K& [! J; s2 F; w& i9 G
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。- f$ Z5 A2 M$ a
$ ~% a" O0 r; w1 _; X+ i
2 N( k# W+ t  @8 e
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
$ K3 T& B+ H* l1 ]. Z; E1 K9 m
8 ]; y6 e0 Z* R- z! o* O4 H. Z4 t( U7 t% z
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?
3 w% q8 M' i% W  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。: ~* O  g4 V$ q/ ]0 U# V
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:8 R6 c* Q: O& V1 r9 M
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。, V2 q6 a1 y/ Z, o: F: _
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。0 w$ P6 w1 A% ]# \  m- |8 [
  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。7 o% c7 c9 a' d: o: v
  场景0 D; q% P% q- a! L* b
  本文的目标是实现如下场景:
2 X) u3 K- h8 o0 Q1 |7 k- Y, _" Y  中心节点创建一个区块链。
5 Q6 {+ ^/ S* O* c  一个其他(钱包)节点连接到中心节点并下载区块链。
2 P! }' c8 G! d+ ^- |; x, \  另一个(矿工)节点连接到中心节点并下载区块链。
2 d9 j# M1 ^5 e+ B6 F/ i  钱包节点创建一笔交易。; n- d! s! x5 R" ~" z# @+ I
  矿工节点接收交易,并将交易保存到内存池中。# H* L4 b7 h# ~3 g5 V
  当内存池中有足够的交易时,矿工开始挖一个新块。
) J; a7 ~, f; n4 V0 B" v/ w0 M4 X  当挖出一个新块后,将其发送到中心节点。0 Q& L- [8 ~% R0 A( |3 s
  钱包节点与中心节点进行同步。8 _( _% q0 B. q) s2 ~2 L7 J% I6 B
  钱包节点的用户检查他们的支付是否成功。+ P) C/ N1 f7 x% @
  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。
) r& X8 t! ~) E9 }7 B1 N5 i  版本: E$ O! v/ B8 z3 g3 O& Q. N, M% m# K
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:; _; [' ]3 d4 X$ c
  1.   type version struct {& z; m  h, ^- E  C* `% e: m2 K
  2.   Version int
    7 d% K" C0 f: U, V. i4 Q6 [
  3.   BestHeight int  F0 X; o" m" c' U1 i5 ^
  4.   AddrFrom string}
复制代码

' m4 }( H% o# m9 z9 q! R  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
6 _% L  I* F) H, `  W- G  N  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
2 M( V8 a/ |  U- m5 v2 U& N3 E3 ?  为了接收消息,我们需要一个服务器:
* ^5 D: l* E- B  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {; B  a4 j4 D$ l
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID): ^; s) g8 z2 m: n: t  \! L
  3.   miningAddress = minerAddress
    : ]9 s$ }6 t4 @
  4.   ln, err := net.Listen(protocol, nodeAddress)
    # U" A$ U3 {6 ?( Z# l% r
  5.   defer ln.Close(). B- f+ z% C4 {- w3 m/ r- f* \
  6.   bc := NewBlockchain(nodeID)# x, }& _  v7 F3 D- e
  7.   if nodeAddress != knownNodes[0] {
    * q, R* t7 \8 K0 q) _: S
  8.   sendVersion(knownNodes[0], bc)3 O6 I& G/ `5 ]* U' d6 ^% W
  9.   }
    8 d  l. W' h+ \; U4 E" ~9 r2 N
  10.   for {. S2 H. M2 x6 B' [4 C
  11.   conn, err := ln.Accept()+ K3 s& |6 f# V! k2 }
  12.   go handleConnection(conn, bc)* H7 k* [) v- o4 L8 P
  13.   }}
复制代码
; U& _" ?% G; v& F5 e1 [6 C9 W
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:* Z7 d0 r  u+ ~/ K' Z. W6 a5 L
 
  1.  if nodeAddress != knownNodes[0] {
    # W* \/ R4 j" o: i# \- q9 @
  2.   sendVersion(knownNodes[0], bc)}
复制代码
6 b1 i  L0 q7 d& f+ W
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。- N9 k, B; M4 N6 x! g" ~& L. j
 
  1.  func sendVersion(addr string, bc *Blockchain) {  |& j4 s# T! D3 j( c
  2.   bestHeight := bc.GetBestHeight(). w0 l2 P1 x; `2 ?, Q
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})- S, N4 T2 Z$ d% ?8 G, ]" ]. z
  4.   request := append(commandToBytes("version"), payload...)+ j# u9 d$ L  g7 @) F
  5.   sendData(addr, request)}
复制代码
' A" D9 F" \9 s8 N" i
  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:" x( G! e: [2 K# B) p  Q) R8 U
 
  1.  func commandToBytes(command string) []byte {
    1 x7 N# v9 S" |# O( u/ D* S
  2.   var bytes [commandLength]byte8 b2 f, z  p2 }  p! K  L; r
  3.   for i, c := range command {/ C. r8 \" I7 v+ B
  4.   bytes = byte(c)8 c" }; A0 D, ~5 ?
  5.   }
复制代码
0 u# ~* t) w8 o4 ?9 a4 k6 f
  return bytes[:]}$ J. f+ ~% P+ |5 y) k2 u% }
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
3 `: V* D0 p: k  
  1. func bytesToCommand(bytes []byte) string {/ E3 K2 n; A! q8 ?; C. E
  2.   var command []byte
      V1 r2 _5 @! V2 I" x
  3.   for _, b := range bytes {9 D+ i4 X- _  v8 V4 k# ^
  4.   if b != 0x0 {" H7 k& |5 ]& v. E8 _+ O0 f* v
  5.   command = append(command, b)
    * F# n: t0 ]+ N5 f
  6.   }/ B/ W- q4 J0 M, m% {3 }  _
  7.   }
    ! d: s7 \6 W8 D8 g; j$ U
  8.   return fmt.Sprintf("%s", command)}
复制代码

! N( [% D+ |/ I0 R7 R7 o& ?  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:% Z9 E  R% N) F, s( c0 e
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    8 C9 S2 ?* W' s) b8 V5 ~* J1 G. D
  2.   request, err := ioutil.ReadAll(conn)+ Z! f. A0 g* P9 [
  3.   command := bytesToCommand(request[:commandLength])0 A; i( g) W2 o: W
  4.   fmt.Printf("Received %s command\n", command)0 }3 f2 v% u" h( Z  U: a! y; c+ r5 i
  5.   switch command {
    9 [" j0 s7 h# u- @) U! e9 {7 m
  6.   ...1 M* y- ], I3 \5 ]; `
  7.   case "version":
    " Q& b6 Q2 ]' ]0 S3 m- g/ R8 _
  8.   handleVersion(request, bc)
    8 q/ d: k6 ^! w/ l
  9.   default:
    - {& h$ x0 J. S3 m$ @6 a
  10.   fmt.Println("Unknown command!")! V; ~! D" I# O5 b
  11.   }( t! A/ y( `: r) L; D, o
  12.   conn.Close()}
复制代码

: k% J- |( n; ?, j& w4 D  N4 v  下面是 version 命令处理器:+ g1 h0 H& j- ~4 C8 N
  
  1. func handleVersion(request []byte, bc *Blockchain) {3 {! w5 G2 M- R6 {
  2.   var buff bytes.Buffer var payload verzion" _/ B* W, Y' v: z
  3.   buff.Write(request[commandLength:])6 x6 Z. M- j$ s5 p4 N! V, f" c3 S
  4.   dec := gob.NewDecoder(&buff)
    % h2 h! t! \% ~5 z% }! c6 v( ~
  5.   err := dec.Decode(&payload)
    4 I, v# A8 T2 L4 I" B
  6.   myBestHeight := bc.GetBestHeight()
    ! n" f1 T# W3 a! q4 c8 H0 X; k
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {/ X7 e4 |, a! i
  8.   sendVersion(payload.AddrFrom, bc)
    ; K8 j# h' w8 ?! d
  9.   }
    - L& y  I0 `2 b" n
  10.   if !nodeIsKnown(payload.AddrFrom) {& d5 _. w1 b6 O( j7 f
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    . E8 B' x* U* r1 b
  12.   }}
复制代码
, x; Z0 V1 k; i
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
: P* c+ H  |7 o% P+ v- i1 T- I5 s" k! i$ D4 `
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
5 W2 D9 ]& G3 b0 w9 J( `3 C+ ^0 O$ G6 i6 l9 u& ?. q( T- T

5 a  ?, V* }! I$ Y: M: f* e
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10