Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

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

9 H9 k0 r' \7 M7 P; k9 |8 x
, N* E  C, b5 I! }: ~* f  节点角色$ {9 s& h: Z# j- \+ m7 R$ J
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:1 J3 a# O9 g+ w2 B$ w
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
' J' l+ |# }# v  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。+ }: l* J$ q# y% f
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。& Z; `, t( `$ s: X% ?8 \" M

5 \6 u5 Z1 M& e5 J
( T; B( J( q, N/ N8 m, m' `( r+ ]  网络简化
4 l# c  \% `* P0 a$ E. b7 \  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
8 i8 C! G/ v$ n, b9 ?& E
4 D+ d% K: G* x! a* e7 Q) v  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。' ]+ B' L" ?8 L- x, K+ J- t! ?1 B
  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。
9 x& ^4 ~- x) Z$ c4 @3 j7 x  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。& F! K+ j2 j2 U% K6 V: O! h
; i1 B8 m) s- W

/ W/ d* x- n. `( n( C  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
; D5 D, \# p, P! G3 q
+ j% w5 q! I0 \
0 d$ t& m8 W" }0 Q0 ?% b" Y  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?+ y% K  @0 [8 h7 S  F* h
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。& H* }' S' `2 o- ]' M2 V
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
. t) S/ n6 L; ^; u9 U+ I  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
* }6 r2 D; p: k  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
( V8 @& S6 B3 P8 u, b, b. G  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。- ]# ]1 y( b, B% ]! g
  场景; T% _- Q: c" ]( u9 }( d$ e$ z2 Y6 s
  本文的目标是实现如下场景:
, O* J) ^0 \  R3 z8 `; J' G1 u  中心节点创建一个区块链。
; P6 ]& u: O$ L' ^' G6 G) S& `  一个其他(钱包)节点连接到中心节点并下载区块链。
$ O2 J# c  ^  i1 a, ?$ x: |  另一个(矿工)节点连接到中心节点并下载区块链。; V2 V9 B' p7 M$ G! E1 ?
  钱包节点创建一笔交易。
# \9 q- \! d% N* }6 r# K  矿工节点接收交易,并将交易保存到内存池中。" J% U# t. a1 y% X- D6 B0 _% S
  当内存池中有足够的交易时,矿工开始挖一个新块。6 e/ \5 Q& n' B7 f
  当挖出一个新块后,将其发送到中心节点。/ P6 V; w( l9 n0 |
  钱包节点与中心节点进行同步。& C1 z) q5 ?, g& T+ n3 Y. k
  钱包节点的用户检查他们的支付是否成功。
0 i% B0 l! \: V  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。6 h: {' z4 J: s& h7 j9 G
  版本7 ^6 g1 V' q' @- }$ @
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
$ d. Z& k% o7 H- O8 o
  1.   type version struct {
    : }! M; B+ [: ~. h# n
  2.   Version int
    6 M- a' G$ D8 v: @- Y  J7 u
  3.   BestHeight int
    1 t0 i0 q. a7 a2 B* l% A
  4.   AddrFrom string}
复制代码

