主页 > imtoken官方安卓版下载教程 > 知乎创宇区块链安全实验室|以太坊活跃数据同步分析
知乎创宇区块链安全实验室|以太坊活跃数据同步分析
前言
开门见山,以太坊数据同步主要是以节点(peer)为数据载体进行存储和传输。 Header、Body、Reciept组成的数据主体通过以太坊p2p通信协议管理数据同步事务,最终交给执行者(主动同步会交给Downloader,被动同步会交给Downloader Fetcher)执行最终的数据下载任务。
然后问数据主体我们需要同步哪些数据来同步? 不同的数据是否需要分类同步?
我们知道,创宇区块链实验室对这些问题进行了全面细致的分析。
数据主体
要进行数据同步,我们首先需要明确我们同步的数据主体是由什么组成的。 一般来说,有两种类型。 一类不需要节点主动发送同步请求,数据打包时节点会自行向网络广播。 这种类型的数据分为三种类型:类型——完整区块、区块哈希和交易Transaction。 今天我们关注下一类数据。
第二种数据是需要本节点主动发送同步请求,其他节点响应才能同步的数据。 除了第一类数据,其他需要同步的数据都属于第二类,所以可以说它的类型比较复杂,最重要的还有区块头、区块体、交易三种receipt 收据(具体代号⬇⬇⬇),与第一种不同,这三种可以说是完整区块的某一部分,这也说明为了主动同步,希望之间数据同步的范围节点可以自由可控,必要的数据可以先同步,临时的非必要数据可以放弃。
type Header struct {
ParentHash common.Hash `json:"parentHash" gencodec:"required"`
UncleHash common.Hash `json:"sha3Uncles" gencodec:"required"`
Coinbase common.Address `json:"miner" gencodec:"required"`
Root common.Hash `json:"stateRoot" gencodec:"required"`
TxHash common.Hash `json:"transactionsRoot" gencodec:"required"`
ReceiptHash common.Hash `json:"receiptsRoot" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Difficulty *big.Int `json:"difficulty" gencodec:"required"`
Number *big.Int `json:"number" gencodec:"required"`
GasLimit uint64 `json:"gasLimit" gencodec:"required"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
Time uint64 `json:"timestamp" gencodec:"required"`
Extra []byte `json:"extraData" gencodec:"required"`
MixDigest common.Hash `json:"mixHash"`
Nonce BlockNonce `json:"nonce"`
}
⬆Header即区块头以太坊爆仓数据,在区块中起着非常重要的作用。 ParentHash会记录前一个区块的区块hash,uncleHash会记录叔块的hash,coinbase会识别矿工地址。 Root、Txhash、ReceiptHash分别是state trie、tx trie、Receipt.trie。 三个前缀树的根节点 RLP 代码哈希用于描述世界状态,一个用于描述交易,一个用于描述交易回执。
Bloom是区块头中的布隆过滤器,用于快速判断目标哈希是否在某个集合中。 Header中的上述数据被区块头用来标识区块和区块中的重要数据模块以太坊爆仓数据,使用hash。 其余数据各有所需。 可以看出,一个区块头已经基本描述了一个区块框架,这也为后面提到的数据同步策略做铺垫。
type Body struct {
Transactions []*Transaction
Uncles []*Header
}
⬆Body比较简洁,包含一组交易对象和一组叔块的块头。
type Receipt struct {
// Consensus fields: These fields are defined by the Yellow Paper
PostState []byte `json:"root"`
Status uint64 `json:"status"`
CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"`
Bloom Bloom `json:"logsBloom" gencodec:"required"`
Logs []*Log `json:"logs" gencodec:"required"`
// Implementation fields: These fields are added by geth when processing a transaction.
// They are stored in the chain database.
TxHash common.Hash `json:"transactionHash" gencodec:"required"`
ContractAddress common.Address `json:"contractAddress"`
GasUsed uint64 `json:"gasUsed" gencodec:"required"`
// Inclusion information: These fields provide information about the inclusion of the
// transaction corresponding to this receipt.
BlockHash common.Hash `json:"blockHash,omitempty"`
BlockNumber *big.Int `json:"blockNumber,omitempty"`
TransactionIndex uint `json:"transactionIndex"`
}
⬆Receipt是区块中所有交易对象执行后生成的数组,用于记录交易处理信息。 生成后会一一插入到Receipt trie中,同时生成block header中的ReceiptHash。
交易回执记录三部分信息,用于交易管理和识别。 第一部分是共识部分,PostState、Status、CumulativeGasUsed、Bloom、Logs。 只有这5个数据会被编码到ReceiptRLP中形成receipt hash,然后receipt hash参与共识 第二部分数据是交易部分,TxHash是指交易回执对应的交易hash,ContractAddress是部署交易时新合约的地址,GasUsed表示交易的gas使用量第三部分是区块部分,记录BlockHash区块哈希,BlockNumber为当前区块号,TransactionIndex为交易序号在块中。
Receipt在数据同步策略上有一个直观的区别。 下面说的fullSync和fastSync最直观的区别就是是在对端同步Receipt还是在本地生成Receipt。
数据载体
数据传输载体为对端节点。 这个数据载体peer,如果你在源码中仔细搜索,你会在一些角落找到一个peer.go。 首先列出我找到的关于数据同步模块的peer结构。 有p2p/peer.go、eth/peer.go、les/peer.go、eth/downloader/peer.go。 以太坊的网络也有传输层、会话层、表示层和协议层。
p2p 包中的对等点充当底层节点模型。 传输层将基于UDP协议发现相邻对等点并维护对等点连接,同时也会基于TCP协议建立对等点之间的信息交换通道。 Session层Peer管理主要管理节点与上层子协议的交互,而NodeTable管理主要管理底层基于udp协议构建的节点连接表。
因此,p2p层的peer节点需要能够获取和开启子协议,还需要能够ping通其他节点,当然也需要能够接收来自其他节点的消息。 这体现在p2p/peer.go中的三个重要功能模块,pingLoop、readLoop、startProtocols,也就是peer.run()方法中的三个函数。
func (p *Peer) run() (remoteRequested bool, err error) {
var (
writeStart = make(chan struct{}, 1)
writeErr = make(chan error, 1)
readErr = make(chan error, 1)
reason DiscReason // sent to the peer
)
p.wg.Add(2)
go p.readLoop(readErr)
go p.pingLoop()
// Start all protocol handlers.
writeStart <- struct{}{}
p.startProtocols(writeStart, writeErr)//开启子协议handle
loop:... // 省略等待error和断开连接的loop代码段
}
close(p.closed)
p.rw.close(reason)
p.wg.Wait()
return remoteRequested, err
}
// Peer represents a connected remote node.
type Peer struct {
rw *conn
running map[string]*protoRW
log log.Logger
created mclock.AbsTime
wg sync.WaitGroup
protoErr chan error
closed chan struct{}
disc chan DiscReason
// events receives message send / receive events if set
events *event.Feed
}
可以看出,上图是底层的peer结构,下图是子协议层的peer结构。 不同的子协议层会有不同的peer结构,这就是为什么会有eth/peer.go和les/peer.go。 毕竟不同的子协议适用于不同的数据场景。
type peer struct {
id string
*p2p.Peer //可以看出其实他是对网络层的peer节点进行封装的顶层节点。
rw p2p.MsgReadWriter
version int // Protocol version negotiated
forkDrop *time.Timer // Timed connection dropper if forks aren't validated in time
head common.Hash
td *big.Int
lock sync.RWMutex
knownTxs mapset.Set // Set of transaction hashes known to be known by this peer
knownBlocks mapset.Set // Set of block hashes known to be known by this peer
queuedTxs chan []*types.Transaction // Queue of transactions to broadcast to the peer
queuedProp s chan *propEvent // Queue of blocks to broadcast to the peer
queuedAnns chan *types.Block // Queue of blocks to announce to the peer
term chan struct{} // Termination channel to stop the broadcaster
}
我们可以看到,数据载体有底层peer和子协议层peer,数据同步需要使用通信协议来管理和连接两层peer。 底层实现peer消息分发和消息监听,子协议层peer进行数据下载和同步,这两层之间的管理和通信需要非常重要的protocolManger。
数据同步过程
那么如何启动protocolManager呢? 上面我们介绍了peer结构,主要代表其他远程节点。 在以太坊中,还有一个同样代表节点的结构Node。 主要代表本地节点本身会略有不同,节点要启动p2p网络。 需要依赖本地Node模块,使用Node.Start()函数启动两个任务,一个是启动Ethereum Service,Ethereum.start启动protocolmanager; 另一种是启动p2p.Server,创建并刷新K bucket,开启UDP端口监听,同时监听TCP端口,处理远程节点发来的消息。
protocolManager在启动时,需要连接底层逻辑层peer(p2p)和顶层协议层peer(eth):
当protocolManager启动时,节点会先初始化并调用NewProtocolManager。 初始化过程中会调用SubProtocol函数从地牢p2p.peer中获取一个消息读写通道,然后构造一个消息处理器Handle,Handle。 请求同步的消息也会接受节点响应的消息。 在protocolManger的start方法中,他会启动一个定时同步的协程syncer,他会根据消息通知调用fecher或者downloader执行器进行数据同步。
1、主动数据同步是指本节点自发地向相邻节点请求区块数据。 数据入口在 eth/downloader/downloader 和 eth/handler
2. 被动数据同步是指本地节点接收其他节点的数据同步消息(Message),然后请求区块数据。 数据条目位于 eth/fetcher/fetcher.go
我们以主动同步为例。 当下载器执行器被调用时,它首先会调用findAncestor来寻找本地链和远程链的公共祖先。 从共同祖先开始,它会配置4个fetcher,分别是fetcherHeader、fetcherBodies、fetcherReceipts,ProcessHeaders最后根据同步方式调用processFullSyncContent和processFastSyncContent,最后调用spawnSync进行同步。
func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td *big.Int) (err error) {
//省略部分代码
...
...
// 获取区块高度
latest, err := d.fetchHeight(p)
if err != nil {
return err
}
height := latest.Number.Uint64()
//获取共同祖先
origin, err := d.findAncestor(p, latest)
if err != nil {
return err
}
//
...
...
//
// 这里他们是为了确保其实中心点在任何快速同步的中心点之前。快速同步使用的是二分查找的方法寻找共同祖先所以这里需要进行验证。
pivot := uint64(0)
if d.mode == FastSync {
if height <= uint64(fsMinFullBlocks) {
origin = 0
} else {
pivot = height - uint64(fsMinFullBlocks)
if pivot <= origin {
origin = pivot - 1
}
}
}
d.committed = 1
if d.mode == FastSync && pivot != 0 {
d.committed = 0
}
//省略部分代码
...
...
//
//构造四个fetcher,获取数据,放在缓存中。
fetchers := []func() error{
func() error { return d.fetchHeaders(p, origin+1, pivot) }, // Headers are always retrieved
func() error { return d.fetchBodies(origin + 1) }, // Bodies are retrieved during normal and fast sync
func() error { return d.fetchReceipts(origin + 1) }, // Receipts are retrieved during fast sync
func() error { return d.processHeaders(origin+1, pivot, td) },
}
//根据不同的同步模式,快速同步则加入processFastSyncContent进入fetchers,完整同步则加入processFullSyncContent
if d.mode == FastSync {
fetchers = append(fetchers, func() error { return d.processFastSyncContent(latest) })
} else if d.mode == FullSync {
fetchers = append(fetchers, d.processFullSyncContent)
}
//最后执行同步,
return d.spawnSync(fetchers)
}
func (d *Downloader) spawnSync(fetchers []func() error) error {
//省略部分代码
...
//
//在spawnSync中会执行fetchers中的5个fetcher
for _, fn := range fetchers {
fn := fn
go func() { defer d.cancelWg.Done(); errc <- fn() }()
}
var err error
//这里会监听fetchers的error返回,当执行出错时关闭queue队列并取消下载
for i := 0; i < len(fetchers); i++ {
if i == len(fetchers)-1 {
// Close the queue when all fetchers have exited.
// This will cause the block processor to end when
// it has processed the queue.
d.queue.Close()
}
if err = <-errc; err != nil {
break
}
}
d.queue.Close()
d.Cancel()
return err
}
以上是同步过程的源代码。 从源码可以看出,主要的下载任务都集中在他构建的可扩展的fetchers中,也就是一个fetcher任务队列。 根据不同的数据同步策略,不仅fetcher任务队列的结构不同,fetcher之间的协作过程也不同。
数据同步策略
fetcher task queue的作用概括起来就是进行中,数据填充,数据组装,最后数据插入。 填充什么样的数据,需要组装哪些数据,什么时候插入数据,都由一个重要的数据结构模式标识,代表了数据同步策略,主要分为三种。
1. lightSync:轻节点同步,数据填充时只填充区块头数据,不需要数据组装,调用insertHeaderchain直接插入区块头。
2.fullSync:全节点同步。 填充数据时,需要填写区块头和区块体,但不填写交易回执,将数据组装到结果集Result中,然后调用importBlockResults将结果集中的数据插入到主链中,与lightSync的区别在于light sync没有块体,所以不会执行和验证交易,而fullSync全节点同步会在插入数据的同时执行交易和验证,这也是它的同步速度较慢的原因,并且那么它会自己生成Transaction receipt Receipt,所以他在填写和组装的时候不需要Receipt参与。
3.fastSync:快速同步,数据填充时,会同时填充Header、Body、Receipt,然后拼装3条数据,进行数据插入,进行数据插入时,完全不同于fullSync和lightSync,他调用的commitFastSyncData函数不同于fullSync全节点同步调用的importBlockResults。 它不会执行交易,只提交同步数据,所以它增加了一个验证交易回执的步骤以确保安全。 这是快速同步的一部分。 为了保证数据安全,旧的区块将采用上述方式进行同步,时间较新的区块将采用fullSync同步方式进行同步。 也就是说,fastSync会快速同步大部分,小部分还是会按照fullSync的同步方式进行同步。
结语
以太坊数据同步非常复杂。 包括其网络架构、通信协议管理、数据传输通道的配合、数据同步策略的安排、数据同步速率的加速算法等。 有很多细节和逻辑关系。 数据同步是区块链节点与外界通信的基石,还有更多令人惊叹的架构彩蛋。 期待下一篇文章与大家分享。
了解创宇区块链实验室官网
创宇存证平台|知乎创宇唯一指定存证平台
联系我们