做者:0x7F@知道创宇404区块链安全研究团队html
区块链的火热程度一直以直线上升,其中以区块链 2.0 —— 以太坊为表明,不断的为传统行业带来革新,同时也推进区块链技术发展。node
区块链是一种分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式,这是一个典型的去中心化应用,创建在 p2p 网络之上;本文以学习和分析以太坊运做原理为目的,将以太坊网络架构做为一个切入点,逐步深刻分析,最终对以太坊网络架构有个大体的了解。git
经过学习以太坊网络架构,能够更容易的对网络部分的源码进行审计,便于后续的协议分析,来发现未知的安全隐患;除此以外,目前基于 p2p 网络的成熟的应用很是少,借助分析以太坊网络架构的机会,能够学习一套成熟的 p2p 网络运行架构。算法
本文侧重于数据链路的创建和交互,不涉及网络模块中的节点发现、区块同步、广播等功能模块。docker
其中第 三、四、5 三个小节是第 2 节「网络架构」的子内容,做为详细的补充。json
在介绍以太坊网络架构以前,首先简单分析下 Geth 的总体启动流程,便于后续的理解和分析。数组
以太坊源码目录安全
初始化工做服务器
Geth 的 main()
函数很是的简洁,经过 app.Run()
来启动程序:网络
其简洁是得力于 Geth 使用了 gopkg.in/urfave/cli.v1
扩展包,该扩展包用于管理程序的启动,以及命令行解析,其中 app
是该扩展包的一个实例。
在 Go 语言中,在有 init()
函数的状况下,会默认先调用 init()
函数,而后再调用 main()
函数;Geth 几乎在 ./cmd/geth/main.go#init()
中完成了全部的初始化操做:设置程序的子命令集,设置程序入口函数等,下面看下 init()
函数片断:
在以上代码中,预设了 app实例的值,其中 app.Action = geth做为 app.Run()调用的默认函数,而 app.Commands保存了子命令实例,经过匹配命令行参数能够调用不一样的函数(而不调用 app.Action),使用 Geth 不一样的功能,如:开启带控制台的 Geth、使用 Geth 创造创世块等。
节点启动流程
不管是经过 geth()
函数仍是其余的命令行参数启动节点,节点的启动流程大体都是相同的,这里以 geth()
为例:
其中makeFullNode()函数将返回一个节点实例,而后经过startNode()启动。在 Geth 中,每个功能模块都被视为一个服务,每个服务的正常运行驱动着 Geth 的各项功能;makeFullNode()经过解析命令行参数,注册指定的服务。如下是 makeFullNode()代码片断:
而后经过 startNode()
启动各项服务并运行节点。如下是 Geth 启动流程图:
每一个服务正常运行,相互协做,构成了 Geth:
经过 main()
函数的调用,最终启动了 p2p 网络,这一小节对网络架构作详细的分析。
三层架构
以太坊是去中心化的数字货币系统,自然适用 p2p 通讯架构,而且在其上还支持了多种协议。在以太坊中,p2p 做为通讯链路,用于负载上层协议的传输,能够将其分为三层结构:
TCP/IP
中的网络层及如下的封装。p2p 通讯链路层
从最下层开始逐步分析,第三层是由 Go 语言所封装的网络 IO 层,这里就跳过了,直接分析 p2p 通讯链路层。p2p 通讯链路层主要作了三项工做:
p2p 源码分析
p2p 一样做为 Geth 中的一项服务,经过「0x03 Geth 启动」中 startNode()
启动,p2p 经过其 Start()
函数启动。如下是 Start()
函数代码片断:
上述代码中,设置了 p2p 服务的基础参数,并根据用户参数开启节点发现(节点发现不在本文的讨论范围内),随后开启 p2p 服务监听,最后开启单独的协程用于处理报文。如下分为服务监听和报文处理两个模块来分析。
服务监听
经过 startListening()
的调用进入到服务监听的流程中,随后在该函数中调用 listenLoop
用一个无限循环处理接受链接,随后经过 SetupConn()
函数为正常的链接创建 p2p 通讯链路。在 SetupConn()
中调用 setupConn()
来作具体工做,如下是 setupConn()
的代码片断:
setupConn()
函数中主要由 doEncHandshake()
函数与客户端交换密钥,并生成临时共享密钥,用于本次通讯加密,并建立一个帧处理器 RLPXFrameRW
;再调用 doProtoHandshake()
函数为本次通讯协商遵循的规则和事务,包含版本号、名称、容量、端口号等信息。在成功创建通讯链路,完成协议握手后,处理流程转移到报文处理模块。
下面是服务监听函数调用流程:
报文处理
p2p.Start()
经过调用 run()
函数处理报文,run()
函数用无限循环等待事务,好比上文中,新链接完成握手包后,将由该函数来负责。run()
函数中支持多个命令的处理,包含的命令有服务退出清理、发送握手包、添加新节点、删除节点等。如下是 run()
函数结构:
为了理清整个网络架构,本文直接讨论 addpeer
分支:当一个新节点添加服务器节点时,将进入到该分支下,根据以前的握手信息,为上层协议生成实例,而后调用 runPeer()
,最终经过 p.run()
进入报文的处理流程中。
继续分析 p.run()
函数,其开启了读取数据和 ping
两个协程,用于处理接收报文和维持链接,随后经过调用 startProtocols()
函数,调用指定协议的 Run()
函数,进入具体协议的处理流程。
下面是报文处理函数调用流程
p2p 通讯链路交互流程
这里总体看下 p2p 通讯链路的处理流程,以及对数据包的封装。
在 p2p 通讯链路的创建过程当中,第一步就是协商共享密钥,该小节说明下密钥的生成过程。
迪菲-赫尔曼密钥交换
p2p 网络中使用到的是「迪菲-赫尔曼密钥交换」技术[1]。迪菲-赫尔曼密钥交换(英语:Diffie–Hellman key exchange,缩写为D-H) 是一种安全协议。它可让双方在彻底没有对方任何预先信息的条件下经过不安全信道建立起一个密钥。
简单来讲,连接的两方生成随机的私钥,经过随机的私钥获得公钥。而后双方交换各自的公钥,这样双方均可以经过本身随机的私钥和对方的公钥来生成一个一样的共享密钥(shared-secret)。后续的通信使用这个共享密钥做为对称加密算法的密钥。其中对于 A、B公私钥对知足这样的数学等式:ECDH(A私钥, B公钥) == ECDH(B私钥, A公钥)
。
共享密钥生成
在 p2p 网络中由 doEncHandshake()
方法完成密钥的交换和共享密钥的生成工做。下面是该函数的代码片断:
若是做为服务端监听链接,收到新链接后调用 receiverEncHandshake()
函数,若做为客户端向服务端发起请求,则调用 initiatorEncHandshake()
函数;两个函数区别不大,都将交换密钥,并生成共享密钥,initiatorEncHandshake()
仅仅是做为发起数据的一端;最终执行完后,调用 newRLPXFrameRW()
建立帧处理器。
从服务端的角度来看,将调用 receiverEncHandshake()
函数来建立共享密钥,如下是该函数的代码片断:
共享密钥生成的过程:
如下是共享密钥生成图示:
得出共享密钥后,客户端和服务端就可使用共享密钥作对称加密,完成对通讯的加密。
在共享密钥生成完毕后,初始化了 RLPXFrameRW
帧处理器;其 RLPXFrameRW
帧的目的是为了在单个链接上支持多路复用协议。其次,因为帧分组的消息为加密数据流产生了自然的分界点,更便于数据的解析,除此以外,还能够对发送的数据进行验证。
RLPXFrameRW
帧包含了两个主要函数,WriteMsg()
用于发送数据,ReadMsg()
用于读取数据;如下是 WriteMsg()
的代码片断:
结合以太坊 RLPX 的文档[2]和上述代码,能够分析出 RLPXFrameRW
帧的结构。在通常状况下,发送一次数据将产生五个数据包:
接收方按照一样的格式对数据包进行解析和验证。
RLP编码 (递归长度前缀编码)提供了一种适用于任意二进制数据数组的编码,RLP 已经成为以太坊中对对象进行序列化的主要编码方式,便于对数据结构的解析。比起 json 数据格式,RLP 编码使用更少的字节。
在以太坊的网络模块中,全部的上层协议的数据包要交互给 p2p 链路时,都要首先经过 RLP 编码;从 p2p 链路读取数据,也要先进行解码才能操做。
以太坊中 RLP 的编码规则[3]。
这里以 LES 协议为上层协议的表明,分析在以太坊网络架构中应用协议的工做原理。
LES 服务由 Geth 初始化时启动,调用源码 les 下的 NewLesServer()
函数开启一个 LES 服务并初始化,并经过 NewProtocolManager()
实现以太坊子协议的接口函数。其中 les/handle.go
包含了 LES 服务交互的大部分逻辑。
回顾上文 p2p 网络架构,最终 p2p 底层经过 p.Run()
启动协议,在 LES 协议中,也就是调用 LES 协议的 Run()
函数:
能够看到重要的处理逻辑都被包含在 handle()
函数中,handle()
函数的主要功能包含 LES 协议握手和消息处理,下面是 handle()
函数片断:
在 handle()
函数中首先进行协议握手,其实现函数是 ./les/peer.go#Handshake()
,经过服务端和客户端交换握手包,互相获取信息,其中包含有:协议版本、网络号、区块头哈希、创世块哈希等值。随后用无线循环处理通讯的数据,如下是报文处理的逻辑:
处理一个请求的详细流程是:
RLPXFrameRW
帧处理器,获取请求的数据。RLP
编码将二进制数据序列化。msg.Code
的判断,执行相应的功能。RLP
编码,共享密钥加密,转换为 RLPXFrameRW
,最后发送给请求方。下面是 LES 协议处理流程:
经过本文的分析,对以太坊网络架构有了大体的了解,便于后续的分析和代码审计;在安全方面来说,由协议所带的安全问题每每比本地的安全问题更为严重,应该对网络层面的安全问题给予更高的关注。
从本文也能够看到,以太坊网络架构很是的完善,具备极高的鲁棒性,这也证实了以太坊是能够被市场所承认的区块链系统。除此以外,因为 p2p 网络方向的资料较少,以太坊的网络架构也能够做为学习 p2p 网络的资料。