以太坊C++源碼解析(五)區(qū)塊鏈同步(1)

在p2p(6)那一節(jié)末尾我們涉及到了BlockChainSync::syncPeer()函數(shù)蛤签,實(shí)際上到這里已經(jīng)進(jìn)入了另外一個(gè)重要模塊:區(qū)塊鏈同步模塊,這個(gè)模塊算是P2P模塊交互模塊震肮。

我們知道區(qū)塊鏈?zhǔn)且粋€(gè)分布式賬本,在所有的全節(jié)點(diǎn)上都有區(qū)塊鏈的一個(gè)完整副本鲫尊,這些全節(jié)點(diǎn)之間是相互同步的關(guān)系沦偎。當(dāng)我們?cè)诒镜卮罱ê靡粋€(gè)全節(jié)點(diǎn)時(shí),首先需要從其他節(jié)點(diǎn)把所有區(qū)塊同步過來鸿捧,目前以太坊mainnet鏈有600多萬個(gè)區(qū)塊疙渣,ropsten測(cè)試鏈有400多萬個(gè)區(qū)塊,具體的區(qū)塊信息可以在Etherscan網(wǎng)站查詢到泼菌。
如果想要在鏈上發(fā)送一個(gè)交易啦租,必須要等到本地區(qū)塊鏈同步接近最新塊,否則交易不會(huì)被廣播出來篷角。換句話說,區(qū)塊鏈同步接近完成是進(jìn)行交易的前提條件虐块!這里用接近完成而不用完成是因?yàn)閰^(qū)塊同步永遠(yuǎn)都不會(huì)完成嘉蕾,以太坊差不多10多秒就會(huì)產(chǎn)生一個(gè)新的區(qū)塊。
幾百萬個(gè)區(qū)塊的同步是一個(gè)相當(dāng)漫長且痛苦的過程儡率,我目前同步的是ropsten測(cè)試鏈,也許是鏈上經(jīng)常存在攻擊儿普,也許是中國這邊節(jié)點(diǎn)少,同步過程相當(dāng)不穩(wěn)定箕肃,快的時(shí)候一晚上能同步50萬個(gè)區(qū)塊,慢的時(shí)候卡在某個(gè)區(qū)塊一動(dòng)不動(dòng)好幾天障贸。
因此非常有必要深入了解區(qū)塊鏈的同步過程吟宦。

題外話不多說了,還是從BlockChainSync::syncPeer()函數(shù)開始吧

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

開頭有一個(gè)判斷條件m_state == SyncState::Waiting袁波,這是一個(gè)是否同步的開關(guān)篷牌,從別的節(jié)點(diǎn)peer同步過來的區(qū)塊是放到一個(gè)緩存里的,當(dāng)這個(gè)緩存滿的時(shí)候枷颊,開關(guān)關(guān)閉脉让,同步會(huì)暫時(shí)中止。

u256 td = host().chain().details().totalDifficulty;
if (host().bq().isActive())
    td += host().bq().difficulty();

u256 syncingDifficulty = std::max(m_syncingTotalDifficulty, td);

這段代碼是計(jì)算本地當(dāng)前同步的區(qū)塊鏈的總難度隔缀。

區(qū)塊鏈礦工競(jìng)爭(zhēng)是通過難度來衡量的,所有節(jié)點(diǎn)傾向于相信難度大的區(qū)塊

如果該節(jié)點(diǎn)peer的總難度比我自身難度大猾瘸,那么就需要從該節(jié)點(diǎn)同步(這里有一個(gè)漏洞,如果有人偽造一個(gè)非常大的難度牵触,那么本節(jié)點(diǎn)會(huì)一直從對(duì)方同步,直到一個(gè)新的更大難度的節(jié)點(diǎn)出現(xiàn),這樣可能會(huì)導(dǎo)致同步卡住)
m_state表示同步的狀態(tài)渊鞋,當(dāng)m_stateSyncState::Idle或者SyncState::NotSynced時(shí)瞧挤,同步就真正開始了特恬!

區(qū)塊分為區(qū)塊頭和區(qū)塊體,這兩部分是分別下載的癌刽。

首先下載的是對(duì)方節(jié)點(diǎn)最新塊的區(qū)塊頭尝丐,也就是:

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

這里調(diào)用的是EthereumPeer::requestBlockHeaders()函數(shù)。
反之如果該節(jié)點(diǎn)難度沒有我自身難度大远荠,并且之前同步過區(qū)塊頭的話失息,就準(zhǔn)備同步區(qū)塊體,也就是:

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

我們先來看看EthereumPeer::requestBlockHeaders()函數(shù)的實(shí)現(xiàn)盹兢。

EthereumPeer類里有兩個(gè)requestBlockHeaders()函數(shù),分別是按區(qū)塊號(hào)來同步和按區(qū)塊hash值來同步浦妄,這里調(diào)用的是后者替裆。

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);
}

