bitcoinjの入門

【目次】

概要

このドキュメントはbitcoinjライブラリのバージョン0.14.5を使用する方法について説明していますGithubで公開されている内容と若干異なる部分がありますので注意して下さい。

bitcoinjはJava7で実装されており、JVMに対応した言語で使用する事が出来ます。このチュートリアルではJavaもしくはJavaScriptで使用することを想定しています。また、Python,Scala,Clojure,Kotlin,Rubyにも対応しており、多くの有名な言語をJVMで実装する事ができます。

注意としてこのチュートリアルでは、Bitcoinプロトコルの基本について熟知していることを前提としています。 もしブロックチェーンの構造やトランザクションの仕組みについて良く理解していない場合は、このチュートリアルを始める前にホワイトペーパーを読んでください。

何をしているのか理解できていないとコインを盗まれたり永久に紛失したりします。
このドキュメントはbitcoinjの使い方を学ぶことが出来ますが、まだ完全に網羅されている訳ではありません。疑問がある場合やコードレビューをしたい場合は、メーリングリストで他の方の意見を聞いてください。またソフトは最新のバージョンかどうか確認して下さい。バグの修正は常に行われ、ウォレットの安全性のために必要になります。bitcoinjのライブラリを常に新しいバージョンにすることが重要です。

初期設定

bitcoinjにはロギングとアサーションが組み込まれています。アサーションは、-eaを指定していなくても、デフォルトで常にチェックされます。 ロギングはSLF4Jライブラリ(Simple Logging Facade For Java)によって処理されます。 JDKロギングやAndroidロギングなど、使用するロギングシステムを選択することができます。デフォルトではstderr(標準エラー出力)に出力する単純なロガーを使用します。 libディレクトリのjarファイルを切り替えることで、新しいロガーを選択することができます。

bitcoinjはビルドシステムとしてMavenを使用しており、git経由で配布されています。bitcoinjはソースコード/ jarでダウンロードする事も出来ますが、リポジトリからソースコードを直接取得する方が安全です。

bitcoinjのソースコードを取得しインストールするには、MavenまたはGradleをインストールしパスに追加します。またgitがインストールされているか確認して下さい。 おそらくJavaIDEにはMavenかGradleとgitとの統合機能があります。

最新バージョンのコードを入手するにはGradleとMavenの使用方法に従って下さい(GradleもしくはMavenはどちらか一方で十分です)。コマンドを実行するだけで、正しいバージョンのコードが得られます。これはウィルスに感染したミラーやソースをダウンロードしてしまうことから保護します。なぜならgitはソースツリーのハッシュを使用し動作するので、正しい方法でソースハッシュを取得すれば正しいコードであることが保証されているからです。あなたはここで全てのプログラム確認する事ができます。

基本構造

bitcoinjのアプリケーションは5つのオブジェクトを使用します。

①"NetworkParameters"
ネットワークの選択を行います。

②"Wallet"
"ECKey"と他のデータを保存します

③"PeerGroup"
ネットワークの接続を管理します

④"BlockChain"
ブロックチェーンのデータを保存します

⑤"WalletEventListener"
ウォレットのイベントを受け取ります


"WalletAppKit"オブジェクトは、これらの設定を簡略化するために上記のオブジェクトを生成し、接続します。これは手動で行う事も出来ます。この記事で紹介するコードはアプリキットの使い方について示しています。

コードを見てどのように動作するか確認しましょう。

設定

log4jのユーティリティ機能を使用してより簡潔で冗長にならないログフォーマットを設定します。コマンドライン引数をチェックします。
※この設定はオプションなので特に設定しなくても問題ありません

//BriefLogFormatter:デフォルトよりも簡潔に出力を書き込むJavaロギングフォーマッタ
BriefLogFormatter.init();
if (args.length < 2) {
    System.err.println("Usage: address-to-send-back-to [regtest|testnet]");
    return;
}

次に、オプションのコマンドラインパラメータに基づいて使用するネットワークを選択します。

