はじめに
bitcoinjはSPVモードに特化したJVM言語向けのBitcoinライブラリです。
今回は、bitcoinjのドキュメント「Working with the wallet」の1/3ほどを意訳してみました。1
本ドキュメントを読み進めることにより、独自ウォレット制作への知見が深まると考えています。
誤りがあれば、どしどしご指摘ください。
原文はこちらです。
Working with the wallet
https://bitcoinj.github.io/working-with-the-wallet
Working with the wallet
Walletクラスの利用方法とカスタムトランザクションの作り方を学びます。
Introduction
Walletクラスは、bitcoinjで最も重要なクラスの一つで、次のような機能を持っています。
- 鍵や鍵に関連するトランザクションを格納します。
- 保存済みのトランザクション出力を消費する新しいトランザクションを作成します。
- ウォレットの内容が変更されたときに通知を行います。
さまざまな種類のアプリを構築するために、あなたはWalletの利用方法を学ぶ必要があるでしょう。
この資料では、SatoshiのホワイトペーパーとWorkingWithTransactionsを読んでいることを前提としています。
Setup
最適な動作のため、WalletをBlockChain
と、Peer
か PeerGroup
のいずれかに接続する必要があります。
BlockChainはWalletのコンストラクタに渡され、Walletに関するブロックを送受信し、Walletに関連するトランザクション(Wallet内の鍵を使ってコインを送受信したトランザクション)を抽出することができます。
Peer/PeerGroupは、ウォレット内のトランザクションがブロックに含まれる前に、トランザクションをネットワークにブロードキャストします。
ウォレットは、ブロックチェーン上での状態と無関係に、トランザクションが一切ない状態、つまり残高ゼロで開始します。 Wallet使用するには、ブロックチェーンをダウンロードしなければなりません。これによりトランザクションがウォレットにロードされ、分析や支払いが可能になります。
Wallet wallet = new Wallet(params);
BlockChain chain = new BlockChain(params, wallet, ...);
PeerGroup peerGroup = new PeerGroup(params, chain);
peerGroup.addWallet(wallet);
peerGroup.startAndWait();
Getting addresses
もちろん、上記コード片にはウォレットに入金する術がなく、あまり有用ではありません。 ウォレットからキーとアドレスを取得するには、次のAPI呼び出しを使用します。
Address a = wallet.currentReceiveAddress();
ECKey b = wallet.currentReceiveKey();
Address c = wallet.freshReceiveAddress();
assert b.toAddress(wallet.getParams()).equals(a);
assert !c.equals(a);
これらは支払いを受け取るために引き渡されます。 ウォレットには「現在」のアドレスの概念があります。 これは、常にアドレスを表示しておきたいGUIウォレットのためです。 現在のアドレスが使用されると、それが新しいアドレスに変わります。 一方、freshReceiveKey/Addressメソッドは、常に新しく導出されたアドレスを返します。
Seeds and mnemonic codes
これらのメソッドによって返却される鍵とアドレスは、BIP 32とBIP 39で規定されたアルゴリズムを使用して、seedから決定論的に導出されます。鍵の寿命は次のようになります。
- 新しいウォレットオブジェクトが
SecureRandom
を使用し、ランダムな128bitのエントロピーを選択します。 - このランダム性は「ニーモニックコード」に変換されます。 (ニーモニックコードとは、BIP 39標準によって定義済みの辞書を使用した12語のセットの事です)
- 12語の文字列形式は、鍵導出アルゴリズム(PBKDF2)への入力として使用され、「seed」を得るために何度も繰り返されます。 seedは、12語を利用した元のランダムなエントロピーの単なる再表現はではないことに注意してください。seedは12語のUTF-8バイト列表現から導出されます。
- 新しく計算されたseedは、マスター秘密鍵と「チェーンコード」に分割されます。 これにより、BIP32で指定されたアルゴリズムを使用した鍵階層が実現可能になります。このアルゴリズムは、楕円曲線数学の特性を利用し、階層内の公開鍵に対して相当する秘密鍵にアクセスすることなく反復できるようにします。これは、パスワードが掛けられている場合でもウォレットの復号が必要ないため、販売用アドレスに有用です。 bitcoinjは、BIP 32のデフォルト推奨ツリー構造を使用します。
- ウォレットは先読み鍵のセットを事前計算します。 これは、ウォレットがcurrent/freshReceiveKey APIを利用して発行した鍵ではありませんが、将来的に使用されます。 事前計算によりいくつかの目標が達成できます。 1つは、APIの高速化です。これは遅いEC演算を待ちたくないGUIアプリケーションで便利です。 2つ目は、未発行鍵に関連するトランザクション生成時のウォレットへの通知です。これはseedが複数のデバイスに複製され、ブロックチェーンが再生された場合に起こります。
ウォレットをロードする際の遅い再導出ループを避けるために、seedと事前計算鍵を含む鍵はディスクに保存されます。
ニーモニックコードは、生の秘密鍵よりも扱いやすく、書き留めやすいように設計されています。ニーモニックコードにより誤記の可能性が減り、利用者はペンや紙で簡単に書き留めることができます。従って、バックアップメカニズムとしてユーザーに単語を公開することをお勧めします(リストアを高速化するために、日付を書き留めてください)。
次のように作業できます:
DeterministicSeed seed = wallet.getKeyChainSeed();
println("Seed words are: " + Joiner.on(" ").join(seed.getMnemonicCode()));
println("Seed birthday is: " + seed.getCreationTimeSeconds());
String seedCode = "yard impulse luxury drive today throw farm pepper survey wreck glass federal";
long creationtime = 1409478661L;
DeterministicSeed seed = new DeterministicSeed(seedCode, null, "", creationtime);
Wallet restoredWallet = Wallet.fromSeed(params, seed);
// now sync the restored wallet as described below.
先読みゾーンは、ウォレットの同期維持において重要な役割を果たします。 先読みゾーンのデフォルトサイズは100キーです。 ウォレットAがウォレットBに複製され、ウォレットAが50個のキーを発行し、最後のキーのみが実際に支払いの受け取りに使用される場合、ウォレットBはその支払いを認識し、先読みゾーンを移動して、合計で150個のキーを追跡する。 もしウォレットAが120個の鍵を配布し、110回目の支払いのみを受け取った場合、ウォレットBは何が起こったのか気づかないであろう。 この理由から、ウォレットの同期には、ある時点での、支払い待ち受け中の未払いアドレスの個数を想定することが重要です。デフォルトの100は消費者のウォレットに適するように選択されていますが、マーチャントのシナリオではより大きなゾーンが必要かもしれません。
Replaying the chain
ウォレットに既に使用済みの鍵をインポートする場合、追加した鍵のトランザクションを取得するには、ウォレットのリセット(reset
メソッドを利用)でトランザクションを削除し、チェーンを再ダウンロードしなければなりません。現時点では、既にトランザクションを保持しているウォレットでブロックチェーンを再生する方法はなく、これを試みた場合、ウォレットを破損する可能性があります。 これは将来変更される可能性があります。その代わり、ブロック・エクスプローラのような他のソースから生のトランザクションデータをダウンロードし、そのトランザクションをウォレットに直接挿入することもできます。しかし、これは現在サポートされておらず、未テストです。ほとんどのユーザーにとって、既存のキーをインポートすることはよくない考えであり、何らかの深刻な機能欠如が存在する状況を表しています。 鍵をウォレットに定期的にインポートする必要がある場合はご相談ください。
ウォレットは、システム内の他のクラスと連携して、ブロックチェーンとの同期を高速化しますが、デフォルトでは一部の最適化のみが有効になっています。 ウォレット/PeerGroupによる最適化内容と設定方法を理解するためには、SpeedingUpChainSyncをお読みください。
Creating spends
チェーンに追いついた後、あなたはいくつかのコインを支出することができます:
System.out.println("You have " + Coin.FRIENDLY_FORMAT.format(wallet.getBalance()));
支出は4ステップで構成されています。
- 送信要求を作成します。
- 送信要求を完了させます。
- トランザクションをコミットし、ウォレットを保存します。
- 生成されたトランザクションをブロードキャストする
利便性のために、これらの手順を実行するヘルパーメソッドがあります。 最も単純な場合:
// Get the address 1RbxbA1yP2Lebauuef3cBiBho853f7jxs in object form.
Address targetAddress = new Address(params, "1RbxbA1yP2Lebauuef3cBiBho853f7jxs");
// Do the send of 1 BTC in the background. This could throw InsufficientMoneyException.
Wallet.SendResult result = wallet.sendCoins(peerGroup, targetAddress, Coin.COIN);
// Save the wallet to disk, optional if using auto saving (see below).
wallet.saveToFile(....);
// Wait for the transaction to propagate across the P2P network, indicating acceptance.
result.broadcastComplete.get();
sendCoins
メソッドは生成されたトランザクションとListenableFuture
を返却します。ListenableFutureはトランザクションがネットワークに受け入れられる(あるPeerに送信し、他のPeerから受信)まで、処理をブロックすることを可能にします。 また、返却されたFutureに対し、伝播の完了タイミングを知るためのコールバックを登録したり、独自のTransactionConfidence.Listener
をトランザクションに登録し伝搬の状況を監視したり、自分自身で採掘したりできます。
より低レベルでは、これらのステップを自分で行うことができます:
// Make sure this code is run in a single thread at once.
SendRequest request = SendRequest.to(address, value);
// The SendRequest object can be customized at this point to modify how the transaction will be created.
wallet.completeTx(request);
// Ensure these funds won't be spent again.
wallet.commitTx(request.tx);
wallet.saveToFile(...);
// A proposed transaction is now sitting in request.tx - send it in the background.
ListenableFuture<Transaction> future = peerGroup.broadcastTransaction(request.tx);
// The future will complete when we've seen the transaction ripple across the network to a sufficient degree.
// Here, we just wait for it to finish, but we can also attach a listener that'll get run on a background
// thread when finished. Or we could just assume the network accepts the transaction and carry on.
future.get();
トランザクション作成のため、まずSendRequest
オブジェクトの静的ヘルパーメソッドを使用します。 SendRequest
は部分完全(invalid)Transaction
オブジェクトで構成されていて、手数料やアドレス変更、将来提供されるプライバシー機能(コイン選択方法など)などの未変更項目を含みます。 必要に応じて部分トランザクションを変更したり、独自のトランザクションをスクラッチで構築したりできます。 SendRequest
の静的ヘルパーメソッドは部分トランザクションを構築するシンプルな別の方法です。
次に要求を完了します。つまり、送信リクエスト内のトランザクションに、入出力が追加され、トランザクションを有効にするための署名が付与されたことを意味します。 これでトランザクションはBitcoinネットワークで受け入れられるようになりました。
completeTx
とcommitTx
の間でロックが保持されていないことに注意してください。 従って、あなたの支配下外でウォレットが変更された場合、このコードは競合し失敗する可能性があります。例えば、ウォレットのキーがエクスポートされて別の場所で使用され、選択された出力を使用するトランザクションが2つのメソッドコールの間に発生した場合です。両方の操作が実行されている間ウォレットがロックされるようなシンプルな構造を使用すると、二重支出をコミットしないことを保証できます。
When to commit a transaction
トランザクションのコミットとは、それが再利用されないようにウォレットのspentフラグを更新することです。 適切なタイミングでトランザクションをコミットすることは重要であり、そのための様々な戦略があります。
sendCoins()のデフォルトの振る舞いは、コミット後のブロードキャストすることです。ほとんどの場合でこれは良い選択です。 つまり、ネットワークで問題が発生した場合や、複数スレッドが同時にトランザクションを作成してブロードキャストしようとした場合に、誤って二重支出が発生する可能性はありません。一方、ネットワークが何らかの理由(例えば、手数料が不十分/非標準的な形式)でトランザクションを受け入れない場合、ウォレットはお金が消費されたと考え、あなたはその問題を修正しなければなりません。
また、単にwallet.commitTx
呼び出さず、代わりにpeerGroup.broadcastTransaction
を使用することもできます。 一度トランザクションが複数のPeerによって参照されれば、トランザクションはウォレットに与えられ、その後コミットします。 ブロードキャスト成功後にコミットしたい主な理由は、新しいコードを試していて、必ずしも受け入れられないトランザクションを作成している場合です。 この場合、ウォレットを常にロールバックしなければならないのは面倒です。 ネットワークが常にトランザクションを受け入れることが判明した後、あなたは送信要求の作成・完了、トランザクション結果のコミットをすべて単一のロック下で行うことができます。そのため複数のスレッドが間違って二重の支出を生み出すことはありません。
-
本当は全部訳したかったんだけど、長すぎて頓挫しました。 ↩