Gunosy Blockchain Blog

Gunosyの開発メンバーが知見を共有するブログです。

Counterfactual: オンチェーン処理なくアプリケーションの導入が可能なGeneralized State Channelフレームワーク

はじめに

こんにちは、中村(@veryNR)です。

さて、State Channelシリーズ第二弾として、今回はGeneralized State Channelと、そのフレームワークであるCounterfactualについて見ていきます。

(前回の記事) blockchain.gunosy.io

Generalized State Channelの概要

Generalized State Channelとは、特定のアプリケーションに依存しないチャンネルです。 「依存しない」というのは、新しいアプリケーションを導入するに当たってオンチェーンでの状態遷移が必要ない、ということを表します。

異なるDappで同じユーザー同士がやりとりするケースは今後かなり増えてくるだろうと思います。Generalized State Channelであれば、既にチャンネルが開かれているユーザー間で新しくアプリケーションを利用する場合、チャンネルを開き直す必要がなく便利です。

今回紹介するもの

Generalized State Channel分野には前回の記事で紹介した通りたくさんのプロジェクトがあるのですが、今回はその中でも、L4 MediaによるCounterafctualというフレームワークを紹介します。
L4 MediaはGeneralized State Channelに一番早くから取り組んでおり、他のチームを巻き込みつつ、State Channel分野をリードしているチームです。 他のプロジェクトと比べてGeneralized State Channel自体の設計の完成度が高く、すっきりとした作りになっています。

Counterfactualの概要

設計思想

Counterfactualは、オンチェーンでの操作・ストレージ利用がなるべく少なくなるように、という思想で設計されています。 チャンネルを開くにあたって、参加者が行う必要があるのは、マルチシグウォレットのデプロイのみです。 また、チャンネル内で新しいアプリケーションを導入するにあたって、それがオンチェーンの状態遷移を必要としないだけではなく、 まだデプロイされていない、オンチェーンで存在しないコントラクト も利用することができます。

"counterfactual"

"counterfactual"は、L4が非常に大事にしている概念で、ホワイトペーパーには実に200回以上出てきます。フレームワークの名前にもなっていますね(少し紛らわしい)。
形容詞なので"counterfactual X"のように使うのですが、これは下記のようなことを意味します。

  • Xがオンチェーンで起きるが、まだ起きていない
  • 参加者の誰でも一方的にXをオンチェーンで起こせる機構がある
  • 参加者がXが実際に起こったかのように振る舞うことができる

これはGeneralized State Channelに限らず、State Channel全体でも非常に重要な概念です。 例えば、Lightning Networkのcommitment txは、いつでもブロードキャストできるので、まさに送金済みの状態をcounterfactualにしていると言えます。

前回の記事で述べた「コミットメント」によりオフチェーンで行われた状態遷移はcounterfactualになります。

Counterfactualはどのように"generalized"を実現しているのか

Generalized State Channelの定義である「任意のアプリケーションをオンチェーンの操作なしにチャンネルに導入する」というのは一見難しく思えますが、要するに任意のアプリケーションの状態遷移をcounterfactualにできるか(そういうコミットメントを作れるか)というのがポイントです。これさえできれば、古いコミットメントに対するチャレンジなど他の部分はgeneralizedではないState Channelと大きな違いはありません。
それでは、これを実現するための仕組みを以下で見ていきます。

基本要素

マルチシグウォレットコントラクト

チャンネル内で使う資産のデポジット管理を担当します。(以下、マルチシグウォレットと省略します) チャンネルをオープンする時にオンチェーンに新しく作る必要があるのはこれだけです。

オフチェーンで合意された状態遷移を(チャンネル参加者のアカウントに代わって)実行するべく、 executeTransaction というpublicな関数を実装します。
executeTransactionは、マルチシグのオーナー全員の署名済みのコミットメントをインプットとし、マルチシグのコンテキストで任意の関数を呼ぶ関数です。 例えば、「オーナーのAさんに5 ETHを送る」という内容のコミットメントを実行することができます。
外部の関数をCALL/DELEGATECALLすることができ、これが基本プロトコルで重要な役割を担います。
GnosisのマルチシグウォレットにもexecuteTransactionというインターフェースがありますが、中身は少し異なります)

