Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
110 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。1 z; I6 p  v9 r% b6 k
. s0 [2 o8 T" Q0 D& B

: w/ S" c: j  x; m4 Y1 m: h) t  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。
  N$ n* T  }" |1 i$ U6 i* G( @  f) f5 \0 P, L5 H, F

' E$ p) m+ C1 |! d! [3 l  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!0 ^5 v- G2 ]* g4 }
  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。
$ W+ }1 X, l* Q) O" E0 D" ~! f" G7 _2 A% z- x4 S/ `8 m
$ }. q# G) o, m
  区块链网络& ]. N/ `  ~5 X8 o3 I1 v
  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。
; r2 f  E) ]' ]) ]3 H5 E  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:
  g" e' G5 ]. S& s+ [; A8 u) a  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。2 }# f! X4 p+ H, C) `" h" _2 i

! h5 q1 J" l8 i" O/ R
5 X* V4 X( S* b( t3 a& Z0 |  节点角色
8 L$ e4 s/ H( e" ^2 W. Y  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:! v- U' t" b* |0 V( y5 j' K
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。
6 e' W3 g1 e' s& Q3 N  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。  k( I, r  ^# q, _5 ?" W
  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。
# A2 f1 d% [8 O3 l" c$ t3 x, Q$ F$ w" z7 y7 g! P
% W( q% N$ |) C# e$ t7 [) z2 O
  网络简化
7 K0 t0 z5 \  H* y% Y  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。
& [( e9 Q/ i4 J' w. n( k( U8 F( G. z, g* Z, d
  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。! u+ p" ]) r  ^( M3 s, F
  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。* \7 b: z% B* f' B
  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。
% Q" D" b" W# m9 B5 [% o# E" V# J( T4 c4 A! k! W
6 t, ~1 h' z' e. G6 b
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。* b3 F# _8 Q2 }' Y
! `+ w& n. y$ y- W; T$ N% {
3 @$ P5 K7 b# @! E
  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?. K0 k; P7 f! b1 z1 a
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。
# z; K; T6 V5 e) T$ S: Q5 \) r  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:& @6 \9 ~# g6 P; E) x; A; j
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。) [  S% w- f+ h. _, p# V* N- B
  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
: w& W, D: v: i/ F  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。& W% I# e. T% s; Q" t$ I
  场景9 p0 z4 N0 Z9 r5 c# Q" V! k8 g+ D+ W
  本文的目标是实现如下场景:5 l: _0 d+ O( ~* {; l4 A3 [8 [* S
  中心节点创建一个区块链。& G+ A8 P, v& V) q6 ^1 g
  一个其他(钱包)节点连接到中心节点并下载区块链。
8 C5 H, r0 }4 [$ O7 q% r  另一个(矿工)节点连接到中心节点并下载区块链。
9 L  r" @) h/ {& C  钱包节点创建一笔交易。
2 I, y/ Z$ a& x1 l0 m) r# H8 p% n  矿工节点接收交易,并将交易保存到内存池中。. c; t' R  _: w
  当内存池中有足够的交易时,矿工开始挖一个新块。; x9 H! C6 P" M& T
  当挖出一个新块后,将其发送到中心节点。; X% `; }  U2 W, l1 y/ Z
  钱包节点与中心节点进行同步。
# [% s& X- \& O8 x- A' `/ t  钱包节点的用户检查他们的支付是否成功。1 G0 t5 U! i% p: F' f# V
  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。( h5 v) h) V; V4 G( v5 [" E
  版本0 t8 a; H' ^" D5 P3 |; {$ F
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:' V/ ~! a7 |7 ?0 C0 M; ~- v# U
  1.   type version struct {2 y4 _' }& F" F. L
  2.   Version int
    " b  Q( w- E- R) C5 Z
  3.   BestHeight int
    3 v: k0 x; s5 Q3 g/ I* ~" t3 i
  4.   AddrFrom string}
复制代码

" [* O/ \. }1 Y: b7 Q, x. C  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。3 ]7 g* A, S' c0 w1 y! I5 Y
  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。. k; Z5 `$ l( g  ~9 j! t
  为了接收消息,我们需要一个服务器:% D. v  ?  E, o+ }  j
  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {
    0 A6 u; `: y( Z/ K; {. u3 G% V
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)( O3 `6 d" I6 b0 A" ], T; }' K
  3.   miningAddress = minerAddress
    8 @" l& ?! m0 X, U6 M
  4.   ln, err := net.Listen(protocol, nodeAddress)0 ^  [. |& A& j+ m* ^
  5.   defer ln.Close(): k% K1 V* w$ A8 \: @" D3 n7 B$ y9 R
  6.   bc := NewBlockchain(nodeID); E( x- v- g, s: _- R! ^  R
  7.   if nodeAddress != knownNodes[0] {
      T3 k$ |% h3 ^" |4 l, W" a
  8.   sendVersion(knownNodes[0], bc), O# `8 C* o! {# p* U
  9.   }
    . F7 l  Z  V) M- m) `% S: |
  10.   for {+ X0 f* k: A4 F
  11.   conn, err := ln.Accept()
    1 j4 C' p6 v4 c. ^4 U; Q3 A2 M
  12.   go handleConnection(conn, bc)8 z$ g- _2 {6 P* J4 s" K: `
  13.   }}
复制代码

/ j& u1 s8 Q! T) }& P/ |% v" a* h  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
, E# G5 B, H0 t& r+ I8 z$ S2 _+ T5 ^ 
  1.  if nodeAddress != knownNodes[0] {& b/ J# C6 x1 j" S6 F5 S& q, [
  2.   sendVersion(knownNodes[0], bc)}
复制代码
. }5 N2 z, B: q
  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
6 l9 d; y% s+ s5 T7 e1 X3 r0 b 
  1.  func sendVersion(addr string, bc *Blockchain) {
    + P1 b+ b) Z' H4 P& m4 ^
  2.   bestHeight := bc.GetBestHeight()
    * e, V9 H* x3 @
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})
    7 z6 U" S+ w- T" H
  4.   request := append(commandToBytes("version"), payload...)# ^9 H, g, m' G1 n1 t( J) R
  5.   sendData(addr, request)}