//どのネットワークに接続すべきかを明らかにする。 それぞれは別のファイルセットを取得します。
NetworkParameters params;
String filePrefix;
if (args[1].equals("testnet")) {
    params = TestNet3Params.get();
    filePrefix = "forwarding-service-testnet";
} else if (args[1].equals("regtest")) {
    params = RegTestParams.get();
    filePrefix = "forwarding-service-regtest";
} else {
    params = MainNetParams.get();
    filePrefix = "forwarding-service";
}

Bitcoinには複数の独立したネットワークがあります。

  • メインネットワーク:本番のネットワーク。
  • テストネットワーク:時々リセットされる、新しい機能についてテストするためのネットワーク。
  • リグレッションテストモード:パプリックネットワークではありません。-regtestをつけてbitcoinデーモンを実行する必要があります。

それぞれのネットワークには独自の起源ブロックを持っており、独自のポート番号とアドレスのプリフィックスバイトを使用しており、誤って違うネットワークにコインを送信しないように設計されています。これらは"NetworkParameters"シングルトンオブジェクトにカプセル化されています。ご覧の通り、各ネットワークには独自のクラスがあり、それらのオブジェクトの1つ"get()"を使い呼び出すことによって、関連する"NetworkParameters"オブジェクトを取得します。

ソフトを開発する時は、テストネットまたはリグレッションテストモードを使用する事をお勧めします。誤ってテストコインを紛失した場合、それは無価値なので大したことではなく、ここから無償で取得できます。

リグレッションテストモードはパブリックネットワークではありません。リグレッションテストモードはbitcoindで"bitcoind -regtest setgenerate true"を実行すると、ブロックを生成する時間を待つことなく、新しいブロックを生成することが出来ます。

キーとアドレス

Bitcoinのトランザクションにおいて、コインは一般的に公開楕円曲線鍵を使ったビットコインアドレスに送ります。送信者はトランザクションに受信者のアドレスを含めて作成します。アドレスは、公開鍵ハッシュが符号化された形式のものです。次にコインを送信する時には、受信者は自分の秘密鍵でコインの所有者として取引に署名します。キーは"ECKey"クラスで表されます。"ECKey"には、秘密鍵と公開鍵のみを含めることができます。楕円曲線暗号公開鍵は秘密鍵から導出されるので、秘密鍵を知ることは本質的に公開鍵を同じように知ることを意味します。これはRSA暗号のような、慣れ親しんだ他の暗号と異なります。

アドレスは、公開鍵をエンコーディングしたものです。 実際には、公開鍵の160ビットのハッシュ値、バージョンバイト、チェックサム、Base58と呼ばれるBitcoin固有のエンコーディングを使用してエンコードされています。 Base58は、1や大文字のiなど、混乱する可能性のある文字や数字を避けるように設計されています(Base64より6文字少ない)。

//最初のパラメータとして与えられたアドレスを解析します。
forwardingAddress = new Address(params, args[0]);

アドレスは、鍵が使用される予定のネットワークにおてエンコードするため、ここでネットワークパラメータ(第一引数)を渡す必要があります。 第二引数のパラメータは、ユーザーが指定した文字列です。 コンストラクタは、解析できない場合や間違ったネットワークだった場合にエラーをスローします。

ウォレットアプリキット

bitcoinjは各レイヤーによって構成され、それぞれのレイヤーが最後のレベルより低いレベルで動作します。コインを送受信をする典型的なアプリでは少なくとも
"BlockChain","BlockStore","PeerGroup","Wallet"が必要になります。それらの全ての
オブジェクトは正しくデータが流れるようにお互いを接続する必要があります。bitcoinjをベースにしたアプリで、データがどのように流れているのか詳しく知りたい場合は"どのようにお互いがフィットするか"を読んで下さい。

