BitcoinプロトコルをRustでお話してみる(後編)

蟹 お知らせ
Table of Contents

はじめに

この記事は
BitcoinプロトコルをRustでお話してみる(前編) | HashPort技術ブログ
この記事はBlockchain Advent Calendar 2019の13日目の記事です。初めにご存じの方も多いと思いますが、先月の11/15にBitcoin Cash(BTC)のハードフォーク(HF)がありました。BTCは律儀に半年に
(https://tech.fressets.com/bitcoinプロトコルをrustでお話してみる(前編)/)">bitcoinプロトコルをrustでお話してみる(前編)の続きの記事です。前回はBlockchain Advent Calendar 2019の中の記事の一つとして書きましたが、気がつくとあっという間に12月も終わってしまっていて、後編をエントリーし損ねました(^^;

今回は実際にブロック到達の通知を貰ってからブロック詳細情報を取ってくるところを書きたいと思います。

処理の大まかな流れ(おさらい)

前編でも書きましたが、処理の流れはこの様になっています。

  1. 繋ぎに行くサーバの情報を取得する
  2. サーバと接続して最初のご挨拶
  3. 相手が生きているかどうか、定期的にチェック
  4. ブロックが到達したという通知を待つ
  5. 「到達した」という情報が来たら、さらに詳しいブロック情報をリクエストしゲット!

前回はStep3までカバーしたので、今回はStep4と5の解説になります。連続した流れなので一つの章で説明しちゃいます。

ブロック情報の取得

新しいブロックやトランザクションの情報がフルノードサーバに届いた場合、その情報は繋がっているピア(接続しているホスト)に通知されます。それが Invメッセージになります。InvメッセージはボディにInventory構造体の配列を持ちます。そしてInventory構造体は以下のように定義されています。

pub struct Inventory {
     pub inv_type: InvType,
     pub hash: sha256d::Hash
}
pub enum InvType {
    Error,
    Transaction,
    Block,
    WitnessBlock,
    WitnessTransaction
}

これを受け取って処理する部分はこのようになります。

pub fn main() {
    ....

    loop {
        let message: Result<RawNetworkMessage, _> = reader.read_next();
        let payload = message.unwrap().payload;
        match payload {
          ...

            NetworkMessage::Inv(dat) => {
                for inv in dat {
                    if inv.inv_type == InvType::Block {
                        self.on_inv_block(inv);
                    }
                }
            }
        }
    }
}

Inventory構造体には、何に関する情報なのか(inv_type)と、ハッシュ値( hash )のみが含まれます。配列で渡されるそのデータを一つずつ確認して、タイプが Block だったらon_inv_block()というメソッドを呼びます。

fn on_inv_block(&mut self, inv: Inventory) {
    println!("{:?}", inv);
    self.send_getdata(inv);
}

fn send_getdata(&mut self, inv: Inventory) {
    Self::send_message(&mut self.stream, NetworkMessage::GetData([inv].to_vec()));
}

on_inv_block()の中でsend_getdata()を呼んでいて、そこで受け取ったInventory型のデータをそのまま渡します。そして、ピアに対してGetDataメッセージを送り、ブロックの詳細情報をリクエストします。

GetDataに対する返答がBlcokメッセージで返されます。ややこしいのですが、BlockメッセージのボディがBlockという名前の構造体で、中身は以下のようになっています。

pub struct BlockHeader {
    pub version: u32,
    pub prev_blockhash: sha256d::Hash,
    pub merkle_root: sha256d::Hash,
    pub time: u32,
    pub bits: u32,
    pub nonce: u32,
}

pub struct Block {
    pub header: BlockHeader,
    pub txdata: Vec<Transaction>
}

headerにはそのブロックのメタデータ(そのブロックと親ブロックのハッシュ、生成日時、Nonceなど)が含まれていて、そのブロックに含まれるトランザクションの配列がtxdata として続きます。

これを受け取るコードが以下になります。

fn store_block(&self, block: block::Block) {
    println!("Block: block_hash: {}, prev_hash: {}, timestamp: {:?}",
        block.bitcoin_hash(),
        block.header.prev_blockhash,
        Utc.timestamp(block.header.time.try_into().unwrap(), 0),
    );
}

pub fn main() {
    ....

    loop {
        let message: Result<RawNetworkMessage, _> = reader.read_next();
        let payload = message.unwrap().payload;
        match payload {
          ...

            NetworkMessage::Block(block)=> {
                self.store_block(block);
            }
        }
    }
}

本来であれば、受け取ったブロックをDBに格納して解析などに利用するところですが、Bitcoinのプロトコルを理解する上では本質的ではない部分なので割愛し、ここでは受け取った情報をそのまま標準出力に書き出しています。

まとめ

分量的にはだいぶコンパクトになりましたが、Bitcoinのプロトコルを用いてブロックの情報を入手する方法を解説してみました。Inv というメッセージにより新たに生成されたブロックの情報が伝播され、それを元に詳細な情報を入手する方法がわかったかと思います。

なお、今回利用した Bitcoinというクレートですが、一つ残念な部分があります。前編でも書いたようにBitcoinのプロトコルを話す最初にVersionメッセージを送るのですが、このクレートはバージョンが70001に固定になっています。Bitcoindのバージョンでいうと、2013年のv0.8.0相当。それ以降に追加されたメッセージタイプなどは残念ながらサポートされていません。ということで、このクレートをそのまま商用に使うのはちょっと厳しそうです。

一方で、オープンソースなので自分で変更を加えることは可能です。実際、Githubリポジトリのプルリクエストを見ると、例えばこれとかはBIP152のCompact Blockの対応を入れようとしてやり取りが続いています。そんな活動に足を踏み入れてOSSに貢献しながら、商用利用の道を探るというのも有りかも知れません。

タイトルとURLをコピーしました