Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
90 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。) a4 z7 ~1 ^/ ]) W
' D! i2 ?7 D3 M

" Y  R# X" ]- D$ c# a/ e2 p- l6 [  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。& g+ I! d- z: m5 K5 }8 U* a

3 F' q1 h5 H: G# U5 y  c* I9 ]+ f4 I* r, P8 E7 r5 q1 a
  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
: }: E9 r3 A2 X2 X3 ~4 r  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。
6 X" i8 ^+ V2 I0 I2 l+ v$ L6 m0 L+ A2 s0 y

% o4 |  ^$ s, j! t  区块链网络. j0 U- u( A  {9 G$ x
  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
# o9 s8 R4 N2 W3 m% W, c  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:* l' f9 T9 C9 R: Y
  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。3 t: v% I0 J" F/ w3 s; S

( N5 o+ c6 @3 `) {" Q1 S# c, d+ ~+ K7 R  g; X
  节点角色6 a: f) Q3 n: @' O) W2 Y
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:
) n! M8 }- [4 ]3 U  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。) Y9 S) a# M! a' n  W' F- G
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。. ]1 q- z$ Q  ]: b9 c% h! Z
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
0 e' d* C  B$ n% g! U0 z) u! W8 G' o1 \: J( w$ B
0 q2 |, R% V1 q* ?* ]
  网络简化
8 }, Y! a& o) t. D# x  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。1 s6 ]  ?  n% V0 A

; P! n2 H% s) \  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。* Y* g# Y8 [$ z' h( Z
  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。
* e5 j* X. ^6 {4 L  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。( U' C; c7 k" x0 ]3 t3 A& X

+ E3 `) f  Z! h. L. Z
* y9 G% b& R. i+ u8 w1 V5 T6 z9 T  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。. ?1 X+ Z+ d8 _+ f6 h

3 Y2 g5 S" W/ z6 j, F: G. W8 R# X3 G
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?
$ j/ u" x! G, ]; D" T  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
: t5 Q7 x) k, w% ^  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:; \- Z$ r9 ?9 l/ S7 w; Z1 K
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
  Z3 B! n# T  T; T4 E3 P) L  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
, z# f1 q( D1 I! `0 o  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。8 x& |: S5 O, H) k' o6 F
  场景
' {' [/ c2 _- `  本文的目标是实现如下场景:! m7 J: T: \1 F
  中心节点创建一个区块链。  i9 C6 j7 p* p, ^9 N
  一个其他(钱包)节点连接到中心节点并下载区块链。
  g: V/ z1 e# R/ H/ d* B! [# D  另一个(矿工)节点连接到中心节点并下载区块链。
* y3 L$ ]6 L/ I- ?5 A% d# a  钱包节点创建一笔交易。
  O- {3 ^- x' ?4 M" X7 r4 Y: Y% d  矿工节点接收交易,并将交易保存到内存池中。
: Q* D! W5 x/ k. @: v) y  当内存池中有足够的交易时,矿工开始挖一个新块。
  ~, F! Z1 D8 G1 z  当挖出一个新块后,将其发送到中心节点。
3 x0 Q) y! s% N  n: S# p  钱包节点与中心节点进行同步。( N- u, V% v& b  V
  钱包节点的用户检查他们的支付是否成功。; B$ i3 L3 q- t' N& a" M  h
  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。7 b: s. o$ E* |2 p
  版本8 D# d) T. f* H- @
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
8 r+ E4 O: P5 q2 I9 G* ^
  1.   type version struct {/ X, K3 h# K2 L3 I& Y( S
  2.   Version int
    7 R9 M, D( C# ~  E% ]2 ]
  3.   BestHeight int
    , h( W& m$ w) U1 H) k+ C
  4.   AddrFrom string}
复制代码
) G# W0 P1 r% t; B
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。) _. e- ]! v4 f" |% m. o$ E
  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
1 ~9 z6 F# o7 V  为了接收消息,我们需要一个服务器:
+ @: A, ~+ J2 R8 K( k+ Y; [' p  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    % W* y' e" ^+ c! c1 ^" {. n, u
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)4 a' s& y7 z. G& D
  3.   miningAddress = minerAddress
    ' c6 q5 r5 y% b3 H: D* ]% z* c" O
  4.   ln, err := net.Listen(protocol, nodeAddress)
    ) J# R0 W: g( J% P
  5.   defer ln.Close()5 |5 I2 o- q% v) M' u
  6.   bc := NewBlockchain(nodeID)
    % r/ `& Y3 \  q! J; N7 t7 M1 J8 ~
  7.   if nodeAddress != knownNodes[0] {  c! O) H9 i( M5 h9 U
  8.   sendVersion(knownNodes[0], bc)
    ; r0 B8 |/ n7 A. z
  9.   }! G: h; Y+ I5 c0 c
  10.   for {
    - S/ l8 f5 M* p4 w% A  I+ Y
  11.   conn, err := ln.Accept()
    ; S0 ?3 K/ s7 v
  12.   go handleConnection(conn, bc)
    # q" M7 r# s% C  e. `. a
  13.   }}
复制代码
4 Z9 U# g. S5 Z( @" r
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:; k0 L5 r0 ?* h/ b& j3 A0 \
 
  1.  if nodeAddress != knownNodes[0] {
    9 k4 C( M7 k& h. d2 _6 C2 H
  2.   sendVersion(knownNodes[0], bc)}
复制代码
; c, W9 U3 a" b. P
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。1 }4 ^; a6 L) L2 z# ^: c3 j5 H9 I
 
  1.  func sendVersion(addr string, bc *Blockchain) {
    , s; s; U& e% K& d
  2.   bestHeight := bc.GetBestHeight()- k' j) x4 l1 ^9 j9 @4 [% a& I
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})! Q8 F' N$ Z4 W3 m$ T! h
  4.   request := append(commandToBytes("version"), payload...)- l. G% u2 S/ z7 k0 R0 h1 \2 T
  5.   sendData(addr, request)}