決まり文句になることが多いこのプロセスを単純化するために、"WalletAppKit"という高レベルのラッパーを提供しています。 これは、あなたが専門家でフルモードで開発していない限り、選択するのに最も適切なモードであるSPVモードでbitcoinjを設定します。 これは、いくつかの簡単なプロパティとフックを提供し、デフォルト設定を変更できるようにします。

将来的には、異なるニーズを持つ異なる種類のアプリケーションに対し、bitcoinjを別々に構成するキットが増えるかもしれません。 しかし、今のところは1つです。

//いくつかの定型文を自動化するクラスを使用して、基本的なアプリケーションを起動します。 常に少なくとも1つのキーがあることを確認してください。 
kit = new WalletAppKit(params, new File("."), filePrefix) {
    @Override
    protected void onSetupCompleted() {
        if (wallet().getKeyChainGroupSize() < 1)
            wallet().importKey(new ECKey());
    }
};
if (params == RegTestParams.get()) {
    kit.connectToLocalHost();
}
kit.startAsync();
kit.awaitRunning();

それでは上記のコードについて詳しく確認していきます。

kit = new WalletAppKit(params, new File("."), filePrefix)

このウォレットアプリキットでは3つの引数を取ります。

  • params:選択するネットワークパラメータ
  • new File("."):ファイルを格納するディレクト
  • filePrefix:作成されるファイルの接頭辞となるオプションの文字列

※filePrefixはウォレットのファイル名を決めるもので、複数の異なるbitcoinjアプリを同じディレクトリに保存する場合に便利に使用できます。 上記の例ではネットワークパラメータがメインネットなのでfilePrefixはforwarding-serviceになります。

@Override
    protected void onSetupCompleted() {

独自のコードを記述し独自オブジェクトを作ることが出来る、オーバーライド可能なメソッドを提供しています。
WalletAppKitは実際にバックグラウンドスレッド上にオブジェクトを作成し設定するので、onSetupCompletedメソッドはバックグラウンドスレッドから呼び出されます。

 if (wallet().getKeyChainGroupSize() < 1)
          wallet().importKey(new ECKey());

ウォレットに少なくとも1つの秘密鍵保有しているか確認をし、そうでない場合は新しく追加します。 ディスクからウォレットをロードすると、もちろんこのコードパスは実行されません。

if (params == RegTestParams.get()) {
    kit.connectToLocalHost();
}

リグレッションテストモードを使用しているかどうかを確認します。もし使用しているであれば、bitcoidでリグレッションテストモードが実行されるlocalhostに接続するよう、キットに指示します。

kit.startAsync();
kit.awaitRunning();

最後に"kit.startAsync()"を呼びます。WalletAppKitはGuava Serviceです。Guavaはgoogleの幅広く利用されているユーティリティライブラリで、いくつかの便利な機能を追加して標準Javaライブラリを拡張したものです。 WalletAppKitは起動と停止が可能なオブジェクトです(ただし、1回のみ)。起動または停止が完了するとコールバックを受け取ることができます。"awaitRunning()"で開始するまで他のスレッドの呼び出しをブロックすることもできます。

ブロックチェーンが完全に同期されるとWalletAppKitは開始されますが、しばらく時間がかかることがあります。 これをより速くする方法について学ぶことができますが、デモアプリにおいて追加で最適化を実装する必要はありません。

キットを構成しているオブジェクトにアクセスできるアクセサがあります。クラスが開始されるか、起動中になるまで、これらのクラスを呼び出すことはできません。なぜなら、オブジェクトが作成されないからです。

アプリケーションの起動後に、アプリケーションが実行されるディレクトリでは".walletファイル"と".spvchainファイル"の2つのファイルがあることがわかります。 それらは別のディレクトリにしてはいけません。

イベントの処理

いつコインを受け取り、そのコインを移動できるのかを知りたいと思います。 これはイベントであり、イベントリスナーを登録することで知ることができます。ライブラリーには、以下の様ないくつかのイベントリスナーインターフェースがあります。

  • WalletEventListener :ウォレットに関連するイベントについて
  • PeerEventListener:ネットワーク内のピアに関連するイベントについて

多くのアプリはこれらすべてを使う必要はありません。例えばリアルタイムでコインの受け取り状況を表示する機能を実装する場合は以下のようなイベントを作成します。

kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
    @Override
    public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) {
        //イベントリスナーは専用のユーザースレッド(バックグランドスレッド)で実行されます。
    }
});

