Bitcoin blog

Follow me:https://twitter.com/22Q5jCESM2tXzRU

アプリの異なるコンポーネントがどのように連携してフィットするか

【目次】

 

概要

この記事では、典型的なbitcoinjベースのアプリケーションのさまざまなオブジェクトとインターフェイスがどのように相互作用するかを説明します。データがネットワークからどのように到着し、Javaオブジェクトに変換され、最終的にさまざまなアクションを実行するか、ディスクに保存されるまでに、これらのオブジェクトがどのように動作するかを確認します。

この記事では、ウォレットアプリを前提としています。


ネットワーク

Bitcoinデータのライフは、P2Pネットワーク内の別のノードから私たちに送信される時、またはトランザクションが作成する時の2つの方法から始まります。

Networking APIの1番低いレベルは"ClientConnectionManager*1"を実装するオブジェクトです。このインタフェースは新しい接続を開くメソッドを提供し、ランダムに選択されたいくつかの接続を閉じるよう要求します。

新しい接続を開くには"StreamParser"インターフェイスを実装するオブジェクトを、接続先のネットワークアドレスと共に提供する必要があります。クライアント接続マネージャはソケット*2を設定し、ソケットに対する読み書きを管理します。ここでのスレッド*3は保証されません。マネージャは、提供された"StreamParser"のメソッドを任意の数のスレッドまたは1つのスレッドで実行することができます。"BlockingClientManager"と"NioClientManager"という2つの実装が用意されています。高レベルの"PeerGroup"オブジェクトを作成すると、デフォルトで"NioClientManager"が作成されますが、コンストラクタを使用して独自のオブジェクトを提供することもできます。

"NioClientManager":
単一スレッドと非同期epoll / selectベースのIOを使用

”BlockingClientManager”:
標準Javaブロッキングソケットとの接続ごとにスレッドを使用

なぜbitcoinjは両方のアプローチをサポートしているのか?

  • ブロック化IOは、機能を必要とするときに便利です。 Javaは透過的にSSL、SOCKSプロキシをサポートし、OrchidライブラリTorを介してサポートされますが、ブロッキングソケットが使用されている場合に限ります。
  • 非同期IOは、1接続につきスレッドの追加メモリ圧をかけずに何千もの接続を同時に処理したい場合に便利です。

多くの種類のアプリ、特にウォレットや業者のアプリでは、同時に多くの接続を必要としないため、2つのパフォーマンスの差はほとんど無関係です。また、コネクションごとのスレッドと非同期IOのスケーラビリティの差は、最近のカーネルスケジューラやマルチコアシステムの方がはるかに大きいため、その差異はもはや明確ではないことがよくあります。注意してスレッドスタックサイズに注意を払うと、接続あたりのスレッドが以前と同じくらい高価ではない場合があります。

理論的には"NioClientManager"は、非同期入出力と複数のスレッドを簡単にサポートできますが、現在の実装ではそうではありません。


シリアライズ/シリアライゼーション*4

上記のように、クライアントマネージャクラスでは"StreamParser"インターフェイスの実装が必要です。このインタフェースは、コネクションのオープンまたはクローズ、未処理のバイトバッファーの受信、"MessageWriteTarget"インタフェースの実装の通知を通知するメソッドを提供します。 "StreamParser"には、何らかのフレーミングや解析が行われずにネットワークから読み取られたデータパケットが与えられます。たとえば、半分のメッセージが"StreamParser"のフロントドアに表示されることは有効です。パーサは、データをバッファし、フレーミングを処理し、何らかの方法でデータを消費します。

クライアントマネージャーに新しいパーサー*5が与えられると、内部オブジェクトが"MessageWriteTarget"として設定されます。このインタフェースは、バイトを書き、接続を閉じるメソッドを公開するだけです。したがって、パーサーオブジェクトは通常、一度開始された接続のライフサイクルを管理します。