3 N3 G+ {/ j1 s  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。& h7 V3 u( S8 ^& `$ w/ f8 `
  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
$ g# G, f8 n" B* F) f  为了接收消息,我们需要一个服务器:
  f' M6 N$ {/ W( U- Y/ O  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {& \9 i' f3 ~! L! Q
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)" \" t5 c; o9 J6 f
  3.   miningAddress = minerAddress& w) [4 X. D8 \! i! |
  4.   ln, err := net.Listen(protocol, nodeAddress)0 P- ]  [* Q) a; e$ h
  5.   defer ln.Close()4 N( B1 b% t2 ~+ d; \) K4 F: Y5 {" j
  6.   bc := NewBlockchain(nodeID)
    - }5 {- F) B* \, m
  7.   if nodeAddress != knownNodes[0] {' r* Y2 V" B! F% c  x
  8.   sendVersion(knownNodes[0], bc)" p, M: A0 O4 c' ~/ S& q- t
  9.   }
    4 Z2 ]5 J5 `6 Z1 z
  10.   for {
    ; i; p& ~% c% D1 L4 o' [$ A
  11.   conn, err := ln.Accept()% J2 @. \. S  b, ]5 x
  12.   go handleConnection(conn, bc)
    + ]! c# C* Y+ q. H0 F3 k
  13.   }}
复制代码

$ V1 n# b2 \. @: A" f" o  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
& E8 Q* _4 l- A0 Z 
  1.  if nodeAddress != knownNodes[0] {0 e' R/ x1 P- m& U" {3 _" e1 l8 d
  2.   sendVersion(knownNodes[0], bc)}
复制代码

* o# E' a$ i: _; H  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。5 S. c  n9 ^. T7 S
 
  1.  func sendVersion(addr string, bc *Blockchain) {
    ( _+ T$ C) N4 I2 X
  2.   bestHeight := bc.GetBestHeight()* D. j5 Y% ?2 Q. ]* X$ y9 }
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})4 D, a/ W& ]' S, g5 o
  4.   request := append(commandToBytes("version"), payload...)8 T, F: a* }/ m( ^# S
  5.   sendData(addr, request)}
复制代码
6 D- P/ a/ w4 X3 K1 m) e
  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:1 _5 I! L% Z1 ]6 g
 
  1.  func commandToBytes(command string) []byte {4 L5 ~9 Z" X3 G0 y* P2 o
  2.   var bytes [commandLength]byte
    $ F# T5 S, n* ^4 x' e
  3.   for i, c := range command {+ E( n" [. R. }5 m* Z% F
  4.   bytes = byte(c)
    0 w- h7 k% t9 Y0 X& ?: O
  5.   }
复制代码
; A5 m4 }: Z8 ~. x1 m
  return bytes[:]}2 _' r1 E* i9 T5 y% k0 `
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
# j+ d' W( D  M% {& ~& u  
  1. func bytesToCommand(bytes []byte) string {
    5 ?: F# M& O, r" Y4 d* u7 ]
  2.   var command []byte
    & x# v( L. l( J
  3.   for _, b := range bytes {
    / e' q# w8 _7 K& O4 g+ b
  4.   if b != 0x0 {
    " Q9 G) a# P0 [% _) e
  5.   command = append(command, b)! ^4 I3 Y* m# g: N9 M
  6.   }
      o2 H2 B# n( i0 G# l- ]8 {
  7.   }. A: W; O5 h. E0 g5 P/ l+ b: f& V
  8.   return fmt.Sprintf("%s", command)}
复制代码
' c. J  z# q& e* W/ G2 p+ ]
  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
, Q& q9 d* c( x( y& W) H  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {$ a* P  e# b& @1 ]# |
  2.   request, err := ioutil.ReadAll(conn)$ q9 `. p( r7 T3 G* @, e8 \
  3.   command := bytesToCommand(request[:commandLength])6 N6 p3 E& U* ^- w# ?
  4.   fmt.Printf("Received %s command\n", command)
    ! i2 z/ z/ V& Q- V) J- K
  5.   switch command {2 F( ~& t+ T/ ^) u! U
  6.   .... y3 b2 x  c! b: p' k
  7.   case "version":  R/ t9 x5 f6 [/ K4 T
  8.   handleVersion(request, bc)4 M. D+ W4 W: P- x$ k: D! d3 Y
  9.   default:
    6 e% p7 B$ O. v
  10.   fmt.Println("Unknown command!")1 k" `4 l' _9 R' W" j; Z7 N8 y- R
  11.   }' h7 A1 y4 x# K" H( j
  12.   conn.Close()}
复制代码

) ?1 n; D* D( r  下面是 version 命令处理器:7 Q/ W& `7 X/ n# u7 L5 e- V- o/ U
  
  1. func handleVersion(request []byte, bc *Blockchain) {) x; c! H0 y' _: c7 V( o
  2.   var buff bytes.Buffer var payload verzion
    ) ]" @* E3 p  H
  3.   buff.Write(request[commandLength:])2 O$ V1 G7 f! s# }: J
  4.   dec := gob.NewDecoder(&buff)3 \; w& y% L5 U' D2 B0 [
  5.   err := dec.Decode(&payload)
    : S: S; P! F0 n; o% O. x
  6.   myBestHeight := bc.GetBestHeight()
    7 [( v+ q1 r$ h) D" m
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {5 F" ^+ w# Z, m7 k- _: S
  8.   sendVersion(payload.AddrFrom, bc)( M$ m, N1 B7 c& l! `3 W
  9.   }
    % o, U7 X8 P8 X5 [) K" t
  10.   if !nodeIsKnown(payload.AddrFrom) {( X6 E& a# `' r* a% ?/ j/ k0 @
  11.   knownNodes = append(knownNodes, payload.AddrFrom)% c3 H3 D4 L8 x$ ^- }
  12.   }}
复制代码

4 L( P9 `2 L* j% j  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。% H  U0 p5 `/ H* Z7 M

9 z, h7 W0 o4 i  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。& O6 G: e4 n7 o
/ f$ P) {9 o# W$ w- M; k! K
. U5 x5 j) b" K4 H, \' A/ m' g: C
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10