bitcoinjのイベントは、ユーザスレッドと呼ばれるイベントリスナーの実行に使用される専用のバックグラウンドスレッドで実行されます。 つまり、アプリケーション内の他のコードと並行して実行されます。GUIアプリケーションを作成している場合、イベントがGUIスレッド(=メインスレッド)にないため、GUIを直接変更することはできません。 ただし、イベントリスナーはユーザースレッドで処理された後に、イベントがhandlerを経由してメッセージキューに入れられて順番に実行されるため、スレッドセーフである必要はありません。 また、マルチスレッドライブラリを使用するときに一般的に発生する他の多くの問題についても心配する必要はありません(たとえば、ライブラリーを再入力しても安全です)。

GUIアプリケーションの作成に関する注意

Swing、JavaFXAndroidなど殆どのツールキッドは、スレッドアフィニティです。つまり、単一のスレッドからしか使用できません。 バックグラウンドスレッドからメインスレッドに処理を戻すには、通常、GUIスレッドが使用されていない状態の時に、実行されるクロージャをいくつかのユーティリティ関数に渡します。

bitcoinjを使ってGUIアプリを作成する作業を簡単にする為に、イベントリスナー登録する度に任意のExecutorを指定する事が出来ます。 このExecutorは、イベントリスナーの実行を求められます。 デフォルトでは、これは指定されたRunnableをユーザスレッドに渡すことを意味しており、次のようにオーバーライドすることができます:

Executor runInUIThread = new Executor() {
    @Override public void execute(Runnable runnable) {
        SwingUtilities.invokeLater(runnable);   //Swing
        Platform.runLater(runnable);   //JavaFX

        //Androidの場合:handlerはActivity.onCreateメソッドで作成されました。
        handler.post(runnable);  
    }
};

kit.wallet().addEventListener(listener, runInUIThread);

"listener"のメソッドがUIスレッドで自動的に呼び出されます。

これは反復して迷惑になることがあるからです。またデフォルトのExecutorを変更することもできます。そのため、全てのイベントは以下の通り、常にUIスレッドで実行されます。

Threading.USER_THREAD = runInUIThread;

場合によってbitcoinjは非常に多くのイベントを生成することがあります。例えば、それぞれのトランザクションのコンフィデンスを変えるイベントを生成できるよう、多くのトランザクションを持つウォレットがブロックチェーンと同期する場合などです。将来的には、この問題を回避するためにウォレットイベントの動作が変わる可能性は非常に高いですが、現在のAPIはこのように動作します。
ユーザースレッドが遅れた場合、イベントリスナーの呼び出しがヒープに待ち行列を作ると、メモリの膨れが発生する可能性があります。これを避けるにはExecutorを"Threading.SAME_THREAD"というイベントハンドラで登録することができます。この場合、それらはbitcoinjで制御されたバックグラウンドスレッドですぐに実行されます。ただし、このモードを使用する際には例外的に注意する必要があります。コード内で例外が発生すると、bitcoinjスタックが巻き戻され、ピアの切断が発生する可能性があります。また、ライブラリを再入力すると、ロックの逆転などの問題が発生することがあります。一般的に、特別余計にパフォーマンスが求められる場合や、何をしているのかを正確に把握していない限りは実行しないでください。

コインの受け取り