复制代码

# e& P$ _0 X5 }  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
% c0 x( W! V" F* B9 \( v/ u* d 
  1.  func commandToBytes(command string) []byte {1 Z5 t+ q" Z& u" z6 V
  2.   var bytes [commandLength]byte$ J5 H  E! S! j. f- n9 j1 Z( C
  3.   for i, c := range command {
    0 H" A% Q' W" X5 |, s
  4.   bytes = byte(c), E% d4 `5 I) }2 S# Y, U% m, a! z
  5.   }
复制代码

: q& b$ J% B$ q: S; p  c/ H% ^  return bytes[:]}9 Z6 S1 z+ G2 r* D# k2 M
  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:# v- G7 M# l3 _8 @" Y
  
  1. func bytesToCommand(bytes []byte) string {
    + U% o* m; Q, \) S; C0 z8 x
  2.   var command []byte
    1 j0 V8 o+ y; z2 f( M
  3.   for _, b := range bytes {
    4 c0 G$ ]' q1 o& K8 d& C6 V: G5 j
  4.   if b != 0x0 {
    ; v( l0 y( F; |+ L: ?0 }8 _
  5.   command = append(command, b)
    * {# x- O1 u7 C! K% _/ X
  6.   }5 j$ g/ m) a8 u; }
  7.   }
    6 o7 s* D+ L3 S( R7 `2 w  N
  8.   return fmt.Sprintf("%s", command)}
复制代码
5 h/ S5 g7 p& @4 V
  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:
; P/ B) Y2 f6 V: w  v  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {/ f2 ^; E& L/ T/ D
  2.   request, err := ioutil.ReadAll(conn)
    4 U0 A/ I- |: a
  3.   command := bytesToCommand(request[:commandLength])
    1 f  B8 R3 k9 _4 ^
  4.   fmt.Printf("Received %s command\n", command)
    7 K% C3 J/ B6 `9 Z2 w  D
  5.   switch command {. `' G6 M% @. T, J% I
  6.   ...9 s. R8 w+ ?  ]' V$ g9 Y" T# F* }
  7.   case "version":+ G( q& [: s1 A- ?3 F
  8.   handleVersion(request, bc)$ N7 n7 I  C5 \# e) Z! J
  9.   default:5 b3 a7 _6 L* g; I# h# ~3 {
  10.   fmt.Println("Unknown command!")
    + t9 x. B4 j+ {  d* H$ S- [; B2 F
  11.   }
    * O8 [$ ]$ O+ H( w
  12.   conn.Close()}
复制代码

4 S. i9 Y$ H8 O6 X3 U! l5 T8 x  下面是 version 命令处理器:9 V1 c" ^8 W0 g. S" ]% z& M
  
  1. func handleVersion(request []byte, bc *Blockchain) {
    7 S0 \7 h6 Q$ l# z4 ?
  2.   var buff bytes.Buffer var payload verzion- B% [: p/ f- K9 R1 D
  3.   buff.Write(request[commandLength:])
    6 [, {# S- r* ~, x1 ^) r4 C2 f
  4.   dec := gob.NewDecoder(&buff)7 m* D4 m* r4 O$ Q4 Y  r1 b0 k( d
  5.   err := dec.Decode(&payload)2 e7 `! j  @4 b: T  U8 G7 m
  6.   myBestHeight := bc.GetBestHeight()
    * c/ ~8 ]0 m- l+ M7 f2 b" O# a0 B
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {
    ; [' d( }/ z. a: F2 ^
  8.   sendVersion(payload.AddrFrom, bc)
    1 i3 N+ x: \" F* C+ H' X" \
  9.   }6 K2 _, L8 N. ^/ J9 Y0 `
  10.   if !nodeIsKnown(payload.AddrFrom) {( h$ U7 g# C8 q+ E2 ^& U
  11.   knownNodes = append(knownNodes, payload.AddrFrom)% N2 C9 n+ B* c1 q6 g0 o- O
  12.   }}
复制代码

# I5 Z$ p$ |1 Q  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。# h, F) b, _) ]) M- W+ u
( i( O  j* ~8 [' u1 ?2 I7 z
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
; h3 H- B8 `# F% z% c& P2 f( O$ E3 t! x5 ?7 W& `# J+ E

' E% L  r% p! e2 [; Q7 r  w8 w
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10