Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

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

+ ?* g: q9 D2 [  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。
5 S# l3 m' R7 G/ I/ A# t
# t( v8 |: r4 c( C* K2 _
/ Y4 R8 l$ N0 R( _2 c  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!" A% c7 }: m9 m: |  ]0 i
  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。+ `! E5 d8 F9 o
( O; v& d8 i# k3 k
9 H; }( y: T. O( D
  区块链网络
) j. [6 e( Z% v7 b. H& c  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
; p/ A% C* }# l/ ]  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
+ W% P4 z% D# l# i# A  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。( V: k' q1 V- K2 U( H. ]5 @
4 G! P: A% |0 t$ h  q
# S$ ]7 G3 n% W- b  A
  节点角色
4 g/ f4 b5 m) M5 b, x, C  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:6 N4 d" Q& ]/ n9 `6 I* J
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。8 |$ ?' g; h" t! f3 y) ?6 K
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
3 H8 o# E$ n7 r. n  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。: g8 I+ I" f  T0 R. v$ y
4 e! _5 O( e  e+ r+ r5 {# J
. a8 _7 ~2 Q7 s: I1 u
  网络简化/ z: Y) h# Z6 W  ?. ]
  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
0 g" y( p+ j. E& K& C
) U$ J2 I3 \/ i' `/ f; b2 \6 H  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
6 |5 r, ~3 p& T$ h6 N- j  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。1 E* n/ P8 D8 G% {
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
7 L$ B0 _% }/ w! D7 }( v4 C/ O, n3 o, k3 n# w5 P

/ |( y7 |. w5 K7 C) ?  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。! _' z" e; E7 s0 v
7 ]' y; A9 V$ A1 X' H

5 X/ s: _# p6 F- p  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?
6 Q3 a, v* R2 Z  z% J6 p  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
: `6 E9 g3 y* r$ N* ^& |# H+ M  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:: O8 m: l8 t) Y+ O# K
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。4 y: t8 b  I' g! j7 D- B: C- @
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。% E6 @2 u5 c/ {% _* J+ y# A
  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
9 e* b! x, O$ ~+ a. Z  场景2 Q4 g1 z& G/ d
  本文的目标是实现如下场景:& j( q) J+ S- s( c/ ~# q
  中心节点创建一个区块链。  M) m- r! c2 N* O$ i4 O+ o
  一个其他(钱包)节点连接到中心节点并下载区块链。
; J3 Y' u& g. _) b  另一个(矿工)节点连接到中心节点并下载区块链。
) B5 W3 e6 ~4 O/ x5 @3 M; h5 [  钱包节点创建一笔交易。
( U8 g0 ~# O) F  矿工节点接收交易,并将交易保存到内存池中。
& r1 U# ^& S( H  当内存池中有足够的交易时,矿工开始挖一个新块。: l' h$ ?4 K. a! t; e
  当挖出一个新块后,将其发送到中心节点。0 m4 z6 V, U  p6 Y5 Q( ^, `
  钱包节点与中心节点进行同步。/ t' p  v: C/ M5 x
  钱包节点的用户检查他们的支付是否成功。
/ U- ?% ]/ {7 m% E- m& F% T7 S+ k  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。2 y$ t7 [0 Z1 C
  版本
+ M& c. ~: u$ s7 J  r5 U  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
2 \$ C1 _& P$ P  ], e
  1.   type version struct {1 t# E$ G% e5 J7 @
  2.   Version int
    8 P0 K( n+ @3 Z9 f# X9 _
  3.   BestHeight int
    % e$ D$ Z' S0 r
  4.   AddrFrom string}
复制代码
, w& p, Q3 m! h" t) u! L0 R
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。: {0 V. C# P5 {! Z
  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
6 _3 e7 a% ]3 }- d* Y& o0 j! }  为了接收消息,我们需要一个服务器:
. W4 I. t; f, \* E- P, O! ~  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    3 m6 e* h" K+ A5 T
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)3 k4 P5 d) c: U( T; A5 K
  3.   miningAddress = minerAddress
    5 t* T. D2 F9 t/ x3 w- p
  4.   ln, err := net.Listen(protocol, nodeAddress)
    * F* {' h2 a. n! {* F/ ]2 Q
  5.   defer ln.Close()
    9 U; ?. _2 W6 C% x2 }7 ]  r, }
  6.   bc := NewBlockchain(nodeID)
    : ?/ {: c+ V! B
  7.   if nodeAddress != knownNodes[0] {/ ?3 b  ~% ~) R( b, U
  8.   sendVersion(knownNodes[0], bc)
    8 A. x2 z: A) Z8 w/ Z8 u* B" r% z/ m
  9.   }5 Y, P; p! m$ {/ U/ R0 \) Z, o* o
  10.   for {
    1 Z5 J& A; \$ T! x8 l% N6 U6 v
  11.   conn, err := ln.Accept()
    # V) l$ t8 W- _( X  Y9 Q
  12.   go handleConnection(conn, bc)
    1 _* G' N! {4 r# F6 Y1 i
  13.   }}