复制代码

( W6 [& w% K  f  }; ]6 S- f" R  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
1 s- c6 t) f$ z' q" B  Q+ o7 n 
  1.  func commandToBytes(command string) []byte {
    " D# L5 `  c: e
  2.   var bytes [commandLength]byte
    % L( i+ f, m- D: y: ]/ I+ o
  3.   for i, c := range command {
    : U* Q  u! |/ w: n7 B
  4.   bytes = byte(c)- ~/ ?% |9 g4 ^, O
  5.   }
复制代码
: O, h- X/ O' v' v
  return bytes[:]}$ u2 v$ T# i# C6 U' }) q6 c) x- ?
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
, d) K( a- s2 F7 v  
  1. func bytesToCommand(bytes []byte) string {
    ' Z( ~9 O% b; J) z5 J  c8 s# c
  2.   var command []byte( y* b6 i$ @2 _0 [4 T
  3.   for _, b := range bytes {5 M+ i+ p9 U, W% U5 O) \4 r
  4.   if b != 0x0 {% M( W6 A$ L8 `
  5.   command = append(command, b)6 V) p: o+ ~# m; P' d
  6.   }8 g' [: F8 [2 o( m5 q. F- V1 c- ?
  7.   }
    / e, G# D4 [- s! `  g6 |
  8.   return fmt.Sprintf("%s", command)}
复制代码

5 i1 k+ H# O1 j5 F: [: j  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:5 @; p0 d4 Z- }; M, v5 r5 p
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    ' u* h8 N* i0 S
  2.   request, err := ioutil.ReadAll(conn)
    , |6 o7 j+ @1 k( [% f
  3.   command := bytesToCommand(request[:commandLength])
    8 {* F( N' ]) D/ M
  4.   fmt.Printf("Received %s command\n", command)/ j* r3 [$ R$ [/ B* C% d2 K
  5.   switch command {
    ( B8 f% C4 c5 w9 H5 W- @0 S
  6.   ...5 @% J7 X# x4 Z( H6 r5 Q! p
  7.   case "version":3 a# T8 f, T/ ]* e1 x7 g* F0 m
  8.   handleVersion(request, bc)4 W% t+ I, T8 u  e
  9.   default:% s7 {* _: A( \* i- P
  10.   fmt.Println("Unknown command!")2 j7 Z* v: I' G* E$ i; {
  11.   }, {1 A6 w! A9 F: m, x9 F9 ~+ R3 N: ?
  12.   conn.Close()}
复制代码

( Z* Y. s1 @% N' k# T  下面是 version 命令处理器:
, J; ~1 ?  z6 P' [) {7 p  
  1. func handleVersion(request []byte, bc *Blockchain) {* Y) T) M1 j5 |8 y: @; l1 j' }
  2.   var buff bytes.Buffer var payload verzion' ]' H9 p' ~6 b0 W+ X* ]
  3.   buff.Write(request[commandLength:])6 a3 A9 N: y  Z$ A+ }4 {: F
  4.   dec := gob.NewDecoder(&buff)0 `  v+ U9 k6 @; P
  5.   err := dec.Decode(&payload)
    # }0 l# a& v" @
  6.   myBestHeight := bc.GetBestHeight(): D6 Z" g; Y7 j$ O" l8 k3 J9 a
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {9 _0 N6 I6 p* W6 U
  8.   sendVersion(payload.AddrFrom, bc): r) {0 ?+ n" r! \6 I$ `  i# T- F2 b
  9.   }
    : g  N: X& B1 m& }
  10.   if !nodeIsKnown(payload.AddrFrom) {9 Y- y1 u3 E: D4 E) {1 w
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    * j7 D1 o/ z: g- a0 Y% k1 A, K
  12.   }}
复制代码
3 O/ @* h, Z# E  e
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。) d$ |# v3 m" N; W# A$ m- o& d
) y1 g! [1 D7 q8 G( w" X/ L- F
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
8 g  p" X, I: n/ Y; b" ]3 s0 K6 s: v# ?! i6 }5 P
$ ~6 |% T8 Y4 {' n5 ], @$ _
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10