Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
172 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。+ W3 E( Z+ A( [( \! N+ `; E" A
8 S- O" K: R* Q

- ^& R  y# r6 }( R4 T% O, S( G  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。" a0 E; f$ S* a" B# C

. e0 w% y5 _  F- z6 J
& Y' ]1 v  ?: s( n' ]0 ~  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
0 A- I; L6 f, u. w  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。8 e9 k- e" I  r! S: T

/ S/ _+ j4 m& @9 K$ v! m9 E: u
0 b3 ]$ T/ D! {# M- w. ?  区块链网络
: y4 M" g# e, ?+ x( P8 w0 l+ ~4 s" ~  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。+ N5 ^7 R# P9 H; j" a4 S! d: E
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
  [5 ]" {( [( C8 j0 o2 e& J  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。1 _! x2 ]+ B; U2 Y- J+ r' k6 Q! ^

) x3 U7 P9 C$ v5 g! n- Z, [# p3 W: U3 `% s* W
  节点角色
9 C  k( a( M+ @8 V. h  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:4 Z, R/ z' E2 S! ~, n* L1 _
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
) Q7 `2 E& ]1 t) V0 h' T  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
/ n3 r+ k( `, R# S9 _  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。; i' M) c' J$ Y9 D2 z, P
* H% f9 y; Z; W6 M- f# n

$ _0 V8 ]- _: ?; P9 O# T4 D: t/ G  网络简化/ D. P) m8 W/ E8 {+ E3 b8 ]9 H4 @
  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
( ~; G, J/ `. f) C  Z3 J! O) u5 B( l4 v3 J# b! P' v( G
  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。; o$ |8 }. p& p2 d. I( ?+ I# O
  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。# S. ?. O8 D& e7 Y
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
! ~2 V/ e4 B3 ~/ Z$ C
3 E9 B1 z7 y6 Z7 ?& t6 Q) d
7 B" @2 l  r2 J  X' n  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
9 S2 B8 l- O0 \, N: M6 a) ]: C4 t2 @: x) I, k6 _8 R

9 a: v. W0 V  \$ E3 o: [+ }  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?+ n/ @! |, Y# {$ I3 n/ M
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。  I4 y: I$ P0 E( N
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
7 Q0 @( N3 |2 U. V, G& F* u% v  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。, n: i6 J* ^1 i0 U( _
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。: L$ d+ N9 f; }' a) j0 P6 j
  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。6 ~& Z: H3 @3 ?3 m. R
  场景
, C) d( N4 _& q- P' O, }  本文的目标是实现如下场景:" s6 B* c. s& }7 R
  中心节点创建一个区块链。+ l' Y3 Q3 C7 o- P: J
  一个其他(钱包)节点连接到中心节点并下载区块链。
$ C* ], E8 c6 X6 @( b  另一个(矿工)节点连接到中心节点并下载区块链。& x+ C/ R3 y( E+ _" {4 Q4 v
  钱包节点创建一笔交易。
1 n; t& w1 b( r9 a2 f* a3 i  ~& C, O  矿工节点接收交易,并将交易保存到内存池中。
3 h, [2 I% h, r2 J6 n  当内存池中有足够的交易时,矿工开始挖一个新块。
$ @8 |0 ]  ?* D% h  当挖出一个新块后,将其发送到中心节点。
5 X# {3 D: s! s  钱包节点与中心节点进行同步。
2 O; H4 Z6 t' @! L) K) w* k  钱包节点的用户检查他们的支付是否成功。
7 y/ _4 q. q0 u$ D: E! r. i  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。0 @) w" u5 A3 i. {1 c* Z
  版本/ a9 O+ y; O# k' f
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:8 b6 \6 O8 n  x
  1.   type version struct {
    1 X: ], [  j$ b  Q7 G$ \+ ~0 B
  2.   Version int/ I0 R& ^( ~  _- @  X
  3.   BestHeight int
    ' V3 a1 l  x5 w; Z
  4.   AddrFrom string}
复制代码
+ E2 l) D$ o/ X( Q
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
  X* C" ~% C5 q  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。% `4 O* |, y. ~- a& u
  为了接收消息,我们需要一个服务器:
( ?! k6 N7 ~  I1 o' y- k: Y  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    , x8 H5 V: J  \4 F9 Z
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    . e) m  ]+ s# l  S7 V1 d# c" i% u
  3.   miningAddress = minerAddress3 [" d( C' X' r
  4.   ln, err := net.Listen(protocol, nodeAddress)
    - S8 ~: V  C# P, _" ~+ T
  5.   defer ln.Close()
      h$ p3 C1 J. X( E6 `
  6.   bc := NewBlockchain(nodeID)8 J6 Y! G1 i$ M+ s4 a. Q  U
  7.   if nodeAddress != knownNodes[0] {5 U: A1 B. w9 I' x. {+ o
  8.   sendVersion(knownNodes[0], bc)7 Q8 x1 V1 a. r7 x* d2 e& H; a) Z
  9.   }
    ; b' x" o4 ~0 t9 C  ~# z/ n
  10.   for {
    4 c' w2 s# ?- i# r8 S0 L; M
  11.   conn, err := ln.Accept()( c& w6 O; f0 x; k2 ?
  12.   go handleConnection(conn, bc)$ Q* R3 m2 |7 v4 O3 N  z
  13.   }}
复制代码
. d2 r' m- Q2 z# a
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:: ?. w9 {4 ?9 {0 K! u
 
  1.  if nodeAddress != knownNodes[0] {
    ( ?* T9 `0 I& e# T6 z$ I7 V) B" {
  2.   sendVersion(knownNodes[0], bc)}
复制代码
. D6 F1 ]+ U2 w, s8 w& l
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
+ T6 c  F2 \# ~1 I 
  1.  func sendVersion(addr string, bc *Blockchain) {
    ' S, j' \+ r+ y! Z% u3 b& K- L% P
  2.   bestHeight := bc.GetBestHeight()3 p# g* k8 U; l0 E1 X7 j
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
    + O+ q' H* Q* a- n" F; ?' o- r" i
  4.   request := append(commandToBytes("version"), payload...)) ^& u8 S: G9 v
  5.   sendData(addr, request)}
复制代码

2 b* X0 M9 z# B  g' m  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
+ X4 u. k& i( \1 N8 ]. X 
  1.  func commandToBytes(command string) []byte {4 N# W  N& T* \( v. J& R
  2.   var bytes [commandLength]byte" O) ], W  E( N+ d  h4 O5 @2 [
  3.   for i, c := range command {0 c" _$ K. J: {. V- j9 X
  4.   bytes = byte(c)
    3 \/ t2 C' o6 S5 F& P
  5.   }
复制代码
2 e; q/ P5 ~; j
  return bytes[:]}
6 c9 x; N! w7 R8 h/ X% G  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:; {: ?6 @0 L: }& r9 R; s; y" d
  
  1. func bytesToCommand(bytes []byte) string {
    8 W- S( Z( h2 p1 A: T" [" j
  2.   var command []byte' I% f% ?( ?4 Q7 h2 V; ^) v
  3.   for _, b := range bytes {  w% f! B: l# W7 v6 ?0 p5 X, D
  4.   if b != 0x0 {6 I- o! a7 ~5 {0 ^8 t5 Q
  5.   command = append(command, b). J! D+ S+ W) ]/ \+ s
  6.   }0 w. f& J7 h- B; C8 [
  7.   }
    # Z7 A) I6 U7 i4 o- p  g
  8.   return fmt.Sprintf("%s", command)}
复制代码

% l" `- w. ?) K; X. P; L5 `* y  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:/ m- e% J  M6 l; ]9 O
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {. m) B; B* I! Q
  2.   request, err := ioutil.ReadAll(conn)3 a* z. S4 H/ ^) x/ [1 ?
  3.   command := bytesToCommand(request[:commandLength]): a0 k, _- f0 I. S' ?+ z
  4.   fmt.Printf("Received %s command\n", command)
    % ]% P4 @/ ]* W! H* N
  5.   switch command {6 p1 w& H# L' G0 |+ u3 n) |
  6.   ...
      ~9 G6 A& ~) n0 ^) d7 q6 y
  7.   case "version":" ~; P2 h1 B0 W$ J6 _
  8.   handleVersion(request, bc)7 y5 Q( }: O( z
  9.   default:
    ) m/ Z  ]3 O" }' E! v
  10.   fmt.Println("Unknown command!"). ~6 o5 f! G( e2 p
  11.   }
    4 U& H) J5 G9 J
  12.   conn.Close()}