アブストラクト"PeerSocketHandler"クラスは、バッファリング、チェックサム、およびバイトストリームの解析をMessageオブジェクトに提供することによって、Bitcoin P2Pネットワークプロトコル用の"StreamParser"を実装します。

これは、メッセージのタイプとそのチェックサムをワイヤから読み取る方法を知っている"BitcoinSerializer"クラスを使用して実行され、そのメッセージを表す適切なオブジェクトを作成します。これは、オブジェクト型に対する静的な名前のマ​​ップを持っています。構築できる各オブジェクトは、Messageクラスの子孫です。各メッセージクラスは、生のバイト形式からの独自の非直列化を担当します。

メッセージが完全に構築され、デシリアライズが終了すると、"PeerSocketHandler"の抽象メソッドに渡されます。したがって、解析されたメッセージのストリームにアクセスしたい場合は、この時点でサブクラス化する必要があります。

メッセージのシリアライズはSatoshiが設計したカスタムバイナリ形式です。オーバーヘッドが最小限に抑えられるため、最小限の柔軟性が得られます。


ピアのロジック

ほとんどの場合、あなたのアプリは生のBitcoinプロトコルメッセージのストリームを処理するのではなく、より高いレベルで動作する可能性があります。この目的のために、Peerクラスは"PeerSocketHandler"をサブクラス化し、接続に関連する状態を追跡し、着信メッセージを処理します。
ブロックのダウンロード、チェーン全体の処理、トランザクションping(おもにネットワークの疎通を確認するために使用されるコマンド)の実行などの高度な操作を提供します。

また、メッセージは、メッセージが接続されている他のさまざまなオブジェクトにディスパッチ*6されます。

  • 登録されているすべての"PeerEventListener"
  • MemoryPool(提供されている場合)
  • 接続されているすべてのウォレット
  • 指定されたブロックチェーンオブジェクト(存在する場合)

メッセージを受信すると、各"PeerEventListener"はメッセージを読み取って遮断し、おそらくメッセージを変更したり、メッセージを別のメッセージに置き換えたり、さらに処理を完全に抑止したりする可能性があります。処理が抑制されていない場合は、次のようになります。

  • 新しいブロックまたはトランザクションを通知する「inv」メッセージは、ピアがデータをダウンロードするように指示されている場合に「getdata」を送信する結果となる。 "MemoryPool"は"inv"の通知を受けます。
  • トランザクションデータを含む受信された「tx」メッセージは、最初にMemoryPoolを通過します。次に、各特定のトランザクションを気にするかどうかを"isPendingTransactionRelevant"で各ウォレットに問い合わせ、保留中のすべてのトランザクション再帰的にダウンロードされるようにします。再帰的なダウンロードが完了すると、トランザクションと保留中のすべての依存関係が"Wallet.receivePending()"に渡されます。最後に"PeerEventListener.onTransaction"が呼び出されます。
  • 受信されたブロック、フィルタ処理されたブロック、またはブロックヘッダは、後で処理するために"AbstractBlockChain"オブジェクトに送信されます.
  • リモート・ピアが "getdata"を使用してトランザクション・データを要求すると、Walletとリスナーがポーリング*7されて、そのデータを提供できるかどうかが確認され、応答があった場合に送信されます。
  • pingやアラートなどのその他のメッセージは適切に処理されます。


メモリプール

いくつのピアが特定のトランザクションをアナウンスしたか知ることは有用です。なぜこれが興味深いのかについては、bitcoinj SecurityModelの記事を参照してください。これを実装するために、MemoryPoolクラスはトランザクショントランザクションハッシュを追跡します。

例えば、ピアがハッシュ"87c79f8d77fe2078333c612e2bdf1735127c6c02"でトランザクションを持つことを示す「inv」をピアから送信した場合、ピアはそれをMemoryPoolに通知し、このピアがそのトランザクションを見たことを記録します。我々は最終的に、それが私たちに属しているかどうかを知るために所定の取引をダウンロードし、その時点で、アプリの一部が興味を持っている場合にそれを保持するMemoryPoolにも与えられます。さらなるinvが入ると、トランザクショントラストオブジェクトが更新されます。

