Develop with pleasure!

福岡でCloudとかBlockchainとか。

bitcoinjのmicropayment channelの実装

bitcoinjでは0.10からClient/Server形式のpayment channelをサポートしているみたいなので、どういったものなのか見てみる。

Working with micropayment channels

Protocol overview

ベースとなってるBitcoin wikiの記事

https://en.bitcoin.it/wiki/Contract#Example_7:Rapidly-adjusted.28micro.29payments_to_a_pre-determined_party

Bitcoinの取引は伝統的な決済システムと比べてとても安価だが、マイニングされブロックチェーンに保存してもらうためにはコストが発生する。トランザクションをブロードキャストするのにコストをかけることなく特定の受信者にお金を送りたいケースがある。

例えば、今まで一度も訪れたことがないカフェのWiFiホットスポットのような信頼できないアクセスポイントを考えてみる。あなたはカフェで口座を開くことなくインターネットの使用量10KBあたり0.001 BTC支払うこととする。月初に携帯電話のモバイルウォレットに予算をチャージする。その後は携帯が自動的に調整しオンデマンドでインターネットアクセスに関する支払いをする。カフェ側も騙される心配なくそれらの支払いを可能にしたい。

こういったことを実現するのに以下のプロトコルが利用できる。
値の送信側をクライアントと定義し、受信側がサーバであると定義するとして、これはクライアントの観点から記載する。

  1. 公開鍵(K1)を作成し、サーバの公開鍵(K2)を要求する。
  2. 10BTCをサーバの秘密鍵と自分の秘密鍵を必要とする(OP_CHECK_MULTISIGを使う)出力に送るトランザクション(T1)を作成し署名するが、まだブロードキャストはしない。
  3. 続いて払い戻し用のトランザクション(T2)を作成する。このT2はT1の出力を入力にセットし、その全額を自分に送るトランザクション。このトランザクションには将来のある時点に設定されたタイムロックを持つ。署名はせず、未署名のままそのトランザクションをサーバに送る。
  4. サーバはK2の秘密鍵を使ってT2に署名し、署名したトランザクションをクライアントに返す。 この時点でT1は登場しておらず、ただ未署名のT2の一部にハッシュとして現れているだけ。
  5. クライアントはサーバの署名が正しいか検証し、正しくなかったら無視する。
  6. クライアントはT1に署名し、署名したトランザクションをサーバに渡し、ブロードキャストする。ここでT1のお金がロックされる。
  7. 続いてクライアントは新しいトランザクションT3を作成する。T3はT2と同様にT1の出力を入力にセットし、2つの出力を持つ。出力のうち1つはK1にもうひとつはK2に宛てたものにする。全てのお金を最初の出力(K1宛)に割り当てればタイムロックの無い払い戻しトランザクションと同様になる。クライアントはT3に署名し、トランザクションと署名をサーバに提供する。
  8. サーバは出力それ自体が意図した金額のものであるか検証し、クライアントから提供された署名が正しいか検証する。
  9. クライアントがサーバに継続して支払いを行う場合、T3をコピーしてサーバ側の出力(K2宛)の値を増やし自分側の出力(K1宛)の値を減らす。そして再度T3に署名し、T3と署名をサーバに送る。サーバはT3のコピーが新しい値になっているか確認、署名を検証する。

これをセッションが終了するか、1日の期限を迎えるまで続ける。アクセスポイントは受け取った(最終金額がセットされている)最後のトランザクションに署名しブロードキャストする。

払い戻しのトランザクションはサーバがどっちつかずの状態で割り当てられた値を残したまま消えたり、ずっと停止したままになった場合に対処するために必要となる。タイムロックが切れた後にクライアントが払い戻しのトランザクションをブロードキャストすることで全てのお金を取り戻すことができる。

micropayment protocolの概要

micropayment protocolはあるユーザ(クライアント)が別のユーザ(サーバ)との間で繰り返されるマイクロペイメントの作成を可能にする。このプロトコルは2つのステージからなり、まず最初にいくらかの資金を両者のコントロール下にあるマルチシグのトランザクションにロックする。続いて両者協力してクライアントに全額戻す払い戻し用のトランザクションを作成するが、このトランザクションBitcoinプロトコルのnLockTimeを使ってタイムロックされる。これにより一定期間経過するまでは払い戻しトランザクションが使えないことを保証する。

払い戻し用のトランザクションは、最初に作成したマルチシグのトランザクションをサーバに送信する前に用意すしておく。この払い戻し用トランザクションへのサーバ側の署名を事前に入手しておくことで、攻撃やクラッシュによりクライアントが資金を失う可能性を排除できる。サーバがこのプロトコルの途中で停止しても、タイムロックされた期間が過ぎればクライアントは資金を取り戻すことができる。

払い戻し用トランザクションをクライアントが入手した後は、最初に作成したマルチシグのトランザクションをサーバに送り、サーバ側で署名しブロードキャストすれば、資金をロックしChannelをオープンすることになる。クライアントは払い戻し用トランザクションをコピーし、支払額分をサーバに送り、残りを自分におつりとしておくる支払い用のトランザクションを作成し署名する。署名をサーバに送り、サーバ側で署名の検証をし正しければ保存する。署名にはかなり緩いSIGHASHモードを使用しているため、サーバ側で払い戻し用トランザクションを変更する余地はあるが、通常自分のウォレットに送り戻す出力を追加するくらいだろう。

