Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

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

  Z/ A, ]" S1 E$ J) }; E. ~# d% f+ [- _9 r
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。3 {. L) {' J! \

$ C( j$ ]" u! z. J5 M- b& [3 I
0 i$ L% ^- s. c# f4 v  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!5 s% i( J) Q& v9 p; n
  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。
. v; h' e" c! K/ n% a2 ^
, ]: @! P3 D! k; S" \2 @; m0 P$ U) K% f9 s: ~; e
  区块链网络
* A4 X) c. @7 t- P  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。' {! t5 [0 L' Z9 D
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
* X, I1 R* U5 e; i/ W: J  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。
  F1 X2 ^/ d" e) u2 y" }/ T- t5 x4 a7 Z

! x0 ?% j; |2 [1 H7 {  节点角色
! ~* Y9 q8 k" G  G% L  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:
; i/ @0 V8 s! r( X2 B4 G6 a* k  r! A0 v  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。) ]0 P# R1 o5 n
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
0 m4 p, K* i$ d( K: x  C  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
  H1 L& P4 ^, N) v( w4 S9 g5 W" L3 z) ^7 M* O* z

  P$ J1 E9 \/ \' l4 a  网络简化
/ K/ E! V# [4 @% U: C3 S  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。, k, y0 b. ~/ W

8 D6 |9 S/ S7 x  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
( t; E. g& k- g/ u5 W3 C1 n8 n  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。
( Z7 `9 E; y; G! {6 V) d  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。: n# D. I7 \* I$ m9 b, y
% g3 N* C8 M4 I* F
1 u! |/ Q% A" u' W& c" F' w& C
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
) i' l# Z' G- x; L+ V8 m1 j
1 K$ Q0 P: n) d1 Z( x2 \; a1 O9 [# x+ o5 |( N6 P- u
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?2 x6 |: ~( F9 M' O
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
: M/ T9 t  \3 [2 Q9 V  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:: Q7 l" g0 b2 r
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
0 u  E) B6 w, b/ p" A7 q4 v8 k  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
. j# E& T$ }6 C: F- \5 h. n  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
( |( J6 X& t& ?! t  场景/ ]0 {# _, T% B  g0 a# K
  本文的目标是实现如下场景:3 Z0 U$ X- R6 Z: d
  中心节点创建一个区块链。- N! v: {# ^  v2 g$ M: X
  一个其他(钱包)节点连接到中心节点并下载区块链。7 K) w. S5 m; W" r: l2 O; _; [
  另一个(矿工)节点连接到中心节点并下载区块链。4 ~% @5 b/ b8 I! k' p3 d
  钱包节点创建一笔交易。
3 @, b$ p6 x6 p- z; u4 _  矿工节点接收交易,并将交易保存到内存池中。
+ p, l& Y! z& Q  C; i  v  z# M" V  当内存池中有足够的交易时,矿工开始挖一个新块。- S& |3 |+ O$ W
  当挖出一个新块后,将其发送到中心节点。* T' o' E6 U. W& Q. Y3 Q; c
  钱包节点与中心节点进行同步。
" x7 B- Y4 m% a- ]/ ~" z  钱包节点的用户检查他们的支付是否成功。
9 B( N4 |! \! l: a  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。
1 c5 ?! Y- }( d  版本
) s& D" O# r2 [" |4 }  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:
' r: @5 j( t# ?+ C$ P
  1.   type version struct {% k" l: J) d: u0 Z% K& J' ]
  2.   Version int* z& |8 c3 s' f
  3.   BestHeight int1 k3 i* z  E& w  c2 j& Y6 q, |- ?) d
  4.   AddrFrom string}
复制代码

5 ?8 u7 p" C+ W- ?& Z  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
! n, I3 x8 t+ Z/ N) [3 V& v  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
# e" g0 U& Z" t8 J& ~0 @  为了接收消息,我们需要一个服务器:
; T3 \7 U3 t( S+ C2 {5 ?  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {: G- ]! \* L' S1 I+ f
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)" z$ |& W. l/ [$ @2 s1 F" Y
  3.   miningAddress = minerAddress
    % u/ u, n5 r( f: d) ?) D
  4.   ln, err := net.Listen(protocol, nodeAddress)) \2 X" }" B, O" l1 x) f
  5.   defer ln.Close(): w8 d* t2 b( h( o9 G
  6.   bc := NewBlockchain(nodeID)
    1 P- X' I( f7 v+ i8 R0 N. p! ^# Q
  7.   if nodeAddress != knownNodes[0] {
    $ P# N$ _0 E, A
  8.   sendVersion(knownNodes[0], bc)
    4 C( k# l* E0 x3 @' |
  9.   }& @9 K2 ?% \: ~; `
  10.   for {7 S3 @0 q& N  }& \: y
  11.   conn, err := ln.Accept()* _! g7 j' ]  e; ]1 M4 {
  12.   go handleConnection(conn, bc)
    ! C6 ]' y! O& N1 i0 L" ^& I7 C, l  x7 F
  13.   }}
复制代码
& z$ E) K: P' {1 |: q
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
" a6 h( ~- _2 y6 G2 R* c6 } 
  1.  if nodeAddress != knownNodes[0] {/ P8 o# K& h( |9 C
  2.   sendVersion(knownNodes[0], bc)}