复制代码

3 F3 D0 B4 R8 }( y- p% o  下面是 version 命令处理器:( d1 e2 r1 ~& V# G- S
  
  1. func handleVersion(request []byte, bc *Blockchain) {) A! |+ J4 N4 T* H% H
  2.   var buff bytes.Buffer var payload verzion  r( T) G: A- P/ ]0 i4 P
  3.   buff.Write(request[commandLength:])
    9 b0 c% A, e5 \" c/ h  C3 C
  4.   dec := gob.NewDecoder(&buff)
    . d$ s# P7 B1 D* w3 Z
  5.   err := dec.Decode(&payload)4 W  ~. @) {, y$ x; J* a; B# r0 ?
  6.   myBestHeight := bc.GetBestHeight()
    8 [; C1 C. r/ b' H; y
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {% ]0 F' F- p' o8 H; r% s
  8.   sendVersion(payload.AddrFrom, bc); v$ _4 s0 |& h; k* [' T
  9.   }
    5 c' l. `8 c, f! }6 V
  10.   if !nodeIsKnown(payload.AddrFrom) {
    ( q) E: e1 S. p6 u( d: c+ [5 |; u
  11.   knownNodes = append(knownNodes, payload.AddrFrom)5 k+ z" }3 r" M- C" ~
  12.   }}
复制代码
; H* t$ K/ i. c: c: [
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
$ n  y4 b) v$ Q- R
1 E6 z9 b  T8 a) B* m  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
& {) b7 K* P+ f7 u1 B3 F
7 K+ L* Z1 k# d) a( {# t7 c* ]  a& f8 f9 c5 B+ [! v
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10