這個(gè)函數(shù)比較簡單,就是向?qū)Ψ桨l(fā)送一個(gè)GetBlockHeadersPacket數(shù)據(jù)包宜咒。那么對(duì)方接到這個(gè)包以后怎么回應(yīng)呢把鉴?照例到EthereumPeer::interpret()函數(shù)里去找:

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

可以看到這里主要是調(diào)用了hostData->blockHeaders()函數(shù)獲取區(qū)塊頭庭砍,并回復(fù)對(duì)方BlockHeadersPacket數(shù)據(jù)包。其中hostDataEthereumHostData類指針怠缸,blockId可能有兩個(gè)值,分別是區(qū)塊號(hào)或者區(qū)塊hash值扳炬,對(duì)應(yīng)前面兩個(gè)requestBlockHeaders()函數(shù)。maxHeaders是請(qǐng)求區(qū)塊頭的數(shù)量恨樟。
我們?cè)倏纯?code>EthereumHostData::blockHeaders()函數(shù)實(shí)現(xiàn):
這個(gè)函數(shù)有點(diǎn)長,先貼一部分代碼吧:

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這個(gè)值是需要發(fā)送的最大區(qū)塊頭數(shù)量缩多,_skip值為0养晋,因此step值為1。
接著判斷_blockId里是區(qū)塊hash還是區(qū)塊號(hào)匙握,貼出來的這部分代碼是區(qū)塊hash,處理區(qū)塊號(hào)那部分代碼類似秦忿,有興趣可以自己去看蛾娶。

if (!m_chain.isKnown(blockHash))
    blockHash = {};

這里是判斷如果該區(qū)塊hash不在我本地區(qū)塊鏈里,則不返回任何東西蛔琅。
_reverse值為false,取出blockHash對(duì)應(yīng)的塊號(hào)n辜窑,計(jì)算要取的最高塊號(hào)top寨躁,再得到當(dāng)前區(qū)塊鏈最新塊號(hào)lastBlock,判斷邊界條件职恳,top值不能超過lastBlock,如果超過了則top=lastBlock色徘,再算出top對(duì)應(yīng)的塊hash值blockHash操禀。

注意這里的blockHash是最高塊的hash值,為什么需要這個(gè)值呢?因?yàn)閰^(qū)塊鏈里區(qū)塊是像單向鏈表連接起來的蔑水,其中0號(hào)區(qū)塊是創(chuàng)世區(qū)塊扬蕊,后續(xù)區(qū)塊從1開始遞增丹擎,每個(gè)區(qū)塊里會(huì)記錄上一級(jí)區(qū)塊的hash值,相當(dāng)于是指向父區(qū)塊的指針再愈,因此我們遍歷的時(shí)候只能從后往前遍歷护戳。

區(qū)塊鏈?zhǔn)疽鈭D

接著往下看:

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

這里定義了一個(gè)函數(shù)nextHash()媳荒,用來從后向前遍歷區(qū)塊hash的。_h是當(dāng)前區(qū)塊hash钳枕,_step值為1。
可以看到這里對(duì)區(qū)塊做了一個(gè)分段衔沼,進(jìn)行了區(qū)別處理,如果_h所在區(qū)塊與最新區(qū)塊距離超過1000個(gè)塊指蚁,則采用區(qū)塊號(hào)遞減方式來遍歷自晰,也就是按遍歷數(shù)組的方式遍歷,即_h = m_chain.numberHash(n - _step);缘圈,否則按單向鏈表的方式遍歷,即_h = details.parent;糟把。
最后一部分準(zhǔn)備返回?cái)?shù)據(jù)

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

把需要返回的區(qū)塊頭放到rlp中牲剃,并統(tǒng)計(jì)返回的區(qū)塊頭數(shù)量itemCount

從這里可以看到有時(shí)候itemCount是0的缠犀,也就是可以不返回任何區(qū)塊頭数苫,在實(shí)際同步中會(huì)經(jīng)常碰到這種情況虐急。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末滔迈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子敬惦,更是在濱河造成了極大的恐慌,老刑警劉巖俄删,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奏路,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡迅矛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門秽褒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來销斟,“玉大人,你說我怎么就攤上這事蚂踊”仕蓿” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵泼橘,是天一觀的道長。 經(jīng)常有香客問我醋粟,道長,這世上最難降的妖魔是什么米愿? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮较鼓,結(jié)果婚禮上违柏,老公的妹妹穿的比我還像新娘。我一直安慰自己勇垛,他們只是感情好闲孤,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布烤礁。 她就那樣靜靜地躺著,像睡著了一般脚仔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲤脏,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天猎醇,我揣著相機(jī)與錄音,去河邊找鬼硫嘶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛沦疾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哮塞,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼忆畅,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起剔交,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤改衩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后葫督,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡偎快,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年洽胶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姊氓。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖翔横,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情效览,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布钦铺,位于F島的核電站肢预,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏烫映。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一抽兆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辫红,春花似錦、人聲如沸贴妻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娩鹉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間弯予,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工受楼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓等舔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親慌植。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容