以太坊C++源码解析(五)区块链同步(1)

在p2p(6)那一节末尾我们涉及到了BlockChainSync::syncPeer()函数,实际上到这里已经进入了另外一个重要模块:区块链同步模块,这个模块算是P2P模块交互模块。

我们知道区块链是一个分布式账本,在所有的全节点上都有区块链的一个完整副本,这些全节点之间是相互同步的关系。当我们在本地搭建好一个全节点时,首先需要从其他节点把所有区块同步过来,目前以太坊mainnet链有600多万个区块,ropsten测试链有400多万个区块,具体的区块信息可以在Etherscan网站查询到。
如果想要在链上发送一个交易,必须要等到本地区块链同步接近最新块,否则交易不会被广播出来。换句话说,区块链同步接近完成是进行交易的前提条件!这里用接近完成而不用完成是因为区块同步永远都不会完成,以太坊差不多10多秒就会产生一个新的区块。
几百万个区块的同步是一个相当漫长且痛苦的过程,我目前同步的是ropsten测试链,也许是链上经常存在攻击,也许是中国这边节点少,同步过程相当不稳定,快的时候一晚上能同步50万个区块,慢的时候卡在某个区块一动不动好几天。
因此非常有必要深入了解区块链的同步过程。

题外话不多说了,还是从BlockChainSync::syncPeer()函数开始吧

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";

        // start sync
        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同步过来的区块是放到一个缓存里的,当这个缓存满的时候,开关关闭,同步会暂时中止。

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_stateSyncState::Idle或者SyncState::NotSynced时,同步就真正开始了!

区块分为区块头和区块体,这两部分是分别下载的。

首先下载的是对方节点最新块的区块头,也就是:

_peer->requestBlockHeaders(_peer->m_latestHash, 1, 0, false);

这里调用的是EthereumPeer::requestBlockHeaders()函数。
反之如果该节点难度没有我自身难度大,并且之前同步过区块头的话,就准备同步区块体,也就是:

if (m_state == SyncState::Blocks)
{
    requestBlocks(_peer);
    return;
}

我们先来看看EthereumPeer::requestBlockHeaders()函数的实现。

EthereumPeer类里有两个requestBlockHeaders()函数,分别是按区块号来同步和按区块hash值来同步,这里调用的是后者。

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()函数里去找:

case GetBlockHeadersPacket:
{
    /// Packet layout:
    /// [ block: { P , B_32 }, maxHeaders: P, skip: P, reverse: P in { 0 , 1 } ]
    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数据包。其中hostDataEthereumHostData类指针,blockId可能有两个值,分别是区块号或者区块hash值,对应前面两个requestBlockHeaders()函数。maxHeaders是请求区块头的数量。
我们再看看EthereumHostData::blockHeaders()函数实现:
这个函数有点长,先贴一部分代码吧:

auto numHeadersToSend = _maxHeaders;

auto step = static_cast<unsigned>(_skip) + 1;
assert(step > 0 && "step must not be 0");

h256 blockHash;
if (_blockId.size() == 32) // block id is a hash
{
    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)); // override start block hash with the hash of the top block we have
        }
        else
            blockHash = {};
    }
}

numHeadersToSend这个值是需要发送的最大区块头数量,_skip值为0,因此step值为1。
接着判断_blockId里是区块hash还是区块号,贴出来的这部分代码是区块hash,处理区块号那部分代码类似,有兴趣可以自己去看。

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值,相当于是指向父区块的指针,因此我们遍历的时候只能从后往前遍历。

区块链示意图

接着往下看:

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; // find the number of the block below which we don't expect BC changes.

    while (_step) // parent hash traversal
    {
        auto details = m_chain.details(_h);
        if (details.number < limitBlock)
            break; // stop using parent hash traversal, fallback to using block numbers
        _h = details.parent;
        --_step;
    }

    if (_step) // still need lower block
    {
        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;
最后一部分准备返回数据

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的,也就是可以不返回任何区块头,在实际同步中会经常碰到这种情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容