仕様把握
まずは仕様を把握しようということで、 bitcoin wiki を見る。
ブロックロケーターオブジェクトの最後の既知のハッシュの直後から、hash_stopまたは2000ブロックのいずれか早い方までのブロックのヘッダーを含むヘッダーパケットを返します。次のブロックヘッダーを受信するには、新しいブロックロケーターオブジェクトを使用してgetheadersを再度発行する必要があります。ブロックロケーターオブジェクトに無効なブランチのハッシュが含まれている場合、一部のクライアントは無効なブロックのヘッダーを提供する場合があることに注意してください。
前提
今回は Inital Block Download で、ブロックヘッダを要求するために投げられてきた getheaders を処理する想定で見ていきます。
この場合のパラメータは以下になります。
locator_hashes: [0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206]
stop_hash: 0000000000000000000000000000000000000000000000000000000000000000
locator_hashes は genesis ブロックのハッシュ値です。これは regtest のときのgenesis ブロックです。
stop_hash はどこまでのヘッダを取得するかを指定するパラメータですが、このようにすべて0で指定すると、可能な限りたくさん返してもらえるはずです。最大で2000ブロック分取得可能なようです。
読んでいく!
code: c++
CBlockLocator locator;
uint256 hashStop;
vRecv >> locator >> hashStop;
パラメータを変数にロードしています。
code:cpp
if (locator.vHave.size() > MAX_LOCATOR_SZ) {
LogPrint(BCLog::NET, "getheaders locator size %lld > %d, disconnect peer=%d\n", locator.vHave.size(), MAX_LOCATOR_SZ, pfrom->GetId());
pfrom->fDisconnect = true;
return true;
}
locator のバリデーションをやっています。MAX_LOCATOR_SZ は 101 に設定されているようですね。これを上回る場合には peer との接続を切るようです。locator の数が101を上回ることは考えにくいので、そのような不正な値を送ってくるpeerは攻撃者とみなして接続を切るということだと思います。
code:cpp
LOCK(cs_main);
排他制御のための記述ですね。cs_main というクリティカルセクションをロックしています。cs_main で守られているリソースを使うときにはロックする必要があります。
code:cpp
if (::ChainstateActive().IsInitialBlockDownload() && !pfrom->HasPermission(PF_NOBAN)) {
LogPrint(BCLog::NET, "Ignoring getheaders from peer=%d because node is in initial block download\n", pfrom->GetId());
return true;
}
この処理は、nodeが IBD(Initial Block Download) の途中だったら何もせずに getheaders メッセージの処理を終えるための処理です。
IBDというのは、ノードを起動したときに、ネットワークから遅れている分のブロックをまとめてダウンロードする処理のことです。IBD中の node は最新のブロックを持っていませんので、外からブロックヘッダを要求されても無視して、送信したピアには他のピアを探してもらうほうが良いということかと思います。
!pfrom->HasPermission(PF_NOBAN)の部分が気になりますね。PF_NOBAN は ピアの認可制御のためのフラグで、「誤った振る舞いをしてもBanしない」という意味です。ここでは、このフラグが立っている場合は、要求に応じてIBD中でも応答する挙動になるようです。
code:cpp
CNodeState *nodestate = State(pfrom->GetId());
const CBlockIndex* pindex = nullptr;
if (locator.IsNull())
{
// If locator is null, return the hashStop block
pindex = LookupBlockIndex(hashStop);
if (!pindex) {
return true;
}
if (!BlockRequestAllowed(pindex, chainparams.GetConsensus())) {
LogPrint(BCLog::NET, "%s: ignoring request from peer=%i for old block header that isn't in the main chain\n", __func__, pfrom->GetId());
return true;
}
}
else
{
// Find the last block the caller has in the main chain
pindex = FindForkInGlobalIndex(::ChainActive(), locator);
if (pindex)
pindex = ::ChainActive().Next(pindex);
}
その側の if 文は、locator が指定されているかどうかで分岐しています。
「locator がnullなら、hashStop のブロックを返す」とコメントにあります。
if 文のブロックでやっているのは、hashStop に対応するブロックがない場合と、そのブロックがある条件を満たさない場合にメッセージを無視する処理です。
ある条件というのは BlockRequestAllowed() の処理の内容で、基本的にはアクティブチェーンに含まれないものは無視する。例外として以下のコメントにあるものだけを無視しない(つまり送り返す)という事であるようです。
BlockRequestAllowed()関数のコメントには以下のようにあります。
// To prevent fingerprinting attacks, only send blocks/headers outside of the
// active chain if they are no more than a month older (both in time, and in
// best equivalent proof of work) than the best header chain we know about and
// we fully-validated them at some point.
↓適当訳
フィンガープリント攻撃を防ぐために、アクティブチェーンの外にあるブロック/ヘッダは、それらが1ヶ月以上古くなく(時間的にも、PoWでも)、知る限り最長のヘッダーチェーンであり、どこかの時点で完全に検証されている場合だけ送る。
if (locator.IsNull())が false のときの処理を見ます。
code:cpp
// Find the last block the caller has in the main chain
pindex = FindForkInGlobalIndex(::ChainActive(), locator);
if (pindex)
pindex = ::ChainActive().Next(pindex);
アクティブチェーンと locator に共通するブロックを探しています。見つけたら、共通するブロックの次のブロックのインデックスを pindex にセットしています。
locatorには送信ピアのtipからgenesisまでのブロックハッシュの抜粋が入っています。getheadersメッセージを受け取った側としては、自分のアクテイブチェーンが正しいチェーンなはずなので、そのtipまでの情報を相手におくって、同期させる必要があります。そこで、共通の祖先を探して、そこから自分のtipまでのブロックヘッダを返してあげることで、自分のチェーンを相手に同期させることが出来ます。
ここまでで pindex にピアへ送る最初のブロックのインデックスが入った状態になっています。
次を見ましょう。
code:cpp
// we must use CBlocks, as CBlockHeaders won't include the 0x00 nTx count at the end
std::vector<CBlock> vHeaders;
int nLimit = MAX_HEADERS_RESULTS;
LogPrint(BCLog::NET, "getheaders %d to %s from peer=%d\n", (pindex ? pindex->nHeight : -1), hashStop.IsNull() ? "end" : hashStop.ToString(), pfrom->GetId());
for (; pindex; pindex = ::ChainActive().Next(pindex))
{
vHeaders.push_back(pindex->GetBlockHeader());
if (--nLimit <= 0 || pindex->GetBlockHash() == hashStop)
break;
}
MAX_HEADERS_RESULTS は 2000がセットされています。
ここではブロックヘッダをpindex から1つずつたどりながら vHeaders ベクターに追加していっています。
追加したブロックが2000個に達するか、hashStop で指定したブロックに達するまでループを続けます。
ブロックヘッダを扱えればよいのに CBlockHeader ではなく CBlock クラスを使っているのは、コメントにある通り、トランザクション数のフィールドを含めるためです。しかし、この値は常に0がセットされて送られます。
getheadersメッセージの処理もクライマックスです。最後の部分を見ていきます。
code:cpp
// pindex can be nullptr either if we sent ::ChainActive().Tip() OR
// if our peer has ::ChainActive().Tip() (and thus we are sending an empty
// headers message). In both cases it's safe to update
// pindexBestHeaderSent to be our tip.
//
// It is important that we simply reset the BestHeaderSent value here,
// and not max(BestHeaderSent, newHeaderSent). We might have announced
// the currently-being-connected tip using a compact block, which
// resulted in the peer sending a headers request, which we respond to
// without the new block. By resetting the BestHeaderSent, we ensure we
// will re-announce the new block via headers (or compact blocks again)
// in the SendMessages logic.
nodestate->pindexBestHeaderSent = pindex ? pindex : ::ChainActive().Tip();
connman->PushMessage(pfrom, msgMaker.Make(NetMsgType::HEADERS, vHeaders));
return true;
コメントの訳
pindex は、::ChainActive().Tip() を送っても、ピアが ::ChainActive().Tip()を持っていて(そのためからの headers メッセージを送っても)もどちらの場合も nullptr になりえます。この両方のケースにおいて、pindexBestHeaderSent を tipとして更新しても安全です。
max(BestHeaderSent, newHeaderSent) を取るのではなく、単純に BestHeaderSent の値をここでリセットすることは重要です。コンパクトブロックを使用して現在接続されている tip をアナウンスした可能性があります。その結果、ピアはヘッダー要求を送信し、新しいブロックなしで応答します。BestHeaderSentをリセットすることにより、SendMessagesロジックのヘッダー(またはコンパクトブロック)を介して新しいブロックを再アナウンスします。
処理内容は、コメントにある通り、pindexBestHeaderSent を pindex があれば pindex で、なければアクテイブチェーンの tip で更新します。
最後に、headers メッセージを送ります。
以上!
前提のパラメータを設定したものの、処理内容がシンプルだったので、あまり分岐もなく使いませんでしたね。