以太坊源码分析报告.docx
- 文档编号:9254284
- 上传时间:2023-02-03
- 格式:DOCX
- 页数:58
- 大小:3.13MB
以太坊源码分析报告.docx
《以太坊源码分析报告.docx》由会员分享,可在线阅读,更多相关《以太坊源码分析报告.docx(58页珍藏版)》请在冰豆网上搜索。
以太坊源码分析报告
以太坊源码分析报告
一、前言
以比特币为代表的虚拟货币时代,代表着区块链1.0,基于P2P网络构建,实现了去中心化的数字货币交易功能。
但是1.0只满足了虚拟货币的需要,很难普及到其他行业。
比如比特币只提供了有限的非图灵完备的脚本能力,不大可能在其上搭建第三方的应用。
发展到区块链2.0,便出现了以以太坊为代表的智能合约平台,提供强大的合约编程环境,可以实现复杂的业务逻辑。
与比特币系统相比以太坊并没有本质的区别,只是全面实现和支持智能合约,让区块链技术不只是发币。
本文以以太坊的官方go语言版本实现go-ethereum为目标,分析其源码实现。
二、以太坊架构简介
1.最顶层是去中心化应用层,即DApp。
它使用truffle开发测试框架(最流行)编写部署和测试客户端,并通过web3.js和智能合约层交互;
2.智能合约层通过以太坊虚拟机EVM交互处理BlockChain及共识相关的事务,同时通过RPC协议进行挖矿和网络层事务的交互;
3.区块链管理模块围绕交易、块和状态进行管理,包括区块的同步验证及异常和分叉处理、交易的广播接收处理和验证及执行、底层数据的读写更新等;
4.共识模块是制定的认定区块合法的机制,包括PoW(ProofOfWork工作量证明,以太坊使用变种的Ethash算法)及PoS(ProofOfStake权益证明,只在测试网络中使用),符合共识算法的新区块才会被节点认可和接纳,链接到分布式账本中,同时才能让矿工得到收益;
5.挖矿模块管理挖矿工作,将争夺记账权的过程分解成多个并行子任务进行;
6.账户管理模块管理以太坊系统中的账户,包括普通账户及合约账户的生成和管理,还有钱包及密钥的生成、导入和导出;
7.网络模块管理着系统中的Peer、Protocol、Downloader、Sync等角色,为整个分布式网络提供节点间的共识基础。
包括节点对端连接的动态管理、ETH/LES/LES2协议的支持、各类数据包的下载和同步;
8.架构的最底层功能为上层模块提供了基础P2P网络的通讯、secp251和sha3等加解密算法、高效的LevelDB键值对存储数据库、合约语言基础及大数字的基本运算。
三、源码目录结构
四、基本概念
在以太坊的YellowPaper中,把整个以太坊看成是一个基于交易的状态机。
从创世状态开始,在一批交易执行后便进入到下一个新的状态,直到当前的终态。
1.交易
当一个账户向另一个账户发送一笔被签名的消息数据包时,就产生了一笔交易。
账户可以是普通账户,也可以是合约账户。
交易执行时需要花费手续费。
交易Transaction定义在core/types/transaction.go中:
Transaction的主体定义在txdata中,其它成员都只是交易常用信息的缓存:
hash:
交易RLP编码后的哈希值;
size:
交易RLP编码后的大小;
from:
交易的发送地址,它并不存储在交易体里,而是由txdata中的V,R,S值推导出来;
交易的主要信息包含在txdata中,包括如下字段:
AccountNonce:
代表发送账户发出的第几笔交易;
Price:
交易发送者愿意支付的一单位gas费用的价格;
GasLimit:
交易执行所花费最大的gas值。
如果超过该值则交易失败;
Receipient:
交易的接收地址;
Amount:
从发送地址向接收地址转移的以太币数量;
Payload:
可选,在创建合约时表示合约代码,或者调用合约时调用参数;
V|R|S:
secp256k1签名数据;
Hash:
同Transaction.hash,在转换为Json格式时用到;
2.区块
一个区块包含了一系列的交易,矿工节点收集本地发起的及网络中其它节点广播的新交易,验证交易的有效性,然后将它们打包到一个原始区块中,最后通过挖矿得到一个数学机制的“工作量证明”写到该区块,从而得到一个新的合法区块,广播到网络中,在其它矿工验证区块有效后添加到主链上。
区块的定义在core/types/block.go中:
header:
Block的核心,由后面给出其定义;
uncles:
叔块,以太坊对孤块(发现晚但是合法的新块)的处理和比特币的抛弃式处理不同,因为以太坊十几秒的出块间隔会导致大量的孤块,因此以太坊鼓励矿工引用孤块成为叔块并支付报酬,减少昂贵成本的浪费,使得主链更重提升安全性,也缓解矿池中心化问题;
transactions:
区块打包的一批交易;
td:
TotalDifficulty,总难度值,主链是td值最大的链;
ReceivedAt:
记录块的接收时间;
ReceivedFrom:
记录块的发送peer;
Header的定义也在core/types/block.go中:
ParentHash:
父区块的哈希值,除了创世块以外每个区块都有且只有一个父区块;
UncleHash:
Block.uncles的RLP编码后的哈希值;
Coinbase:
挖出该区块的矿工地址,矿工的挖矿收益都是发给这个地址;
Root:
“statetrie”的根节点的RLP哈希值。
所有账户对象逐个插入一个Merkle-PatricaTrie(MPT)结构里形成一棵“statetrie”;
TxHash:
“txtrie”的根节点的RLP哈希值。
Block.transactions中的所有tx对象逐个插入一个MPT结构,形成一棵”txtrie”;
ReceiptHash:
“receipttrie”的根节点的RLP哈希值。
Block的每一笔交易执行完后会生成一个Receipt数组,这个数组中的所有Recipt被逐个插入一个MPT结构里,形成一棵“receipttrie”;
Bloom:
Bloom过滤器,由Block中的所有交易收据中的log生成关于地址和topic的索引,用于快速判断指定的条件(指定地址或指定的事件)是否存在于一组已知的Log集合;
Difficulty:
Block的难度值。
由父块的难度值和时间戳计算得到;
Number:
Block的序号,等于其父块Number+1;
GasLimit:
Block内的所有gas消耗的上限;
GasUsed:
Block内所有交易执行后实际消耗的gas总和;
Time:
Block的创建时间;
Extra:
和该Block相关的任意字节数组;
MixDigest:
256位的哈希值,和Nonce一起用来证明该块持有有效的工作量证明;
Nonce:
64位的哈希值,和MixDigest一起用来证明该块持有有效的工作量证明;
3.区块链
上节说到Block的成员header.ParentHash是父区块的指针,把所有区块按照这种链接有关系连接起来,便形成了一条从创世块到当前块的反向链表,即区块链。
该区块链包含了所有的历史交易信息,且只有在共识机制下被矿工承认的合法区块才能被添加到链中,称为主链。
如果有多个合法区块同时产生,但是因为网络延时问题被不同的矿工节点接收到添加到链上,便会出现分叉。
以太坊使用“GHOST(GreedyHeaviestObservedSubtree)”机制确定有效的路径,即选择一个拥有最多计算量的路径。
BlockChain的定义和维护操作在core/blockchain.go中:
chainConfig:
包含链配置,包括以太坊各历史版本升级时的区块高度,链ID,采用的共识引擎等;
cacheConfig:
主要用于控制Trie的缓存开关和大小;
db:
底层db操作接口,用于读写leveldb数据;
triegc:
存放已插入数据库的Block对应的trie树根,在超过cacheConfig配置的内存限制时将其回收;
gcproc:
累计处理合法块的总时间,超过配置的trie刷新到磁盘的等待时间的话,在trie树内存超过上限时选择一个旧块把其trie写到磁盘,并重置;
hc:
头链,把区块链的头部数据连接起来形成的链。
因为区块链的很多操作如验证、获取块信息都只需要头部,所以独立出来方便操作调用;
rmLogsFeed/chainFeed/chainSideFeed/logsFeed/scope:
事件订阅相关,其它组件需要监听主链的状态变化来触发相应的处理过程;
genesisBlock:
创世块,在peer握手时的沟通协商是否是始于同一区块;
mu/chainmu/procmu:
互斥锁;
currentBlock:
该节点的当前块,即它所认可的最新块;
currentFastBock:
快速同步模式下的当前块;
stateCache:
封装trie.Database加了一层缓存cachingDB以快速访问trie,主要用于根据trieroot读入trie树,而trie树里包含了所有账户状态;
bodyCache/bodyRLPCache/blockCache:
body/RLP编码body/block的缓存,用于快速访问;
futureBlocks:
如果新区块的时间戳是在距现在15s之后,或者父区块在futureBlocks中则把新区块放到futureBlocks中,然后定时加到区块链里;
quit:
接收退出信号;
running:
服务正在运行标志;
procInterrupt:
服务中断运行标志,如果它为1则停止Block处理;
wg:
等待锁,用于服务停止时等待运行的goroutine结束;
engine:
共识引擎接口,共识相关操作都是调用这个接口;
processor:
区块处理接口,用于运行区块里的交易并生成收据;
validator:
块和状态的检验接口;
vmConfig:
EVM虚拟机配置,在执行交易生成新的EVM时使用;
badBlocks:
已经的坏区块。
程序启动从磁盘加载链时需要检查是否包含坏区块;
BlockChain对内提供Block的管理,如初始化时loadLastState(…)从磁盘加载区块链并检验链的正确性,调用Validator().ValidateState(..)、Validator().ValidateBody(…)检验块中的状态和区块体数据,调用InsertChain(…)插入新的区块等,这些都会在后面的功能流程里分析到。
五、启动流程
go-ethereum编译出来的官方客户端程序geth,提供了庞大的子命令和命令行参数,分别控制节点的运行模式、挖矿参数、网络参数、交易及调试参数等,这些选项由geth处理后修改相应的默认配置项,控制geth节点的行为。
我们先从geth的启动流程开始分析,了解节点运行所需的核心组件及相互关系。
geth使用urfave/cli库封装了命令行参数解析过程,抽象出Flags/Commands这些模块,用户只需要提供一些模块的配置即可。
导入包以后调用cli.NewApp()创建一个实例,然后调用Run()方法执行app.Action入口函数。
geth的启动入口在main包cmd/geth/main.go中,默认首先执行包体的init()函数,为app指定Action(geth)、Commands、Flags、Copyright等信息,然后在main()中调用app.Run()正式启动,进入geth(…)函数:
geth(…)函数所做的事看起来很简单,就是先创建一个Node,然后启动节点运行,直到Node退出,程序结束。
我们先看makeFullNode(ctx)是怎样创建一个节点的。
(1).调用makeConfigNode(ctx)根据配置创建一个Node(命名为stack)
a.先创建默认配置,分成四部分:
Eth(客户端相关)、Shh(Whisper相关)、Node(节点相关)、Dashboard(dashboard相关);
b.如果命令行里指定了配置文件,则从配置文件加载覆盖对应的配置项;
c.将node相关的命令行选项配置应用生效,包括P2P网络配置、IPC/HTTP/WebSocket的相关配置,及数据目录路径、账户密钥目录等;
d.处理完node配置后便可以调用node.New(&cfg.Node)创建一个Node类对象,该对象还包含一个AccountManager,用于管理本地账户和密钥;
e.将Eth相关的命令行选项配置生效,包括coinbase、同步模式、gcMode、挖矿协程数、交易池相关的配置等;
f.将Whisper相关的命令行选项配置生效;
g.将Dashboard相关的命令行选项配置生效;
至此,Eth、Dashboard、Whisper相关的配置都已经准备好了。
(2).向Node注册Eth服务(即创建客户端类,重点核心)
(3).向Node注册Dashboard服务(如果指定了—dashboard选项)
(4).向Node注册Whisper服务(如果指定了—shh选项)
所谓注册,就是把注册的服务的启动函数添加到Node的服务启动函数数组中,在后面的startNode(..)会取出并调用。
现在我们有了配置好的Node节点,和Eth、Dashboard、Whisper服务相关的配置,现在调用startNode(…)在节点上启动相应的服务。
startNode的主要调用在utils.StartNode(stack)中,stack是上面创建的Node节点,其它的几个部分代码是账户密码解锁、设置钱包打开关闭事件监听及启动挖矿(如果指定了—mine)。
我们直接进到stack.Start()函数里去瞅瞅它的启动过程(node/node.go)。
(1).调用OpenDataDir()在数据目录下创建一个LOCK文件,防止数据被多个程序访问造成数据不一致;
(2).配置n.serverConfig,它是P2P网络的配置,包括:
●节点私钥,用于与网络中其它P2P节点握手交换密钥并生成公共密钥。
从数据目录的nodekey文件里读取,并转换成一个椭圆加密算法的私钥。
如果没有则生成新密钥并写到该文件。
●节点名字,如
●Logger
●静态节点:
p2p节点启动后会和静态节点建立连接
●可信任节点:
即使超过最大允许连接数,可信任节点仍允许继续连接
(3).使用n.serverConfig配置创建一个p2p.Server,从而便该节点成为p2p网络的一员,可以监听新连接,与网络中别的节点握手建立新连接,及通信交换数据。
这里不讨论细节,下一节会深入P2P网络进行分析
(4).还记得之前在node上注册的那3个服务吗,现在是时候关照一下它们了(在我的测试里并未启用dashboard,而whisper也是用于DApp的分布式通信,暂时不深入。
主要以ETH服务为主线,因为它是节点功能实现的核心)。
我们先回到过去,看看ETH服务的启动函数:
依据同步模式的不同,如果是LightSync则服务函数是创建一个轻节点客户端les.New(…),否则创建一个全节点客户端Eth.New(…)。
回到node.Start(),之前被注册到node的服务函数被取出来并执行,对于ETH服务则返回一个eth.Ethereum类指针(假设未指定轻同步),然后存放到map[类型]=>对应实例的services中
(5).P2P节点在握手的时候还需要其上运行的协议信息,所以要把services中每个service对应的Protocol信息加到p2p.Server中(Protocol信息在实例创建的时候已经创建了)
(6).现在可以正式开搞了,首先启动p2p节点服务(running.Start())
(7).依次启动services里的服务,如果有一个服务启动失败则全部停止并返回错误(servcie.Start(running))。
对于全节点这里调用的是eth.Ethereum.Start(running)启动了客户端
(8).最后启动RPC相关服务,IPC/HTTP/WebSocket
作为核心的eth.Ethereum,它的Start()正式宣告了节点的启动完成。
在探究Start()的过程前,有必要先分析一下eth.Ethereum类和它的创建过程,先看它在代码中的定义(eth/backend.go:
62):
eth.Ethereum类其实现以太坊全节点功能模块的容器,它内含管理动态变化交易的交易池TxPool,完成ETH协议交互的ProtocolManager,有执行挖矿的Mine,有维护区块链数据的BlockChain,而它需要为这些功能模块提供正确运行所需的ETH相关配置、链配置、数据库接口、共识引擎、账户管理、gasPrice等。
它的创建过程就是创建上述核心模块并初始化的过程:
(1).打开数据目录下的$DATADIR/chaindata数据库
(2).从数据库里读出chain配置和创世块的哈希值
(3).创建一个新的Ethereum结构,除了从入参config和ctx取出需要的变量config、accountManager、gasPrice等进行传递赋值,还创建了共识引擎、BloomIndexer
(4).core.NewBlockChain(…)从数据库“chaindata”中加载创世块、已知的最新块和对应的TD(TotalDifficulty,用来确定最重的权威主链)
(5).启动bloomIndexer服务
(6).对于本地交易如果指定了交易池的Journal选项,则会将本地交易持久化到数据库
(7).创建交易池TxPool,交易池中存放本地交易和从网络接收到的交易
(8).创建ProtocolManager
(9).创建矿工Miner
(10).创建API处理服务
(11).返回创建的Ethereum指针
Now,eth.Ethereum.Start(..):
(1).启动bloom位数据获取的协程提供服务
(2).提供RPC接口中的network相关命令的处理函数
(3).启动protocolManager
(4).如果提供LES请求支持的话启动lesServer
为什么这里只启动了protocolManager?
TxPool和Miner呢?
其实TxPool在创建后就已经开始了无休止的loop循环处理过程,至于Miner,还记得Node启动时调用的StartNode(…)函数吗,该函数最后是这么说的:
如果命令行指定了开启挖矿,或者是开发模式,就启动挖矿.如果没有指定的话,还有一种方式启动挖矿,就是在终端console里输入命令:
web3.miner.start().
好了,现在我们说回ProtocolManager的启动,也是启动流程的最后任务.
它一共开启了4个go协程:
(1).向TxPool订阅新交易事件,然后开启BroadcastLoop(),一旦有新交易,它就会把该交易通知给不包含该交易的节点;
(2).订阅挖出新区块的事件,然后开启minedBroadcastLoop(),一旦有新区块,它就会把该区块发送给网络中的其它节点,告诉它们本节点有该区块
(3).开启syncer()同步区块
(4).开启txsyncLoop(),向新连接的节点发送本交易池中Pending的交易
ProtocolManager协议相关的部分留到下一节P2P网络中一起分析.
到目前为止我们大致梳理了一下geth主启动流程,忽略了一些不太紧要的东西,如没考虑轻客户端(类似全客户端),没展开whisper和dashboard,也没有提Bloom位有什么功能,因为现在只考虑启动过程。
现在我们启动了P2P网络服务可以和网络中其它节点通信交换信息,有了一个运行中的eth.Ethereum全节点客户端可以接收管理交易,挖矿赚取收益,同步区块链和维护数据库,还提供了对外访问接口的RPC服务。
如果没有底层的P2P网络,全节点只是一个毫无生气的死循环程序,没有数据流动,没有交易交换,没有块沟通,区块链不可篡改的分布式账本定位也是空中楼阁.所以紧接着在下一节我们就先分析区块链整以生存的P2P网络实现.
六、P2P网络和节点
go-ethereum代码中P2P的实现在p2p/目录下。
回顾node.Start()启动和p2p相关的部分:
创建了一个p2p.Server,只初始化了它的配置,然后把其它服务所支持的协议收藏起来,然后启动。
p2p.Server结构当然不止这么简单,先看其定义(p2p/server.go:
147):
newTransport:
一个生成一个transport接口的函数,transport提供传输层的握手功能和消息读取;
ntab:
kademlia算法的节点发现实现(重要);
listener:
监听接口;
ourHandshake:
在peer握手时发送的数据,因为常用而且不变所以存放起来;
lastLookup:
用于控制去网络中寻找新节点的频率;
DiscV5:
轻节点LES使用的节点发现协议(未研究);
其它成员基本都是内部使用的channel,用于支持添加/删除静态节点、握手通知等;
这里先果断圈个重点,peer.Server中有个很重要的成员ntab,以太坊使用Kademlia分布式路由存储协议来进行网络拓扑维护,其算法实现在ntabdiscoverTable中,discoverTable是一个提供Resolve(..)、Lookup(…)、ReadRandomNodes(…)功能的接口。
先简单介绍一下这种常见又巧妙Kademlia算法:
Kademlia是一种分布式散列表(DHT)技术,以异或运算为距离(而不是物理距离)度量基础。
异或有一个重要的性质:
假设a、b、c为任意三个数,如果aXorb=aXorc成立,那就一定有b=c。
因此,如果给定一个结点a和距离L,那就有且仅有一个结点b,会使得D(a,b)=L。
通过这种方式,就能有效度量Kademlia网络中不同节点之间的逻辑距离。
Kademlia使用了名为K-桶的概念来储存其他(临近)节点的状态信息,这里的状态信息主要指的就是节点ID,IP,和端口。
对于160bit的节点ID,就有160个K-桶,对于每一个K-桶i,它会储存与自己距离在区间[2^i,2^(i+1))范围内的节点的信息,每个K-桶中储存有k个其他节点的信息,每个节点根据与邻居节点距离之间的距离(NodeID的差距),分别放到不同的桶(bucket)中。
下表反映了每个K-桶所储存的信息
K-桶
储存的距离区间
储存的距离范围
储存比率
0
[20,21)
1
100%
1
[21,22)
2-3
100%
2
[22,23)
4-7
100%
3
[23,24)
8-15
100%
4
[24,25)
16-31
75%
5
[25,26)
32-63
57%
10
[210,211)
1024-2047
13%
i
[2i,2i+1)
/
0.75i-3
每个节点都更倾向于储存与自己距离近的节点的信息,形成 储存的离自己近的节点多,储存离自己远的节点少 的局面。
从上表可以
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 以太 源码 分析 报告
![提示](https://static.bdocx.com/images/bang_tan.gif)