こうしてPayment Channelが確立すると、わずかクライアント側の1署名操作とサーバ側での署名の検証のみでマイクロペイメントが可能になる。

最終的にクライアントがマイクロペイメントによる支払いを追えると決めたら、Channelを閉じるようサーバにメッセージを送る。サーバ側は最後のトランザクションに自分の鍵で署名しブロードキャストすると、Channelの最終状態がブロックチェーン上で確認できるようになる。クライアントがChannelを閉じようとする前に、サーバが消えたり適切な応答をしない場合は、初期の払い戻し用トランザクションが有効になるまで24時間(タイムロックした時間)待つ必要がある。

当然ながら、払い戻し用トランザクションはタイムロックした時間が近づくと全ての資金をクライアントが取り戻せることになるので、サーバはその前にChannelをクローズし、継続して取引する場合は新しいChannelをゼロから構築する必要がある。

API設計

bitcoinjでは↑の構成のクライアントとサーバの一連の実装を提供する。TCPを使ってプロトコルバッファを送るが、HTTPやXMPPのような他のプロトコルに簡単にmicropaymentsをバインドできるよう設計されている。

  1. APIの心臓部はPaymentChannelClientStateとPaymentChannelServerStateという2つのステートオブジェクトからなる。この2つのクラスは特定のネットワーク・プロトコルから独立して動作し、適切なタイミングで適切なオブジェクトを取得するためのステートマシンを提供する。どのタイミングでどのオブジェクトを返すかは自前で実装する必要があり、例えばincrementPaymentByメソッドを呼び出した際、署名を含むバイト配列が戻るのを期待するが、ステートオブジェクト自体がそのデータの伝送を行うことは無い。
  2. ステートマシンの遷移をバイト配列としてシリアライズしネットワーク伝送の準備をしたり、アプリケーションの再起動やネットワーク分断の際にマイクロペイメントを再開できるようステートマシンをディスクに保存する処理については、PaymentChannelClientとPaymentChannelServerで実装されている。これらのオブジェクトは基本的なChannelパラメータと、クライアント/サーバの特定のインターフェースを実装した特定のオブジェクトを持ち、ステートマシンを構築しプロトコルバッファベースのメッセージをシリアライズしウォレットファイル内に保存されたChannelのデータを確認するためにウォレットの拡張機能を使用する。また悪意のあるクライアントやサーバが変な動作をしないようプロトコルのエラーチェックを行う。
  3. プロトコルは特定の時間に取るべき特定のアクションを必要とする(ここでいう時間はブロックチェーンのtime フィールドによって定義される時間)。StoredPaymentChannel{Client/Server}Statesはウォレットの拡張として動作し、ブロックチェーンの状態をみながら適切なタイミングで適切な処理を行う。そのため、自分で直接ステートマシンクラスの駆動を管理する場合、適切な時間になるまで払い戻し用のトランザクションをブロードキャストしないよう自前でロジックを実装できる。
  4. プロトコルバッファの取得/提供は素のJavaオブジェクト上で実装しているが、ネットワークへそれらを読み込み/書き込みといったハンドルはまだ十分ではない。多くの場合micropayment protocolは別のプロトコルの内部に埋め込まれるが、もし(テストのためなどで)スタンドアロンで動かしたい場合に、PaymentChannelClientConnectionとPaymentChannelServerListenerを提供している。これらはホストとポートのペアとChannelパラメータをとって構築される。実際のネットワークのハンドリングコードは、TCPコネクション上のプロトコルバッファをシンプルに読み書きする別のパッケージ(org.bitcoinj.protocols.niowrapper)に委譲している。

多数のオブジェクトがあるように思われるかもしれないが、これは抽象化のためである。
例えば、Web上の広告をスキップするため課金をする際に広告を配信するAdネットワークにプライベトなマイクロペイメントを行うプロトコルを構築することを想像してみよう。個別のTCPコネクションを使うのはおそらく適切ではない。その代わりいくつかの特殊なヘッダでHTTPを拡張しウォレットアプリをブラウザとリンクし、そのインラインHTTPヘッダ上でmicropayments protocolを動かすのが有用だと思われる。 この際、ステートマシンとプロトコルバッファのシリアライゼーションを使用したいが、ネットワークコードそれ自体は有用ではないかもしれない。
さらに掘り下げて、既に持っている独自のシリアライゼーションの仕組みにこのプロトコルを組み込みコアステートマシンを再利用したいといったことも可能である。

所感

  • bitcoinjでサポートしている↑のPayment Channelは一方向のPayment Channelで、双方向のPayment Channelについては触れてないっぽい。
  • かなり緩いSIGHASHって具体的にどれを使ってるのか?(SINGLE or NONE)
  • データ交換する際のプロトコルとしてXMPP使うのも良いな。
  • bitcoinj自体がウォレットの実装持ってるので、Payment Channelもそのウォレットの拡張機能として連携できるのはメリット大きい。
  • Web上の広告とか既存のサイトに組み込まれているコンテンツへの支払いにmicropayment channel使うというユースケースおもしろい。

2016.09.05 追記
↑のフローを行う単方向Payment Channelを簡易実装してみた↓

github.com