同じ「tx」メッセージが複数回送信されることがあります。通常は起こりません。しかし、MemoryPoolがそれらを重複排除すると、複数回デシリアライズされたとしても、1つのJavaオブジェクトだけが広まることが保証されます。


チェーンとストア

"AbstractBlockChain"のサブクラスは、ブロックの受信し、それらを一緒にフィッティングし、検証を行います。 "BlockChain"クラスはSPVレベルの検証を行い、"FullPrunedBlockChain"は名前が示すように完全な検証を行います。

ブロックチェーンをPeerまたはPeerGroupに渡し、ブロックデータは、その接続を介してネットワークからブロックチェーンオブジェクトを介して、BlockStoreインターフェイスの実装に向かってデータが流れます。複数の種類のブロックストアがありますが、すべてがブロックデータを取得し、少なくともヘッダーを保存し、トランザクションデータも(フルストア用に)保存します。 SPVクライアントの場合、通常は"SPVBlockStoreが"選択され、フルモードクライアントの場合は、"H2FullPrunedBlockStore"などの"FullPrunedBlockStore"の実装が必要です。

ストアは、データベースまたはディスクファイルに直接、伝達します。それらの下には他のオブジェクトはありません。

チェーンは"BlockChainListeners"でコールバックを呼び出します。 Walletはブロックチェーンリスナーの例ですが、より具体的なBlockChain.addWallet()メソッドを使用することをお勧めします(addListener()と同じことですが、将来変更される可能性があります)。

リスナーは、次のイベントに対して呼び出されます。

 

"notifyNewBestBlock":
最長のチェーンを拡張する新しいブロックが見つかったときに呼び出されます。これはシステムが正常に継続しています。 blockパラメーターはブロックヘッダーのみです。トランザクションデータは使用できません。


"reorganize"
:ブロックが受信されたときに呼び出され、サイドチェーンを拡張し、それを新しいベストチェーンにします。 1つのタイムラインが、トランザクションの並べ替え、置換、または完全削除の可能性のある別のタイムラインに置き換えられます。このため、再編についての聞き取りに際して、リスナーは、新しい現実を説明するために内部の簿記を更新する必要があります。再編成メソッドには、何をすべきかを把握できるように変更されたブロックチェーンセグメントが与えられます。独自のリスナーを実装していて、アプリが動作するように見えるが、再編成を無視すると、セキュリティ攻撃やデータの破損につながる可能性があります。


"isTransactionRelevant":
リスナーが関与しているかどうかを調べるためにブロック内の各トランザクションに対して呼び出されます。これは将来的に削除される可能性のある最適化ステップです。ブロックには、ウォレットに関連する可能性のある実際のトランザクションがブロック内にない限り、完全な(フィルタリングされていない)ブロックを持つSPVモードのマークルツリーの検証を避けることができます(資金を送る/我々の鍵から)。これは携帯電話に大きな違いをもたらしますが、Bloomフィルターにより、ますます少なくなります。


"receiveFromBlock":
それを含むブロックが受信されたとき、前のメソッドによって関連があるとみなされた各トランザクションに対して呼び出されます。ブロックが最長のチェーン上にあるかもしれないし、そうでないかもしれません。Bloomフィルターがアクティブな場合、すべてのトランザクションがここに表示されるわけではないことに注意してください。 - 前のトランザクションがピアによって前に送信された場合、それを含むブロックがマイニングされたときにトランザクションを再度送信することはありません。。これは帯域幅を節約するためです。

"notifyTransactionIsInBlock":
これは"receiveFromBlock"と同じですが、完全なトランザクションの代わりにハッシュが提供されています。リスナーは、この時点ですでにトランザクションデータのコピーを持っていることが予想されます。

