Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
158 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。
2 r- r, I% n* \- d/ H& S2 {" O5 V" l) y; T+ k2 u8 X# w9 T8 C
4 J* u: D& ^$ i+ G  {
  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。+ G% N) \( b: v& t! X7 {

! z- z+ q$ d6 M' w/ W
- W& K1 h. ?+ q3 T* |5 k* J  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!; w3 t- M( l+ V# ^# i
  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。0 z1 c" e6 H  G9 k

& X8 x( S. r. g9 f9 W) M4 P( M$ h
& G. v( ]( K& }2 [5 d  区块链网络
& W. ?3 Q" a. O1 j; {  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。4 a% b- w% @2 r4 e
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
% a; |) i% M0 t5 m  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。9 Y, U  f/ q8 M# r  S6 Y7 _
! [% D* j2 C/ e3 X

& I4 s7 w( p% G2 u4 h% ]  节点角色7 O) c% z3 @& k7 A7 R3 {- B2 D
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:
+ `2 D$ N9 {' R5 l  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。  Y; c* u; B* ?
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。% Q3 {" R8 y4 e0 ^! O, o
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。* y+ d8 v$ \/ Q! D5 t
$ F: Y1 C- }4 M
5 S# u1 H# s0 b, V, |$ @9 Q; `
  网络简化' Q3 s# w$ B  Q* Y6 L3 }- I+ I1 M# g
  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。1 q9 U! Y: T$ i+ {' j5 _

/ c' e8 V! ~4 f# P6 n9 u  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。+ d2 u& U# P6 r- Y& }
  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。' |* R: }. U! `- U& q) k- i% t
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
. _1 i3 X  M0 q/ N& U! ?* j3 e2 L1 N; Q; Q$ r& y
6 `1 L  e0 B& o% Z# e
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。
& W1 Y2 a/ F3 t8 d/ m. H0 e: F" Z7 ?
4 G" |# N  C2 X0 U; l3 p
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?
% o% J- c+ ?: Q$ T+ u' V* ~  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
% _+ v7 M& z1 J4 t, U  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:
# l2 i  l7 n- Q$ b. K! ]  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。+ n. f7 q( w9 ?8 p  ]+ Q4 @
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。& U6 }# |: [' P4 n3 z3 H
  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。# L  K# s+ y5 A+ d( T" m" D
  场景+ O& C" Y! ^, ^* a1 B
  本文的目标是实现如下场景:# W7 ~) G" h+ F# s( x
  中心节点创建一个区块链。
/ u3 P9 r! _. m" u' k: m- u  X  一个其他(钱包)节点连接到中心节点并下载区块链。
- `0 v0 J! O/ W  Y, l$ h+ u  另一个(矿工)节点连接到中心节点并下载区块链。
% V3 z; D5 S$ F6 C  钱包节点创建一笔交易。
2 }6 ]& p+ Q! c0 r) ?/ s- @  矿工节点接收交易,并将交易保存到内存池中。/ Q. r7 b# T, f4 C) X
  当内存池中有足够的交易时,矿工开始挖一个新块。  m" l9 |+ S: x& t
  当挖出一个新块后,将其发送到中心节点。
5 T4 M( K: n8 o# ^- q  钱包节点与中心节点进行同步。7 @( A0 O+ E8 O% ^# J# v, K
  钱包节点的用户检查他们的支付是否成功。
% H& M- z& Q9 t, m6 Q9 U$ H# O" e  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。% B, O* A) w8 S; S' {- E& N$ a9 j
  版本
4 i$ F8 ~; n- D  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:! T$ m6 U3 P3 ^2 I" N: y0 |
  1.   type version struct {8 b7 l  l/ q1 @$ w; \
  2.   Version int. B6 _3 T$ Y0 [2 J9 x, x
  3.   BestHeight int  V' t( y3 u: b8 t
  4.   AddrFrom string}
复制代码
  M) N# ~8 w7 G* F
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
$ l7 b9 t: N% |' c  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。
2 \9 w9 v' C6 I+ S9 j" s- Z  为了接收消息,我们需要一个服务器:8 X$ H( u$ `4 z* }1 T- E1 r! b! F
  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {1 k; j) s$ E9 M$ o' x, g( F
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    ' i1 J6 L- P) V' W7 T9 Z0 w2 U
  3.   miningAddress = minerAddress
    4 n! I7 X  v0 \9 F- C  G4 p2 O. W
  4.   ln, err := net.Listen(protocol, nodeAddress)0 c) w3 \6 `  O
  5.   defer ln.Close()+ `1 B2 z$ i/ r4 j  f
  6.   bc := NewBlockchain(nodeID)
    8 Q3 @. V6 V0 D: c0 a: t
  7.   if nodeAddress != knownNodes[0] {
    6 S  c9 {3 U. _6 f  U0 r! p; L
  8.   sendVersion(knownNodes[0], bc)& w1 h. Z* n, G* _2 r
  9.   }
    0 G$ t7 U) K. w& N. E! _" M# B1 q$ X
  10.   for {  P% b, X! p2 |1 m# z. X
  11.   conn, err := ln.Accept()
    2 f3 j& O+ C0 G- J$ Y: r9 ]" F
  12.   go handleConnection(conn, bc)
    ; B8 [: u- R. X7 U8 C$ R/ n( |  U$ R
  13.   }}
复制代码
2 ^- d  P* H1 l2 }' z. k
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
2 @4 _  q# K8 C 
  1.  if nodeAddress != knownNodes[0] {/ k! W. X1 c9 ?; t* P
  2.   sendVersion(knownNodes[0], bc)}
复制代码
- s9 \4 d1 Y2 k' E# U
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
/ y  s; p3 F, k7 j% }! P2 a 
  1.  func sendVersion(addr string, bc *Blockchain) {- V$ i8 P% g# z: X! i' a8 U
  2.   bestHeight := bc.GetBestHeight()
    * w$ Z$ u+ [* n! I3 L: }7 }9 W
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
    + w$ z: f: u$ o4 D: C
  4.   request := append(commandToBytes("version"), payload...)+ N+ |2 m  T8 c8 ?6 H7 u
  5.   sendData(addr, request)}