kit.wallet().addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
    @Override
    public void onCoinsReceived(Wallet w, Transaction tx, Coin prevBalance, Coin newBalance) {
        // ”ユーザースレッド”専用に実行
     // "tx"(トランザクション)は保留中もしくはブロックに含まれている可能性があります
        Coin value = tx.getValueSentToMe(w);
        System.out.println("Received tx for " + value.toFriendlyString() + ": " + tx);
        System.out.println("Transaction will be forwarded after it confirms.");

        // ブロックチェーン形成するまで待ってください(既に存在する場合は直ちに実行するかもしれません)
        // もちろん、このダミーのアプリケーションでは、未確認トランザクションを転送することができます。二重払いが発生しても問題はありません。
        // 上記のonSetupCompleted()でWallet.allowSpendingUnconfirmedTransactions()を呼び出す必要があります。
        //しかし、ブロックを待つより一般的なケースを実証するために、ここではそうしません。
        Futures.addCallback(tx.getConfidence().getDepthFuture(1), new FutureCallback<TransactionConfidence>() {
            @Override
            public void onSuccess(TransactionConfidence result) {
                // ここの「結果」は上記の「tx」と同じですが、わかりやすくするためにこれを使用しています。
                forwardCoins(result);
            }

            @Override
            public void onFailure(Throwable t) {}
        });
    }
});

それでは上記のコードの詳細を確認していきます

Coin value = tx.getValueSentToMe(w);
        System.out.println("Received tx for " + value.toFriendlyString() + ": " + tx);
        System.out.println("Transaction will be forwarded after it confirms.");

ここでは、アプリがお金を受け取ったときにどうなるかを見ることができます。 どれくらい受け取ったのか、静的ユーティリティメソッドを使ってテキストにフォーマットしました。

ListenableFuture<TransactionConfidence> future = tx.getConfidence().getDepthFuture(1);

もう少し進んだことをする為に、上記のfutureメッソドを呼び出します:

すべてのトランザクションには、それに関連付けられたコンフィデンスオブジェクトがあります。
このコンフィデンスの概念は、常にBitcoinがグローバルなコンセンサスシステム*1カプセル化しています。 コンフィデンスの概念は難しい問題で、悪意のあるノードに直面した場合にトランザクションが二重払いされる可能性があります(bitcoinj用語では、「死んでいる」と言います)。 つまり、私たちはお金を受け取ったと信用したが、後になって残りの部分が食い違っていることを発見することがあります。

コンフィデンスオブジェクトには、実際にコインを受け取ったかどうかについて、リスクベースで判断を下すために使用できるデータが含まれています。またコンフィデンスが変わった時や閾値に達した時に、学習するのにも役立ちます。