一般的なマルチシグだと、必要承認数を設定できたりしますが、今回のマルチシグウォレットは全員の署名を必要とするものが想定されています。

Counterfactual Address

まだデプロイされていないコントラクトをオフチェーンで参照する仕組みです。(Celer Networkでも同じような仕組みがあります)
まだデプロイされていないコントラクトを参照するコミットメントを作る場合に問題となるのは、そのコントラクトのアドレスがまだ定まっていないことです。
(コントラクトのアドレスは、デプロイした人のアドレスとそのnonceにより決まるため、実際にデプロイして見るまでわからない)
そこで、デプロイしなくても決定的に決まる、「コントラクトのバイトコードとマルチシグのアドレスのハッシュ値」を counterfactual address とし、これをデプロイされていないコントラクトを参照するために使います。

具体的には、オンチェーンの Registryコントラクト が、これを実現します。
Registryコントラクトはグローバルに一つ準備されていればよく、チャンネル開設のたびに作る必要はありません。
以下の関数を備えており、後述する基本プロトコルで登場します。

  • deploy
    • 与えられたコントラクトをデプロイし、counterfactual addressとオンチェーンのアドレスのmappingを記録します
    • counterfactual addressはkeccak256(C.dbytecode, multisig.address)により生成します
  • resolve
    • 与えられたcounterfactual addressに対応するオンチェーンのアドレスを返します
  • proxyCall/proxyDelegateCall
    • 与えられたcounterfactual addressに対応するコントラクトをresolveで解決し、それに対しCALL/DELEGATECALLします

オフチェーンでは、counterfactual addressとresolve関数を使って、まだデプロイされていないコントラクトに対するコミットメントを作ります。 Ethereum Name Serviceと似たような仕組みですね。

f:id:nrryuya:20180723101523p:plain

Counterfactual Object

まだデプロイされていないが、Instantiateプロトコル(後述)によりデプロイがコミットされたコントラクト(counterfactually instantiated contract)です。 counterfactual objectは、ペイメント用のオブジェクト、賭けチェス用のオブジェクト、などとアプリケーションによって分かれます。
このcounterfactual objectを単位として、オブジェクト指向的にチャンネルを設計していきます。

counterfactual objectは、以下をAPIとして持ちます。

  • isFinalized関数
    • True/Falseを返し、Trueのときはstateが変更不可能になります(ファイナライズ)
    • ファイナライズの条件として、以下を設定します
      • 依存先のオブジェクトのisFinalizedがTrueであり、コンストラクタで設定したnonceであること
      • アプリケーション特有な条件(例: チェスの勝敗が決まった)
  • withdraw関数
    • マルチシグウォレットのコンテキストで呼ばれ、このオブジェクトのstateを参照してマルチシグにロックされたデポジットを配分します
      • 例: stateの残高情報(ex: Alice: 2ETH, Bob: 0ETH)を参照しその通りにetherを送金する
    • isFinalizedがTrueの場合のみ実行されます
  • update関数
    • 引数で渡されたとおりにstateを書き換えます
    • update関数がコールされるとチャレンジ期間が開始し、より新しい(バージョンナンバーが大きい)stateが引数で渡された場合のみstateを書き換えます
  • コンストラクタ
    • ストレージ変数ownerにmsg.sender(=デプロイするマルチシグウォレットのアドレス)を保存します
      • owner変数はupdate関数の実行を所有するマルチシグウォレットに限定する等で使います
    • isFinalizedで参照するオブジェクトのcounterfactual addressとそのnonceのmappingを保存します

チャンネルにデポジットした資産は、チャンネルの中でさらにそれを使うアプリケーションのオブジェクトにアサインされます。 例えば、AliceとBobが5ETHずつデポジットし合計が10ETHで、ペイメントと賭けチェスのcounterfactual objectがある場合、ペイメントに8ETH、賭けチェスに2ETHという形でアサインできます。 これによって、コードにバグがあった場合に被害をアサインされた分に止めることができます。

Counterfactual State

後述するUpdateプロトコルで、update関数をコールするコミットメントを作ることで、このコントラクトのstateが"counterfactual"になっていると言えます。
このようなオフチェーンのstateを、 counterfactual state と呼びます。以下の二つに分かれます。

  • nonce
    • counterfactual stateのバージョンを表す値(uint256)です
    • application-specific stateがオフチェーンで更新される(Update)ごとにインクリメントされます
  • application-specific state
    • アプリケーション特有のstateです
    • 各参加者とその残高のmappingや、チェス盤の状態などです

