在p2p(6)那一节末尾我们涉及到了BlockChainSync::syncPeer()
函数,实际上到这里已经进入了另外一个重要模块:区块链同步 模块,这个模块算是P2P模块交互模块。
我们知道区块链是一个分布式账本,在所有的全节点上都有区块链的一个完整副本,这些全节点之间是相互同步的关系。当我们在本地搭建好一个全节点时,首先需要从其他节点把所有区块同步过来,目前以太坊mainnet链有600多万个区块,ropsten测试链有400多万个区块,具体的区块信息可以在Etherscan 网站查询到。 如果想要在链上发送一个交易,必须要等到本地区块链同步接近最新块,否则交易不会被广播出来。换句话说,区块链同步接近完成是进行交易的前提条件!这里用接近 完成而不用完成是因为区块同步永远都不会完成,以太坊差不多10多秒就会产生一个新的区块。 几百万个区块的同步是一个相当漫长且痛苦的过程,我目前同步的是ropsten测试链,也许是链上经常存在攻击,也许是中国这边节点少,同步过程相当不稳定,快的时候一晚上能同步50万个区块,慢的时候卡在某个区块一动不动好几天。 因此非常有必要深入了解区块链的同步过程。
题外话不多说了,还是从BlockChainSync::syncPeer()
函数开始吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void BlockChainSync::syncPeer(std ::shared_ptr <EthereumPeer> _peer, bool _force){ if (m_state == SyncState::Waiting) return ; u256 td = host().chain().details().totalDifficulty; if (host().bq().isActive()) td += host().bq().difficulty(); u256 syncingDifficulty = std ::max(m_syncingTotalDifficulty, td); if (_force || _peer->m_totalDifficulty > syncingDifficulty) { if (_peer->m_totalDifficulty > syncingDifficulty) LOG(m_logger) << "Discovered new highest difficulty" ; m_syncingTotalDifficulty = _peer->m_totalDifficulty; if (m_state == SyncState::Idle || m_state == SyncState::NotSynced) { LOG(m_loggerInfo) << "Starting full sync" ; m_state = SyncState::Blocks; } _peer->requestBlockHeaders(_peer->m_latestHash, 1 , 0 , false ); _peer->m_requireTransactions = true ; return ; } if (m_state == SyncState::Blocks) { requestBlocks(_peer); return ; } }
开头有一个判断条件m_state == SyncState::Waiting
,这是一个是否同步的开关,从别的节点peer同步过来的区块是放到一个缓存里的,当这个缓存满的时候,开关关闭,同步会暂时中止。
1 2 3 4 5 u256 td = host().chain().details().totalDifficulty; if (host().bq().isActive()) td += host().bq().difficulty(); u256 syncingDifficulty = std ::max(m_syncingTotalDifficulty, td);
这段代码是计算本地当前同步的区块链的总难度。
区块链矿工竞争是通过难度来衡量的,所有节点倾向于相信难度大的区块
如果该节点peer的总难度比我自身难度大,那么就需要从该节点同步(这里有一个漏洞,如果有人伪造一个非常大的难度,那么本节点会一直从对方同步,直到一个新的更大难度的节点出现,这样可能会导致同步卡住 )
m_state
表示同步的状态,当m_state
为SyncState::Idle
或者SyncState::NotSynced
时,同步就真正开始了!
区块分为区块头和区块体,这两部分是分别下载的。
首先下载的是对方节点最新块的区块头,也就是:
1 _peer->requestBlockHeaders(_peer->m_latestHash, 1 , 0 , false );
这里调用的是EthereumPeer::requestBlockHeaders()
函数。 反之如果该节点难度没有我自身难度大,并且之前同步过区块头的话,就准备同步区块体,也就是:
1 2 3 4 5 if (m_state == SyncState::Blocks){ requestBlocks(_peer); return ; }
我们先来看看EthereumPeer::requestBlockHeaders()
函数的实现。
在EthereumPeer
类里有两个requestBlockHeaders()
函数,分别是按区块号来同步和按区块hash值来同步,这里调用的是后者。
1 2 3 4 5 6 7 8 9 10 11 void EthereumPeer::requestBlockHeaders(h256 const & _startHash, unsigned _count, unsigned _skip, bool _reverse){ setAsking(Asking::BlockHeaders); RLPStream s; prep(s, GetBlockHeadersPacket, 4 ) << _startHash << _count << _skip << (_reverse ? 1 : 0 ); LOG(m_logger) << "Requesting " << _count << " block headers starting from " << _startHash << (_reverse ? " in reverse" : "" ); m_lastAskedHeaders = _count; sealAndSend(s); }
这个函数比较简单,就是向对方发送一个GetBlockHeadersPacket
数据包。那么对方接到这个包以后怎么回应呢?照例到EthereumPeer::interpret()
函数里去找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 case GetBlockHeadersPacket:{ const auto blockId = _r[0 ]; const auto maxHeaders = _r[1 ].toInt<u256>(); const auto skip = _r[2 ].toInt<u256>(); const auto reverse = _r[3 ].toInt<bool >(); auto numHeadersToSend = maxHeaders <= c_maxHeadersToSend ? static_cast <unsigned >(maxHeaders) : c_maxHeadersToSend; if (skip > std ::numeric_limits<unsigned >::max() - 1 ) { cnetdetails << "Requested block skip is too big: " << skip; break ; } pair<bytes, unsigned > const rlpAndItemCount = hostData->blockHeaders(blockId, numHeadersToSend, skip, reverse); RLPStream s; prep(s, BlockHeadersPacket, rlpAndItemCount.second).appendRaw(rlpAndItemCount.first, rlpAndItemCount.second); sealAndSend(s); addRating(0 ); break ; }
可以看到这里主要是调用了hostData->blockHeaders()
函数获取区块头,并回复对方BlockHeadersPacket
数据包。其中hostData
是EthereumHostData
类指针,blockId
可能有两个值,分别是区块号或者区块hash值,对应前面两个requestBlockHeaders()
函数。maxHeaders
是请求区块头的数量。 我们再看看EthereumHostData::blockHeaders()
函数实现: 这个函数有点长,先贴一部分代码吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 auto numHeadersToSend = _maxHeaders;auto step = static_cast <unsigned >(_skip) + 1 ;assert(step > 0 && "step must not be 0" ); h256 blockHash; if (_blockId.size() == 32 ) { blockHash = _blockId.toHash<h256>(); if (!m_chain.isKnown(blockHash)) blockHash = {}; else if (!_reverse) { auto n = m_chain.number(blockHash); if (numHeadersToSend == 0 ) blockHash = {}; else if (n != 0 || blockHash == m_chain.genesisHash()) { auto top = n + uint64_t (step) * numHeadersToSend - 1 ; auto lastBlock = m_chain.number(); if (top > lastBlock) { numHeadersToSend = (lastBlock - n) / step + 1 ; top = n + step * (numHeadersToSend - 1 ); } assert(top <= lastBlock && "invalid top block calculated" ); blockHash = m_chain.numberHash(static_cast <unsigned >(top)); } else blockHash = {}; } }
numHeadersToSend
这个值是需要发送的最大区块头数量,_skip
值为0,因此step
值为1。 接着判断_blockId
里是区块hash还是区块号,贴出来的这部分代码是区块hash,处理区块号那部分代码类似,有兴趣可以自己去看。
1 2 if (!m_chain.isKnown(blockHash)) blockHash = {};
这里是判断如果该区块hash不在我本地区块链里,则不返回任何东西。_reverse
值为false,取出blockHash
对应的块号n
,计算要取的最高块号top
,再得到当前区块链最新块号lastBlock
,判断边界条件,top
值不能超过lastBlock
,如果超过了则top=lastBlock
,再算出top
对应的块hash值blockHash
。
注意这里的blockHash
是最高块的hash值,为什么需要这个值呢?因为区块链里区块是像单向链表连接起来的,其中0号区块是创世区块,后续区块从1开始递增,每个区块里会记录上一级区块的hash值,相当于是指向父区块的指针,因此我们遍历的时候只能从后往前遍历。
接着往下看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 auto nextHash = [this ](h256 _h, unsigned _step){ static const unsigned c_blockNumberUsageLimit = 1000 ; const auto lastBlock = m_chain.number(); const auto limitBlock = lastBlock > c_blockNumberUsageLimit ? lastBlock - c_blockNumberUsageLimit : 0 ; while (_step) { auto details = m_chain.details(_h); if (details.number < limitBlock) break ; _h = details.parent; --_step; } if (_step) { auto n = m_chain.number(_h); if (n >= _step) _h = m_chain.numberHash(n - _step); else _h = {}; } return _h; };
这里定义了一个函数nextHash()
,用来从后向前遍历区块hash的。_h
是当前区块hash,_step
值为1。 可以看到这里对区块做了一个分段,进行了区别处理,如果_h
所在区块与最新区块距离超过1000个块,则采用区块号递减方式来遍历,也就是按遍历数组的方式遍历,即_h = m_chain.numberHash(n - _step);
,否则按单向链表的方式遍历,即_h = details.parent;
。
最后一部分准备返回数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bytes rlp; unsigned itemCount = 0 ;vector <h256> hashes;for (unsigned i = 0 ; i != numHeadersToSend; ++i){ if (!blockHash || !m_chain.isKnown(blockHash)) break ; hashes.push_back(blockHash); ++itemCount; blockHash = nextHash(blockHash, step); } for (unsigned i = 0 ; i < hashes.size() && rlp.size() < c_maxPayload; ++i) rlp += m_chain.headerData(hashes[_reverse ? i : hashes.size() - 1 - i]); return make_pair(rlp, itemCount);
把需要返回的区块头放到rlp中,并统计返回的区块头数量itemCount
。
从这里可以看到有时候itemCount
是0的,也就是可以不返回任何区块头,在实际同步中会经常碰到这种情况。