复制代码
# C6 d5 _$ c5 f$ K& h$ g# p
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
7 d' D6 q/ m4 K1 @ 
  1.  func sendVersion(addr string, bc *Blockchain) {6 @; ~) _0 l6 d8 x( @+ u# V
  2.   bestHeight := bc.GetBestHeight()$ n8 ]# r. P# q! B4 R( [8 H
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})3 `. ?* |) {7 z& e
  4.   request := append(commandToBytes("version"), payload...)
    * G# ~1 x0 t7 q! N5 F; c
  5.   sendData(addr, request)}
复制代码

/ _6 K1 B! X+ @) q- e4 H  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:) ?; A/ k) E5 A
 
  1.  func commandToBytes(command string) []byte {
    3 `) ~1 d+ a+ l0 [5 [7 j* `0 [" q: g
  2.   var bytes [commandLength]byte
    7 }; u$ k( Q2 d
  3.   for i, c := range command {/ K7 G, Y9 ~3 w0 t% I
  4.   bytes = byte(c)2 d& W2 }) D+ H$ _! T2 S1 u% e; F
  5.   }
复制代码

( k0 N5 D9 E& x& ~* i5 t  return bytes[:]}
9 y/ }2 @& Y* ~' D0 O! f& ]5 C0 s$ n  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
" h  x7 H$ j2 {* V  
  1. func bytesToCommand(bytes []byte) string {
    , y) @! m$ S6 D) I# A8 `) J
  2.   var command []byte" c1 u# M! i7 f$ V/ M" b
  3.   for _, b := range bytes {# Q1 i8 }4 F" S# z% }) M
  4.   if b != 0x0 {5 `! L: F+ i8 Z+ n
  5.   command = append(command, b)
    ' z) m7 z! |4 ?% r
  6.   }
    2 F& k) {' ^5 U, M  `) P; q
  7.   }3 C' H/ |* B  C$ t0 F
  8.   return fmt.Sprintf("%s", command)}
复制代码

: l' Y* G& D6 m/ z( F  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
4 h  i1 H: T) t# p  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {& L( O2 a1 o5 U/ _! P% j
  2.   request, err := ioutil.ReadAll(conn)0 V, `4 B" ~- E& x. v
  3.   command := bytesToCommand(request[:commandLength])
    " {/ @& C( ?; n; C* A) m" G
  4.   fmt.Printf("Received %s command\n", command), i) \8 r) P" U7 O( S3 M' c) u
  5.   switch command {
    : i3 s6 m, {% ~) Y3 O! ~2 n9 v* `
  6.   ...+ g- |' H' V) \7 W! U
  7.   case "version":
    ) t3 N4 Y; p. O9 K6 d5 F: n7 w
  8.   handleVersion(request, bc): R0 T5 N5 B2 r4 c6 K( _1 V4 d  r
  9.   default:% l- T1 w* h: O, M) G- ]0 d
  10.   fmt.Println("Unknown command!")
    ) W  H3 E- T) P' m- I6 g1 X
  11.   }
    ; b) G9 A8 L" S* W5 p! K1 I0 [. d
  12.   conn.Close()}
复制代码

/ i* R. v2 ?/ @- {: J  下面是 version 命令处理器:
" s9 x2 Q% C. ?% x4 v  p  
  1. func handleVersion(request []byte, bc *Blockchain) {' x6 J6 e. ?4 j5 E
  2.   var buff bytes.Buffer var payload verzion- Y. X/ C  @7 q. s! T
  3.   buff.Write(request[commandLength:])
    9 T1 {0 s1 p/ E  y; P" n* ]* u& `
  4.   dec := gob.NewDecoder(&buff)
    " W% t* L  q4 I- o6 J) B3 a
  5.   err := dec.Decode(&payload)' `- [$ r6 h" t% ^1 c' A
  6.   myBestHeight := bc.GetBestHeight()4 t0 L: l8 ]4 R( ?1 C3 Q
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {
    ( u$ v1 W; D. j; N
  8.   sendVersion(payload.AddrFrom, bc)" q/ T1 j2 x3 b) L% T
  9.   }# D  h0 m- o; c! W8 e
  10.   if !nodeIsKnown(payload.AddrFrom) {: t' I9 O4 m; {5 y3 J
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    % a, G& A! \  g& ~, ?5 S
  12.   }}
复制代码

6 U/ s1 K6 A& e; A5 t9 b  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。: g; ^0 [/ m$ Q

* l* t; z( A2 r4 q8 _  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
! @& ~# B# {) V) |; }: ^8 R
+ \7 z  f$ E  b1 k
! G: l8 z2 x0 f* f
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10