Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

区块链之网络架构详解

945坏男人
166 0 0
  到目前为止,我们所构建的原型已经具备了区块链所有的关键特性:匿名,安全,随机生成的地址;区块链数据存储;工作量证明系统;可靠地存储交易。尽管这些特性都不可或缺,但是仍有不足。能够使得这些特性真正发光发热,使得加密货币成为可能的,是网络(network)。如果实现的这样一个区块链仅仅运行在单一节点上,有什么用呢?如果只有一个用户,那么这些基于密码学的特性,又有什么用呢?正是由于网络,才使得整个机制能够运转。
& K) [( @5 G- M& H
3 m# n. W. C& z  M
+ l( o* o0 [- f! F2 k  你可以将这些区块链特性认为是规则(rule),类似于人类在一起生活,繁衍生息建立的规则,一种社会安排。区块链网络就是一个程序社区,里面的每个程序都遵循同样的规则,正是由于遵循着同一个规则,才使得网络能够长存。类似的,当人们都有着同样的想法,就能够将拳头攥在一起构建一个更好的生活。如果有人遵循着不同的规则,那么他们就将生活在一个分裂的社区中。同样的,如果有区块链节点遵循不同的规则,那么也会形成一个分裂的网络。. f3 B# w7 c& m& _8 B' j

. O1 A# w3 p; h) W$ T, l
! K+ Y- w5 H. F& I* `% f1 S  重点在于:如果没有网络,或者大部分节点都不遵守同样的规则,那么规则就会形同虚设,毫无用处!
" z  f( f4 }3 I+ E7 r  声明:这是一个实验模式下 P2P 网络原型。本文会展示一个最常见的场景,这个场景涉及不同类型的节点。继续改进这个场景,将它实现为一个 P2P 网络,对你来说是一个很好的挑战和实践!除了本文的场景,无法保证在其他场景将会正常工作。2 {  s# l& U% r3 Q+ ?

0 H/ b, R& {+ \; T3 t. m$ o/ e
2 S- Y, j3 U: m, A( N  区块链网络
, T, f- G9 t$ M1 q( a2 _0 s  区块链网络是去中心化的,这意味着没有服务器,客户端也不需要依赖服务器来获取或处理数据。在区块链网络中,有的是节点,每个节点是网络的一个完全(full-fledged)成员。节点就是一切:它既是一个客户端,也是一个服务器。这一点需要牢记于心,因为这与传统的网页应用非常不同。$ K' u* x8 t& Q4 x
  区块链网络是一个 P2P(Peer-to-Peer,端到端)的网络,即节点直接连接到其他节点。它的拓扑是扁平的,因为在节点的世界中没有层级之分。下面是它的示意图:" c/ O% \9 a, S2 u
  要实现这样一个网络节点更加困难,因为它们必须执行很多操作。每个节点必须与很多其他节点进行交互,它必须请求其他节点的状态,与自己的状态进行比较,当状态过时时进行更新。- j% |1 r. U8 `2 M) v  r1 l

. E3 X8 t& L4 E* _" v; T1 w
* l4 l/ v: @. L: I2 f  节点角色3 A5 Q' H! i% i0 S& S6 a
  尽管节点具有完备成熟的属性,但是它们也可以在网络中扮演不同角色。比如:4 `; `4 M/ J9 N- R
  矿工 这样的节点运行于强大或专用的硬件(比如 ASIC)之上,它们唯一的目标是,尽可能快地挖出新块。矿工是区块链中唯一可能会用到工作量证明的角色,因为挖矿实际上意味着解决 PoW 难题。在权益证明 PoS 的区块链中,没有挖矿。+ y; E- V% |1 l3 N$ G. d
  全节点 这些节点验证矿工挖出来的块的有效性,并对交易进行确认。为此,他们必须拥有区块链的完整拷贝。同时,全节点执行路由操作,帮助其他节点发现彼此。对于网络来说,非常重要的一段就是要有足够多的全节点。因为正是这些节点执行了决策功能:他们决定了一个块或一笔交易的有效性。
' q2 T3 E  p2 i3 e8 f3 u0 d  R0 h' Q  SPV SPV 表示 Simplified Payment Verification,简单支付验证。这些节点并不存储整个区块链副本,但是仍然能够对交易进行验证(不过不是验证全部交易,而是一个交易子集,比如,发送到某个指定地址的交易)。一个 SPV 节点依赖一个全节点来获取数据,可能有多个 SPV 节点连接到一个全节点。SPV 使得钱包应用成为可能:一个人不需要下载整个区块链,但是仍能够验证他的交易。8 c3 w: _% z( Z8 Y5 @
% t$ H/ |& [' d( \7 T
2 N. A4 [  D8 p
  网络简化
0 _2 i! y  ^  o+ X  为了在目前的区块链原型中实现网络,我们不得不简化一些事情。因为我们没有那么多的计算机来模拟一个多节点的网络。% F! }$ S: e. T

. g; E3 x- Z, L! |/ I  当然,我们可以使用虚拟机或是 Docker 来解决这个问题,但是这会使一切都变得更复杂:你将不得不先解决可能出现的虚拟机或 Docker 问题,而我的目标是将全部精力都放在区块链实现上。
  i: i- {  M# d  我们想要在一台机器上运行多个区块链节点,同时希望它们有不同的地址。为了实现这一点,我们将使用端口号作为节点标识符,而不是使用 IP 地址,比如将会有这样地址的节点:127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002 等等。
# t/ ^3 Y% H# c( U  我们叫它端口节点(port node) ID,并使用环境变量 NODE_ID 对它们进行设置。故而,你可以打开多个终端窗口,设置不同的 NODE_ID 运行不同的节点。/ w0 T0 @9 [! \. O& l% W

8 u# w/ p. P6 A* \% l4 H, L) `/ v. c6 {7 S4 R1 I7 @
  这个方法也需要有不同的区块链和钱包文件。它们现在必须依赖于节点 ID 进行命名,比如 blockchain_3000.db, blockchain_30001.db and wallet_3000.db, wallet_30001.db 等等。& s( T  A* n. B6 Y$ O) i( n, ^

2 f/ c7 X( j. ]1 W7 U# g! m4 H
5 Y) t; v* M/ |  所以,当你下载 Bitcoin Core 并首次运行时,到底发生了什么呢?它必须连接到某个节点下载最新状态的区块链。考虑到你的电脑并没有意识到所有或是部分的比特币节点,那么连接到的“某个节点”到底是什么?$ g8 D/ u. ^  h
  在 Bitcoin Core 中硬编码一个地址,已经被证实是一个错误:因为节点可能会被攻击或关机,这会导致新的节点无法加入到网络中。在 Bitcoin Core 中,硬编码了 DNS seeds。虽然这些并不是节点,但是 DNS 服务器知道一些节点的地址。当你启动一个全新的 Bitcoin Core 时,它会连接到一个种子节点,获取全节点列表,随后从这些节点中下载区块链。0 g& n2 ]3 z2 [! d
  不过在我们目前的实现中,无法做到完全的去中心化,因为会出现中心化的特点。我们会有三个节点:- p$ F$ J% T# o0 Y0 l  N5 [
  一个中心节点。所有其他节点都会连接到这个节点,这个节点会在其他节点之间发送数据。
  T3 J; X; T! O' J. u) t" w  一个矿工节点。这个节点会在内存池中存储新的交易,当有足够的交易时,它就会打包挖出一个新块。
0 |5 K" S, ]% P$ S# c6 `  一个钱包节点。这个节点会被用作在钱包之间发送币。但是与 SPV 节点不同,它存储了区块链的一个完整副本。
/ B2 r2 L& s8 @% J6 c1 t, I  场景5 n; i6 L$ ~) q. s0 {/ L
  本文的目标是实现如下场景:
; n, ^' S  a/ ]0 n# n- ^  中心节点创建一个区块链。
# S2 j; W" U, Y) b! \; ~" j) B  一个其他(钱包)节点连接到中心节点并下载区块链。
& T* r  v6 I! Z# Q* ?) g  另一个(矿工)节点连接到中心节点并下载区块链。6 d6 a& ?- n# U4 I/ _! ~) ~/ B% A
  钱包节点创建一笔交易。$ H. z4 p. n+ C7 R% k% P1 \
  矿工节点接收交易,并将交易保存到内存池中。
) m3 v* f4 q* O  y. ^& |  当内存池中有足够的交易时,矿工开始挖一个新块。
" _+ C" B& |: [! P  当挖出一个新块后,将其发送到中心节点。  W( A: Z: r- L! e9 r
  钱包节点与中心节点进行同步。
  O: c: O4 A, p' M/ t  钱包节点的用户检查他们的支付是否成功。
4 Q8 z" I# R5 M: S  这就是比特币中的一般流程。尽管我们不会实现一个真实的 P2P 网络,但是我们会实现一个真实,也是比特币最常见最重要的用户场景。
1 T8 K  X* F1 g- p6 p3 r  版本, x! K- z2 K4 p6 p5 r5 {
  节点通过消息(message)进行交流。当一个新的节点开始运行时,它会从一个 DNS 种子获取几个节点,给它们发送 version 消息,在我们的实现看起来就像是这样:% ^' f) h3 b0 l7 j: H) z! I# b
  1.   type version struct {
    3 c. ]3 K8 d  ^7 K/ B
  2.   Version int' Z' W  h) f& n+ Z2 g
  3.   BestHeight int& u/ \+ W- p' `8 G+ g
  4.   AddrFrom string}
复制代码
$ `/ p* J, U8 @$ c) O! A- ]8 |
  由于我们仅有一个区块链版本,所以 Version 字段实际并不会存储什么重要信息。BestHeight 存储区块链中节点的高度。AddFrom 存储发送者的地址。
- }3 A7 M' Y( _. e( S# Q! ?  接收到 version 消息的节点应该做什么呢?它会响应自己的 version 消息。这是一种握手:如果没有事先互相问候,就不可能有其他交流。不过,这并不是出于礼貌:version 用于找到一个更长的区块链。当一个节点接收到 version 消息,它会检查本节点的区块链是否比 BestHeight 的值更大。如果不是,节点就会请求并下载缺失的块。* q6 Z& K8 _3 L7 Q
  为了接收消息,我们需要一个服务器:
* ~& ^0 z' p/ B9 M6 P  
  1. var nodeAddress stringvar knownNodes = []string{"localhost:3000"}func StartServer(nodeID, minerAddress string) {0 L: X4 v8 B0 w$ M
  2.   nodeAddress = fmt.Sprintf("localhost:%s", nodeID)
    ( r6 k# j3 Y  k: ]5 I& ^# n/ w
  3.   miningAddress = minerAddress# P+ ~' N5 ]3 _3 X6 U
  4.   ln, err := net.Listen(protocol, nodeAddress)
    3 P( q3 r6 m1 Z% S! B
  5.   defer ln.Close()1 M  \( K% o, [
  6.   bc := NewBlockchain(nodeID)
    ; K7 t+ m* K5 |! q, K3 o2 e
  7.   if nodeAddress != knownNodes[0] {* D2 H/ t# i( I4 L  @
  8.   sendVersion(knownNodes[0], bc): M. [. s: h7 C
  9.   }
    " S* f1 K' }3 }
  10.   for {# X2 N, x  V& V6 r/ w
  11.   conn, err := ln.Accept()
    4 ?6 `5 f: p$ b( v8 h, v
  12.   go handleConnection(conn, bc)# y; j  U/ Y& f- D" t/ R
  13.   }}
复制代码
+ m, i; m  [2 k
  首先,我们对中心节点的地址进行硬编码:因为每个节点必须知道从何处开始初始化。minerAddress 参数指定了接收挖矿奖励的地址。代码片段:
3 d) u0 a$ v7 T8 K5 x 
  1.  if nodeAddress != knownNodes[0] {# n2 x& {$ \1 L  e& z
  2.   sendVersion(knownNodes[0], bc)}
复制代码

' k- K# U' k0 Y  U( t2 y$ q  这意味着如果当前节点不是中心节点,它必须向中心节点发送 version 消息来查询是否自己的区块链已过时。
; ]2 F4 I1 A# K& ~) r4 Q& U( s& ] 
  1.  func sendVersion(addr string, bc *Blockchain) {! U% p, B3 H. A' `& C+ M3 ?
  2.   bestHeight := bc.GetBestHeight(); I, v# Q) A. O& W1 A
  3.   payload := gobEncode(version{nodeVersion, bestHeight, nodeAddress})1 N0 H! b  ?9 M# t0 C2 y, {
  4.   request := append(commandToBytes("version"), payload...)
    ! B) b. i1 i" u; Z: E. k
  5.   sendData(addr, request)}
复制代码
- K. F8 Q9 Z) k# T0 f
  我们的消息,在底层就是字节序列。前 12 个字节指定了命令名(比如这里的 version),后面的字节会包含 gob 编码的消息结构,commandToBytes 看起来是这样:
& E2 |$ D9 M8 a" B 
  1.  func commandToBytes(command string) []byte {; \1 B. Z4 P; d) T/ `5 q2 ~
  2.   var bytes [commandLength]byte8 `# ^" `! T& s) w. ~* R
  3.   for i, c := range command {
    . ~( w8 {) S$ R: s* a4 c
  4.   bytes = byte(c)
    ; `) m7 Q4 S* D2 v
  5.   }
复制代码

. j8 V! c' P$ n+ L( U+ U  return bytes[:]}
' s. i7 ?/ c6 \5 g7 N8 L; R% b  它创建一个 12 字节的缓冲区,并用命令名进行填充,将剩下的字节置为空。下面一个相反的函数:3 v( R6 ~. [* }( L
  
  1. func bytesToCommand(bytes []byte) string {
    3 j3 \( I- T9 S, Q6 p1 W6 p# C
  2.   var command []byte
    % r0 @  n0 _2 g) `
  3.   for _, b := range bytes {2 S+ B5 }: Q6 o7 @
  4.   if b != 0x0 {: V' W; W* u4 r3 r+ V6 i. t
  5.   command = append(command, b)* O5 F# `! S) x  q7 |: c
  6.   }
    4 ?7 |  l# s* B6 m( C' w& Y
  7.   }
    4 t5 ~- C9 p$ L9 }
  8.   return fmt.Sprintf("%s", command)}
复制代码

* l2 M! D% I4 [' z  h  当一个节点接收到一个命令,它会运行 bytesToCommand 来提取命令名,并选择正确的处理器处理命令主体:, z) l- y6 D; `! ?6 ?6 d
  
  1. func handleConnection(conn net.Conn, bc *Blockchain) {$ O" t+ d, j# t$ ?: r3 p
  2.   request, err := ioutil.ReadAll(conn)5 @$ s+ t; @" d
  3.   command := bytesToCommand(request[:commandLength])
    / S+ J1 S$ j9 T5 F, T$ w. w
  4.   fmt.Printf("Received %s command\n", command)" X) H7 n& R6 S/ H! {
  5.   switch command {7 C6 @. n6 F/ g! N' ^" G7 N
  6.   .... `: J; t8 U& Y) t
  7.   case "version":
    ' L- w- b/ m' n# C" S
  8.   handleVersion(request, bc), I" Q0 b$ B. _5 F
  9.   default:+ y& x8 r" o: L7 _
  10.   fmt.Println("Unknown command!")  c* x7 i" w# U$ T+ L
  11.   }2 [5 a1 B6 U) L2 t/ E& ?+ ]' {
  12.   conn.Close()}
复制代码
2 u! R; n% N9 J9 S. p
  下面是 version 命令处理器:
3 Y4 e9 G! u5 ~! O* _1 ~' C  
  1. func handleVersion(request []byte, bc *Blockchain) {
    " L0 b2 |3 p( X2 w. u% N& S/ Y# h
  2.   var buff bytes.Buffer var payload verzion5 b+ i+ [% q# p% l& c9 t
  3.   buff.Write(request[commandLength:])
    " n  \$ C. f6 T6 G: s
  4.   dec := gob.NewDecoder(&buff)5 S. e4 k( r8 n
  5.   err := dec.Decode(&payload)% ?7 [: }+ m( g! ?( X
  6.   myBestHeight := bc.GetBestHeight(), M* T& d; z% E
  7.   foreignerBestHeight := payload.BestHeight if myBestHeight foreignerBestHeight {* j6 P- |' V; t& t* Q
  8.   sendVersion(payload.AddrFrom, bc)
    " f! a7 t( i( ~' X' T
  9.   }
    9 U1 o' b8 ^: R, z# Z
  10.   if !nodeIsKnown(payload.AddrFrom) {1 H- [1 h/ }9 d% _* x
  11.   knownNodes = append(knownNodes, payload.AddrFrom)
    2 b3 h  u5 j$ f7 i! k. \. J# |0 b
  12.   }}
复制代码
8 l0 x9 A, a, @0 S
  首先,我们需要对请求进行解码,提取有效信息。所有的处理器在这部分都类似,所以我们会下面的代码片段中略去这部分。
3 k" a  o( n  {9 A# h, f' v& F8 k2 Y' f! S2 s
  然后节点将从消息中提取的 BestHeight 与自身进行比较。如果自身节点的区块链更长,它会回复 version 消息;否则,它会发送 getblocks 消息。
' z/ ?% D1 O( m7 E1 m
; [2 g! A$ a, V  V! s0 O! C4 m$ I" r( Y
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

945坏男人 初中生
  • 粉丝

    0

  • 关注

    0

  • 主题

    10