复制代码
8 U# a4 B$ |' s* Y9 x
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
- W  U4 J+ b6 a 
  1.  if nodeAddress != knownNodes[0] {
    . }! \7 w: }3 T. d" O0 d5 s
  2.   sendVersion(knownNodes[0], bc)}
复制代码

! b# w$ H8 P* W  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
" C& q( j2 I) S 
  1.  func sendVersion(addr string, bc *Blockchain) {6 A% h7 ]  h2 {. z, b1 g$ [
  2.   bestHeight := bc.GetBestHeight()
    3 \/ n8 E0 g, j7 Y  V. [
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
    ; [7 S5 k3 y" ~6 @4 p) W
  4.   request := append(commandToBytes("version"), payload...)% L! C* g1 M& {
  5.   sendData(addr, request)}
复制代码
/ }; h7 M5 ]$ I
  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
! e  q; a7 |# M0 ~ 
  1.  func commandToBytes(command string) []byte {' b0 J+ u. x& r7 i7 E
  2.   var bytes [commandLength]byte& f$ d' E: |2 x! E" S2 F
  3.   for i, c := range command {* k1 W" ]% U3 I4 J
  4.   bytes = byte(c)
    $ w' Z3 p4 l* e$ p1 |% z1 S7 B
  5.   }
复制代码

; j* X( u& q/ R, \- g  return bytes[:]}
0 E7 z* x* l/ e5 S, w% l3 r  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
+ \% V* b$ W5 X1 C$ p2 k# H$ @  
  1. func bytesToCommand(bytes []byte) string {* ^! j, X% v8 V0 n
  2.   var command []byte
    ) B$ ?$ g- R* C4 q7 f
  3.   for _, b := range bytes {' Q: c' z, L) r
  4.   if b != 0x0 {& Q+ {2 Q$ K  B( h
  5.   command = append(command, b), E' ~% E+ f" F9 W" Q0 b
  6.   }" w3 [0 s% l8 H' C# |
  7.   }
    & s" x# ]: u/ q! G7 k1 C
  8.   return fmt.Sprintf("%s", command)}
复制代码

% z1 F( o+ @+ l! M4 H  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:+ j5 h( C% z8 ~0 B+ [" z+ u2 p8 G
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {
    1 j- b2 Q4 @( {* Q# @2 B2 g* `5 A
  2.   request, err := ioutil.ReadAll(conn)
    & e9 A3 K7 q- j+ _) Q9 A+ G& y
  3.   command := bytesToCommand(request[:commandLength])4 }& s0 d1 X7 @3 d$ z- @# A' F
  4.   fmt.Printf("Received %s command\n", command)
    1 d/ J% L& v, a4 i6 s! X' y9 i# I
  5.   switch command {
    " g& Z+ i; G/ u) ]- o7 @- T
  6.   ...* M- D. s) o. v6 R% D! U$ L
  7.   case "version":
    + U* g* j$ i5 u0 F: g0 y
  8.   handleVersion(request, bc)
      p3 X+ j6 I$ N5 i0 o
  9.   default:0 O( [3 q9 O- c
  10.   fmt.Println("Unknown command!")5 \( J  E7 g5 B, Y1 Q$ D
  11.   }: \/ d5 m  c' }; k$ i
  12.   conn.Close()}
复制代码
7 L4 Q/ @2 V. K) c3 s2 L" W2 h/ W
  下面是 version 命令处理器:7 N$ _" _2 d! q: s) ~8 [
  
  1. func handleVersion(request []byte, bc *Blockchain) {' R% q+ ~& r9 M, H1 g2 b+ Y
  2.   var buff bytes.Buffer var payload verzion
    7 j/ I$ }0 J8 @; @  @- ^! h
  3.   buff.Write(request[commandLength:])7 V; o4 L4 k8 [# T& n
  4.   dec := gob.NewDecoder(&buff)
    ; c' i" Q- z  v, t$ y& T6 m
  5.   err := dec.Decode(&payload), b; e: \3 h$ Y9 [1 w# D; y
  6.   myBestHeight := bc.GetBestHeight()
      d5 F# C# i  p; w; ?$ t* G
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {+ S7 [, u7 n6 t+ V$ n
  8.   sendVersion(payload.AddrFrom, bc)
    1 W8 {# E- T: X$ m! O
  9.   }, D  ~, G9 p3 V2 {
  10.   if !nodeIsKnown(payload.AddrFrom) {
    8 X( }+ ?6 F+ E1 v' e6 b; N6 {
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    2 H( f0 w! a: F$ J' C" M! q% e
  12.   }}
复制代码
, t# {- _  Y. a
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。: z$ r- G7 C" t; j6 j
- ^% \6 C" I3 ^6 R
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
! G2 x8 y. `3 |, v. \0 s
) X: q3 l' b) p2 X' ?2 U" Q2 [; U; k$ U0 U) U
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10