nonceによる条件付きファイナライズ

isFinalizedの条件に、コンストラクタで設定した他の特定のオブジェクトのnonceの条件が含まれていることで、あるオブジェクトを他のオブジェクトに依存させることができます。
次の図では、他のあるオブジェクトのnonce=3に依存するPayment objectを、nonceのインクリメントで削除しています。
f:id:nrryuya:20180717163543p:plain

これにより、複数のcounterfactual objectのstateの更新とファイナライズと削除を同時に行うことができます。
次の図は、ペイメントのオブジェクトの残高を減らし、賭けポーカーのオブジェクトに移動させています。 f:id:nrryuya:20180717163354p:plain

このように、複数のオブジェクトをオフチェーンでアトミックに更新することで、ペイメントの残高は減らしたけどポーカーのオブジェクトのコミットメントに相手が署名してくれなかった、などの問題を防ぐことができます。 基本的なチャンネル構成では、全てのアプリケーションのオブジェクトを依存させるcounterfactual objectである Root Nonceオブジェクト を用いて、複数のオブジェクトのアトミックな更新を行います(以下の図を参考)。

f:id:nrryuya:20180717213457p:plain

基本プロトコル

ここまで紹介したものを使った以下の3つのプロトコルで各種コミットメントを作ります。

Instantiate

Registyコントラクトのdeploy()でコントラクトをデプロイするコミットメントに全員で署名します。 (コントラクトのコードとコンストラクタに渡す引数が合意されます) 参加者の誰もがいつでもデプロイできる状況になるので、counterfactual objectになります。

f:id:nrryuya:20180721121639p:plain

CommitWithdrawal

Registryを経由し、counterfactual objectのwithdraw()で資産を引き出すコミットメントに全員で署名します。

f:id:nrryuya:20180721121715p:plain

withdraw()は、マルチシグウォレットにデポジットされたetherを送ったりERC20トークンならトークンコントラクトのtransferFrom()をコールするので、マルチシグウォレットコントラクトのコンテキストで実行される必要があります。したがって、delegatecallが2回繰り返されています。 マルチシグウォレットのコンテキストで実行されるwithdraw()の中では、このオブジェクトのstateを参照することができないので、getState()をcallして参照しています。

Update

Registryを経由し、counterfactual objectのupdate()でstateを更新するコミットメントに全員で署名します。 例えば賭けチェスのアプリケーションの場合は、update()によってチェス盤の状態を変えていきます。

f:id:nrryuya:20180721121701p:plain

ここでも、delegatecall/callには意味があり、呼び出し元をマルチシグウォレットにし、update()では正しいマルチシグウォレットが呼び出し元かチェックします。update()の実行はそのコントラクトのコンテキストになるので、stateの書き換えが可能です。

アプリケーション導入の流れ

それでは、ここまで説明してきた仕組みとプロトコルを用いて、どのようにチャンネルに新しいアプリケーションを導入するのか見ていきましょう。 先ほどの以下の図のように、ペイメントのみを行なっているチャンネルにおいて、ポーカーを始める場合を考えます。 f:id:nrryuya:20180717163354p:plain

なお、表記について、

  • k: ある時点でのRoot Nonceオブジェクトのnonce
  • n: チャンネルの参加者の人数
  • P_i: 参加者iのアドレス
  • s_i: ある時点での参加者iのetherの残高
  • r: Root Nonceオブジェクトのcounterfactual address

とします。

① ポーカーコントラクトをデプロイするコミットメントに全員で署名

  f:id:nrryuya:20180721161515p:plain

  この時、ポーカーコントラクトはRoot Nonceのk+1に依存させます。
  p_iが参加者iのポーカーへのデポジットを表します。

② ポーカーコントラクトのstateを参照した引き出しのコミットメントに全員で署名

  f:id:nrryuya:20180721161658p:plain