Futures.addCallback(tx.getConfidence().getDepthFuture(1), new FutureCallback<TransactionConfidence>() {
            @Override
            public void onSuccess(TransactionConfidence result) {
                // ここの「結果」は上記の「tx」と同じですが、わかりやすくするためにこれを使用しています。
                forwardCoins(result);
            }

Futureは並行プログラミングの重要な概念であり、Bitcoinjはそれらを頻繁に使います。ListenableFutureと呼ばれるJavaの標準クラス "Future"を拡張しています。"ListenableFuture"は、将来の計算または状態の結果を表します。それが完了するのを待つ(呼び出しスレッドをブロック)か、もしくは呼び出されるコールバックを登録することが出来ます。Futureは失敗する事もありますが、その場合は結果の代わりに例外が返されます。

次にdepth futureについて。このfutureはトランザクションブロックチェーンの多くのブロックによりに埋められた時に完了します。深さ1とはブロックチェーンの先頭にあるブロックを意味します。ですので「トランザクションが少なくとも1承認を得ている時このコードは実行される」と言えます。通常、ユーティリティメソッドに呼ばれる”Futures.addCallback”を使います。とは言え、これらは他の方法でリスナー登録する事も出来ます。

次にコインを送金するトランザクションが確認された時に、"forwardCoins"と呼ばれるメソッドを呼び出します。

ここで注意すべき重要なことがあります。 depth futureが実行されると、トランザクションの深さがfutureのパラメータよりも小さくなる様に変更される可能性があります。 これは、常にBitcoinネットワークで最も長いチェーンが別の分岐したチェーンに切り替わる「再編成」が起きる可能性があるからです。 トランザクションが別の新しいチェーンに表示される場合、深度は実際には上がらずに下がります。 インバウンド決済を処理する際に取引のコンフィデンスが低下した場合には、そのお金を提供していたサービスを中止する必要があります。 このトピックの詳細については、SPVセキュリティモデルを参照してください。

複雑なトピックである「再編成」と「二重払い」処理については、このチュートリアルでは取り扱いません。他の記事を読むことで学習するする事が出来ます。

コインの送金

"ForwardingService"の最後の部分では、受け取ったコインを送金します。

Coin value = tx.getValueSentToMe(kit.wallet());
System.out.println("Forwarding " + value.toFriendlyString() + " BTC");
//今コインを返送してください! 迅速な確認を確実にするために小さな手数料を添付して送信してください。
final Coin amountToSend = value.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
final Wallet.SendResult sendResult = kit.wallet().sendCoins(kit.peerGroup(), forwardingAddress, amountToSend);
System.out.println("Sending ...");
//トランザクションがネットワークを介して伝播したときに呼び出されるコールバックを登録します。
//これは、ListenableFutureコールバックを登録する第2のスタイルを示しています。これは、将来返されるオブジェクトにアクセスする必要がない場合に機能します。
sendResult.broadcastComplete.addListener(new Runnable() {
    @Override
    public void run() {
         //ウォレットは今変更されました。すぐに自動保存されるか、アプリがシャットダウンします。
         System.out.println("Sent coins onwards! Transaction hash is " + sendResult.tx.getHashAsString());
    }
});

はじめに、ウォレットにコインがいくらあるか確認します。

Coin value = tx.getValueSentToMe(kit.wallet());
System.out.println("Forwarding " + value.toFriendlyString() + " BTC");

次に、どれくらい送るかを決定します。実際に送金するコインは手数料を差し引いたものです。 手数料を払う必要はありませんが、払わない場合は、確認に時間がかかることがあります。 デフォルトの手数料はかなり低いです。

final Coin amountToSend = value.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE);
final Wallet.SendResult sendResult = kit.wallet().sendCoins(kit.peerGroup(), forwardingAddress, amountToSend);
System.out.println("Sending ...");

コインの送金する際にウォレットの"sendCoins"メソッドを使用します。その際に3つの引数を取ります。

  • kit.peerGroup():P2Pネットワークの接続
  • forwardingAddress:送信先のアドレス
  • amountToSend:送金額

"sendCoins"は作成されたトランザクションとネットワークが支払いを受け付けたことを知る為に使用する"ListenableFuture"の両方を含むSendResultオブジェクトを返します。もしウォレットが十分なコインを有していな場合には、"sendCoins"メソッドはいくらコインが足りないのか例外を投げてくれます。

送金処理のカスタマイズと手数料設定

トランザクションには手数料を付けることができます。 これは、サービス拒否対策の仕組みとしては有効ですが、主に、マイニングによる収益がなくなった後のシステムで、マイナーにインセンティブを与えることを意図しています。 送信要求をカスタマイズすることによって、トランザクションにつける手数料を制御することができます。

SendRequest req = SendRequest.to(address, value);
req.feePerKb = Coin.parseCoin("0.0005");
Wallet.SendResult result = wallet.sendCoins(peerGroup, req);
Transaction createdTx = result.tx;

ここでは、実際に作成されたトランザクションキロバイトあたりの料金を設定しています。 これはBitcoinの仕組みで、取引の優先順位は、手数料をサイズで割った値で決まります。したがって、手数料は送金する金額の大小ではなく、かき集めるUTXOのデータサイズに依存します。

次のステップ

このチュートリアルでは網羅していないbitcoinjの他の機能が多くあります。他の資料を読むことで完全な検証、ウォレットの暗号化などについて、より多くを学習する事が出来ます。もちろんJavaは完全なAPIを記述しています。

*1:世界規模でトランザクションの要求に合意するよう参加者が最善に努める