順番に、最長のチェーンの新しいブロック全体が、トランザクションごとに"isTransactionRelevant"、"receiveFromBlock"、"notifyNewBestBlock"の順に起動されます。最長のチェーンで新しいフィルタリングされたブロックが"isTransactionRelevant"を起動します。これは"receiveFromBlock"または"notifyTransactionIsInBlock"の組み合わせ、次に"notifyNewBestBlock"です。サイドチェーンを拡張する新しいブロックは同じシーケンスを持ちますが、"notifyNewBestBlock"とサイドチェーンを拡張して再編成を行う新しいブロックは同じシーケンスを持ちますが、最後に"notifyNewBestBlock"ではなく"reorganize"を呼び出します。

SPVモードを採用するアプリの場合、ブロックストアには接続先に関係なく、非オーファンブロックが与えられ、新しいヘッドが変更された時にディスクに書き込むことが出来ると通知されます。ヘッダーを格納することだけが想定されています。


データのプルーニング

完全に検証されたノードの場合、ストアはさらに多くの作業を行うために"FullPrunedBlockStore"インターフェイスを実装する必要があります。

チェーンとストアが一緒になって、Bitcoin 0.8+と同じようにultrapruneアルゴリズムが実装されます。しかし、Bitcoin 0.8とは異なり、ストアは実際にはしばらくしてから不要なデータを完全に削除するので、チェーンに他のノードを提供することはできませんが、使用されるディスクスペースはずっと少なくなります。

プルーニングノードは、ブロックチェーン全体を格納しようとはしません。代わりに、未使用トランザクション出力(UTXO)のセットのみを格納します。トランザクション出力が消費されると、そのデータはもはや必要なくなり、削除することができます。イベントを再編成すると、履歴を書き換えることができるため、状況が多少複雑になります。したがって、プルーニングストアは、UTXOセットの変更を元に戻すことを可能にするいくつかの「元に戻すブロック」を保持することも期待されます。格納された取り消しブロックの数は、使用可能なディスクスペースと、処理可能な最大の再編成の間のトレードオフです。 UNDOブロック*8があまりにも積極的にスローされると、大規模な再編成はノードをチェーンから恒久的にキックして、最初から再初期化を強制する可能性があるので、慎重に行うのが最善です。

"FullPrunedBlockStore"インターフェイスは、UTXOセットを追加、削除、およびテストするためのメソッドを提供します。また、ブロックの挿入とブロックの取り消し、およびデータベーストランザクションの開始/終了のメソッドもあります(
Bitcoinトランザクションとは違います


ウォレット

"Wallet"クラスはブロックチェーンリスナーとして機能し、チェーンオブジェクトからデータとイベントを受け取ります。受け取ったデータをそれ自身の中に保存し、キーに送金するものなど、ウォレットユーザーにとって関連深い可能性のあるすべてのトランザクションを追跡します。
ウォレットは"WalletProtobufSerializer"を使用してプロトコル・バッファーに保管することができ、ウォレットが変更されたときに自動的に行うように機能が提供されています。

現在、ウォレットは自身をデータベースに格納する方法を持っていません。その機能は将来実装される予定の素晴らしい機能です。

また、ウォレットは、その内部に置かれた取引の信頼水準を更新する責任も負います。ウォレット外のトランザクションは、新しいピアがそれを発表したときにMemoryPoolによって更新される可能性がありますが、最終的にチェーン内の位置については知ることは出来ません。

*1:クライアント接続用の管理インターフェイス

*2:ネットワークとの接続口

*3:プログラムの実行単位

*4:メモリ上に存在する情報を、ファイルとして保存したり、ネットワークで送受信したりできるように変換すること

*5:プログラムのソースコードXML文書など、一定の文法に従って記述された複雑な構造のテキスト文書を解析し、プログラムで扱えるようなデータ構造の集合体に変換するプログラムのこと

*6:処理の内容や用途に応じて,動作の割り当ての制御を行う

*7:主となるシステムが他のシステムに対して一定間隔で順繰りに要求がないか尋ねる方式

*8:トランザクションロールバックに使用されるのが主な目的