③ ペイメントコントラクトをデプロイするコミットメントに全員で署名

  f:id:nrryuya:20180721161931p:plain

  ポーカーコントラクトにデポジットした分残高(balance)を減らしています。
  ポーカーコントラクトと同じくRoot Nonceのnonceがk+1の場合に依存させます。
  (元々ペイメントコントラクトのコミットメントはあったはずなので、新しく作り直した形になります)

④ ペイメントコントラクトのstateを参照した引き出しのコミットメントに全員で署名

  f:id:nrryuya:20180721163310p:plain

⑤ Root Nonceオブジェクトのnonceをk+1に更新

  f:id:nrryuya:20180721163346p:plain

こうして、ポーカーのアプリケーションの導入がオンチェーンの処理なしに完了したことになります。
ポーカーをプレイする場合は、Updateによってポーカーのオブジェクトのstateを更新していけば良いです。 (ちなみに、ポーカーゲームなど乱数列が必要な場合は、各自がハッシュチェーンを事前に作って、毎回一つずつ最後から順に提出させるという方法をとります)

紛争解決とチャレンジについて

相手が応答しなくなったなどの紛争解決時は、InstantiateのコミットメントとUpdateのコミットメントをマルチシグのexecuteTransaction関数に渡します。
前回の記事で紹介した非協力的クローズに近いですが、Counterfactualでは、チャンネルをクローズすることなく、アプリケーション別にオンチェーンに逃げることができます。例えば先ほどの例なら、ポーカーのオブジェクトとそれが依存するRoot Nonceオブジェクトのコミットメントだけを提出することができます(オブジェクト指向的な設計の特徴)。

この時、コントラクトにあらかじめ規定して置いたチャレンジ期間が終了するまでオフチェーンで合意されたstateは確定されません。期間内にもっと新しいコミットメントが提出されupdate関数がより大きいnonceを引数に呼ばれた場合、古いstateは捨てられます。チャレンジ期間が終了しstateが確定したら、CommitWithdrawalのコミットメントを提出して引き出しを行うことができます。

チェスなどのゲームでは、コミットメントが提出されてチャレンジ期間が終了しても、勝敗が決まっていなければファイナライズしないで、オンチェーンでチェスを続けられるような設計が考えられます(負けそうになったらコミットメントを提出してデポジットを引き出す戦略を防ぐため)。

開発状況と今後

まだ、Generalized State Channelはメインネットで使う事はできません。
Counterfactualのコア開発者たちは、今後開発や検証をしつつ、様々なチームで協力して 技術の標準化 に取り組んでいきたいと発言しています。EthereumのState ChannelはまだLightning NetworkのBOLTのような標準仕様がないのですが、仕様がバラバラになってしまうと、プロトコルが異なる相手とチャンネルを開けなかったり、State Channelにおいてオフチェーンの状態の保持などを実際に担当するクライアントアプリのSDKなどもバラバラになってしまうなどの問題が起きるので、標準化は非常に大事です。
また、Counterfactualだけではなく、Celer Networkも開発を進めています。こちらはSDKを自社で開発し、ICOが前提かつそのトークンのエコシステムも作っていくなど、少し毛色が異なります。

最後に

今回はCounterfactualのGeneralized State Channelとしての性質に注目し、新しいアプリケーションを導入し実行するプロトコルについて紹介しました。
出来る限りイメージがつきやすいように、一部実装のレベルまで具体的に書いたのですが、ホワイトペーパーはもっと設計の自由度を感じさせるような書き方をしていますし、上記はあくまで一つのやり方に過ぎません。 また、ホワイトペーパーでは、今回紹介できなかったチャンネルのオープンやデポジットの追加・引き出し(スプライシング)などの他のプロトコルや、 State Channelの一般的な脅威モデルや、Meta Channel という仲介の仕組み(次回以降の記事で紹介します!)についても解説されているので、興味のある方は是非読んでみてください!(そして語り合いましょう!)

参考

Counterfactual: Generalized State Channels

宣伝

LayerXではブロックチェーン技術の研究・開発を進めており、メンバーを募集しています。

少し話を聞いてみたい!という方も歓迎していますので、下記のリンクからの応募お待ちしております。

www.wantedly.com

また弊社では、blockchain.tokyo を主催しています。

イベントグループへメンバー登録をしていただくとイベント参加者募集の通知が来るので、こちらもぜひ登録してみてください。

blockchain-tokyo.connpass.com