复制代码
* ^9 l4 k3 F9 Q3 G' Y' S! j
  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:5 w. Q% W' c8 o' k2 K$ E# K
 
  1.  func commandToBytes(command string) []byte {, E8 b% f/ p: ]8 P' o( F& U) Z
  2.   var bytes [commandLength]byte
    / C9 Y6 E8 J) K1 r( N5 L
  3.   for i, c := range command {
    1 n2 A1 f2 x3 Y  R9 ]* z$ ]
  4.   bytes = byte(c); E5 i$ r8 o+ K  c1 E7 G2 _1 m  g
  5.   }
复制代码

, q+ s; b  R) l, N  return bytes[:]}" \: p4 j: C# f) S$ M
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:
  \5 E- T5 C  y' W1 H# w" Q$ u  
  1. func bytesToCommand(bytes []byte) string {
    9 L7 |, S0 S# a, m3 l7 V+ V( u
  2.   var command []byte
    ! B3 U1 x) t" K, V9 F+ g
  3.   for _, b := range bytes {! p; T0 A, q" `# E+ f
  4.   if b != 0x0 {1 K: @4 W  v! S% ^/ u7 d
  5.   command = append(command, b)% V1 H6 W$ t2 e' C
  6.   }
    7 y: q1 Q. i- z) v7 {7 n( i) s
  7.   }+ }7 {6 t6 m& s
  8.   return fmt.Sprintf("%s", command)}
复制代码

" V4 i6 L8 M0 `! d% W  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
5 n7 Z% p+ [# F: \8 Y! ?: r% b  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {& Z; b1 }5 o. [2 @! ^9 h
  2.   request, err := ioutil.ReadAll(conn)' t0 P5 J/ [  `- T, H5 Q  T
  3.   command := bytesToCommand(request[:commandLength])
    / t( h- c; D; t: ]8 V3 q0 m
  4.   fmt.Printf("Received %s command\n", command)
    1 i; }1 l$ p5 H5 r
  5.   switch command {" f% \# A: r# e, S2 @
  6.   ...
    + d, j7 `7 }6 m- Y4 Q/ W
  7.   case "version":3 B, f1 I5 ~1 v; q+ y: {
  8.   handleVersion(request, bc)+ Q1 p6 J- D; z, d0 ~
  9.   default:2 B: h/ O& W8 M* t
  10.   fmt.Println("Unknown command!")- p4 A* |( o; k
  11.   }
    , @) O" M* y  d6 t
  12.   conn.Close()}
复制代码
2 H7 a$ T4 d' ?! [& \/ w8 x
  下面是 version 命令处理器:
8 l5 j. c: w& i  G+ r+ d8 a  
  1. func handleVersion(request []byte, bc *Blockchain) {- `4 ~" w1 A  l! N  H2 J
  2.   var buff bytes.Buffer var payload verzion
    4 O$ E' n- d) S$ ~; v1 c# y" b* i; r
  3.   buff.Write(request[commandLength:])) Q, N5 ]' l: e0 Y7 H4 o
  4.   dec := gob.NewDecoder(&buff)
    $ b2 H. K  r, ~- K0 e
  5.   err := dec.Decode(&payload)
    0 q+ k# w" c6 C
  6.   myBestHeight := bc.GetBestHeight()  p  K0 S7 e% z
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {
    2 c" M/ H3 \( M, H9 x7 r# y: ?2 T- K
  8.   sendVersion(payload.AddrFrom, bc)
    2 s5 W7 i8 X# \' |! A
  9.   }5 W! w5 E' x3 Z  n; W6 h
  10.   if !nodeIsKnown(payload.AddrFrom) {
    & c5 Z) g6 h8 M' r
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    # ]. ?% J4 e  K2 V" f
  12.   }}
复制代码

1 q$ a; M8 e; g  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
) [+ H6 y; G; {, B, |
$ M" `7 F) ]: c! O  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
+ l$ F" z7 G5 @& [- R
) G) L$ [1 L. V
, S! n. G. Y: O0 D7 _
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10