2020.01.09

Plasmaフレームワークのアーキテクチャの紹介

Pocket

こんにちは。次世代システム研究室のL.W.です。

 

Plasmaフレームワークというと、システムとしてどのような仕組みだかっていう疑問を持つ方がいるかと思います。

今回はオーバービューで、plasma-rust-frameworkのOSSをベースに、Plasmaの仕組みを覗いていきましょう。

 

Plasmaフレームワークは、少なくとも三つのコンポーネントが必要です。


クライアント(Client)

エンドユーザ、オペレーター、イーサリアムのスマートコントラクトと対話(インターアクション)する機能を持つコンポーネントです。

ウォレット管理、トランザクション管理、ステート管理、スマートコントラクトとのやり取り(deposit、Exit、Challengeなど)、自分と関わるデータのローカル保持などを行います。

 

オペレーター(Operator)

クライアント、イーサリアムのスマートコントラクトと対話(インターアクション)する機能を持つコンポーネントです。

クライアントのトランザクションをチェック処理し、一定の時間で集めたトランザクション、ステートをインプットとして、Merkle Treeを構築し、Merkle TreeのRootをイーサリアムに提出します。

全てのデータをローカルに保持し、フルノードの存在となります。

 

スマートコントラクト(Smart Contract)

レイヤ1でデプロイされます。

disputeロジック実現できたので、レイヤ1と同様のセキュルティーが裏付けます。

 

クライアント(Plasma-Client)

全体的な構図は以下のようとなります。

 

RpcServer

RpcServerは、エンドユーザーが操作できるJSON RPCサービスを提供します。

これらのリクエストを満たすためには、他のさまざまなRPCメソッドを呼び出します。

ウォレットアカウントの作成、トークンの残高の表示、支払履歴の表示、送金、スワップのトランザクションの作成などのエンドユーザーからのリクエストは想定されます。

 

RpcClient

RpcClientは、他のクライアントおよびオペレーターとのインターアクションを行います。

Plasmaクライアントの場合、オペレーターに新規作成したトランザクションを送ったり、オペレーターからブロックコンテンツあるいはMerkle Treeの存在証明などを求めたりなどを行います。

State Channelクライアントの場合、他のクライアント同士とのメッセージ交換などを行います。

 

OVM Runtime

OVMとは、レイヤ2ソリューションズ(Plasma, State Channel, Rollupなど)を抽象化し、同じ枠組みで扱えるものとなります。
OVM Contract(L1)とOVM Runtime(L2)を分けられています。
OVM ContractとOVM Runtimeのインターアクションにより、イーサリアム並みのセキュルティー性は裏付けられます。
OVM Runtimeで送信されたクレームをローカル情報(各種DB情報)によってtrueまたはfalseに評価することもできます。
簡単にまとめると、このRuntimeで、さまざまな機能をサポートし、ステート、トランザクション、サイン、存在証明の履歴などの情報の保存と更新などを行います。

OVMはあくまでバーチャルマシンであり、バーチャルマシンの役目はインストラクションを実行することです。 そのためには、OVMはインストラクションのベースインターフェイスを定義する必要があります。 OVMはレイヤ2からのクレームとそのクレームの紛争に完全に基づいているため、そのインストラクションはすべて「入力NがルールセットSに従って有効かどうかを判断する」という形式に従う必要があります。

こうするために、OVM RuntimeにはOVMならではのプロパティ(Property)、ステート(StateUpdate)、トランザクション(Transaction)の概念が定められています。

 

Property

struct Property {
    pub decider: Address,      // L1のスマートコントラクトのアドレス。対応するコントラクトはインプットを真/偽か判定できます。
    pub inputs: Vec<PropertyInput>, // レイヤ2のローカル情報であり、判定される対象となります。
}

StateUpdate

struct StateUpdate {
    block_number: Integer,       // ブロック番号
    deposit_contract_address: Address, // トークンアドレス
    range: Range,                      // 紐づいているレンジ
    property: Property,                // プロパティ
}

Transaction

struct Transaction {
    deposit_contract_address: Address, // トークンアドレス
    range: Range,                       // レンジ
    parameters: Bytes,                  // プロパティ
    signature: Bytes,                   // 署名後のメッセージ(トークンアドレス、レンジ、プロパティ)
    metadata: Metadata,                 // メタデータ(トランザクションの性質を定義する)
}
 

StateUpdateとTransactionの関係

ownershipの場合を例として挙げます。

 

 

EventWatcher

EventWatcherは、Ethereumのさまざまな重要なイベントを監視します。 他のコンポーネントは、EventWatcherに特定のイベントを監視するようにリクエストでき、イベントが発生するたびに通知されます。イベントを見逃したり、同じイベントをコンポーネントに複数回通知したりしないように設計されています。

クライアントには、オペレーターのブロック提出したイベント(BlockSubmitted、他のクライアントのExitのリクエストしたイベント(ExitStarted)などを監視しないといけないです。

オペレーターには、クライアントのExitのリクエストしたイベント(ExitStarted)、入金デポジットしたイベント(Deposited)などを監視しないといけないです。

ContractWrapper

ContractWrapperは、イーサリアムのスマートコントラクトの単純なラッパーです。

各コンポーネントはイーサリアムとのインターアクションはContractWrapper経由となります。

 

DB

このPlasma Rustフレームワークでは、LevelDBは使われています。

LevelDBはkey-value型のデータストアの一つです。イーサリアムもこれを利用します。

イメージとして、以下のようになります。

データシリアライズするために、RLPエンコーディングを採用しました。

Plasma Rustの場合、Rangeベースにステート、トランザクション、Merkle Treeの存在証明のCRUDは頻繁に行われるなので、設計では工夫されました。

 

StateDB

ステート(ブロック番号、トークンアドレス、プロパティ)に関する情報を保存します。

Key: deposit_contract_address + Range(start, end).end

Value:
struct StateUpdateRecord {
    block_number: Integer,
    deposit_contract_address: Address,
    property: Property,
}
上の「StateUpdateとTransactionの関係」の例では、StateDBの変化は以下のようとなります。

TransactionDB

トランザクション(トークンアドレス、レンジ、パラメータ、サイン、メタデータ)に関する情報を保存します。

Key: 参照されたステートのblock_No. + Range(start, end).end

Value:
struct Transaction {
    deposit_contract_address: Address,
    range: Range,
    parameters: Bytes,
    signature: Bytes,
    metadata: Metadata,
}
上の「StateUpdateとTransactionの関係」の例では、TransactionDBの変化は以下のようとなります。

 

RangeAtBlockDB

Plasma RustではMerkle Interval Treeを採用しました。

 

あるステートはブロックに存在するかの証明情報(Merkle TreeのRoot、チェック結果、証明情報、対象のステート)を保存します。

Key: 新規作成されたステートのblock_No. + deposit_contract_address + Range(start, end).end

Value:
struct RangeAtBlockRecord {
    pub root: Bytes,
    pub is_included: bool,
    pub inclusion_proof: Bytes,
    pub state_update: StateUpdate,
}

上の「StateUpdateとTransactionの関係」の例では、Range(0,5)のステートをUTXOとしてDBに更新する。

 

SignedByDB

署名されたメッセージ情報(パブリックキー、メッセージ、署名)を保存します。

Key: PublicKey + Hash(Message)

Value:
struct SignedByRecord {
    pub public_key: Address,
    pub message: Bytes,
    pub signature: Bytes,
}

トランザクション毎にSignedByDBを追加します。

WalletManager

エンドユーザー向けに、ウォレットを管理するコンポーネントです。

例えば、Public keyとPrivate keyのペアの生成、Private Keyのインポートなどを行います。

 

WalletDB

セッションとPrivate Keyのペアを保存します。

Key: セッション

Value: Private Key

 

EventDB

イーサリアム発火したイベントと関する情報を保存します。

 

オペレーター(Plasma-Operator)

全体的な構図は以下のようとなります。

 

RpcServer

クライアントのリクエストに応じて、レスポンスを送ります。

例えば、クライアントはイーサリアムからBlockSubmittedというイベントを受信したら、オペレーターにブロックコンテンツをリクエストし、

誠実なオペレーターの場合、正しいトランザクション、ステートの含めたブロックコンテンツを送付します。

トランザクションのMerkle Tree存在証明を送ったり、クライアントからのトランザクションをブロードキャストしたりすることもあります。

 

BlockManager

トランザクション、ステートをDBに一時的に保存したり、まとめたステートをleafとして、Merkle Treeを構築したりします。

ContractWrapperモジュールを呼び出して、イーサリアムに接続してから、Merkle TreeのRootを提出します。

イーサリアムのOVM ContractにRootはきちんと登録されたら、BlockManagerはブロックコンテンツをDBに永久に保存します。

 

BlockDB

ブロックに関する情報を保存します。

Key: Block番号

Value:
struct PlasmaBlock {
    block_number: Integer,
    state_updates: Vec<StateUpdate>,     // ブロックに含めたステート
    transactions: Vec<NewTransactionEvent>, // ブロックに含めたトランザクション
    tree: Option<DoubleLayerTree>,      // Merkle Interval Tree
}
 

送金のシナリオ

 

AliceはBobに5ETHを送金することとします。

 

クライアントのAliceサイド

1.Aliceは送金アカウントを作ります。

WalletManagerとWalletDBモジュールが使われます。

 

2.ステートでのRangeの残高を確認する

OVM RuntimeモジュールからStateDBで5ETHあるいは5ETH以上の持つState(UTXO)を検索します。

検索結果がない場合、送金不能となります。

検索結果が出たら、このStateをUTXOとしてトランザクションを新規に作成します。

 

3.トランザクションの作成、署名、発送

ウォレットのPrivate Keyでトランザクションを署名し、RpcClientモジュールを通して、オペレーターに送ります。

 

オペレーターサイド

1.トランザクションのチェック処理、保持処理を行います。

このトランザクションはオーナー本人からの署名だか、二重払いないかをチェックを行います。

問題がなければ、OVM RuntimeモジュールよりStateDB、TransactionDB、SignedByDBを更新します。

不正が発見できれば、廃棄します。

同時ににこのトランザクションとこのトランザクションの含めたのステートをBlockManagerモジュールによりBlockDBで一時的に保持されます。

 

2.トランザクションのブロードキャストを行います。

受け取る側のBobを受信できるように、このトランザクションをブロードキャストを行います。

 

3.ブロックのMerkle Treeの構築とRootの提出を行います

一定の時間間隔あるいは集まったトランザクションは上限値を超えた場合、BlockManagerモジュールにより一時的に集めたトランザクションとステートをインプットとしてMerkle Treeを構築します。

ContractWrapperモジュールを通して、イーサリアムのOVM Contractに提出します。

BlockDBで一時的に保持されたトランザクションとステートをクリアします。

 

クライアントのBobサイド

1.イベントが監視されて、オペレーターにブロックコンテンツをリクエストします。

EventWatcherモジュールにより、BlockSubmittedというイベントを監視できたら、RpcClientモジュールよりブロックコンテンツをリクエストします。

 

2.トランザクションのチェック処理を行います。

トランザクションの正しさとトランザクションの含めたステートの履歴をチェック処理を行います。

問題がある場合(不正署名、不正履歴)、取引は失敗となります。

 

3.ローカルDBのアップデートを行います

ライトクライアントの場合、自分と関わるトランザクション、存在証明情報だけをローカルに保持します。

ライトクライアントではない場合、全てのトランザクション、存在証明情報をローカルに保持します。(現状のPlasma Rustはこのケースに該当します。)

StateDB、TransactionDB、RangeAtBlockDB、SignedByDBをアップデートをします。

 

4.取引は成立となります。

ファイナリティとみなされる場合、取引は成立となります。

実際にはPlasmaの実質的なファイナリティはイーサリアムに頼りますが、Cryptoeconomic finality という案でファイナリティを短縮できる研究は進んでいます。

 

 

スマートコントラクト(OVM Smart-Contract)

 

レイヤ1が得意なことだけにレイヤ1を活用し、スマートコントラクトは明確に定義されたルールセットに基づいて、不変で全会一致のコンセンサスを達成することです。

OVM Contractではレイヤ2のクレームの裁定に使用する明確なルールセットが定義されています。

裁判官のように、ルールが常に守られていることを保証するためにすべてを積極的に監督することより、ルールが守られていないという主張(クレーム)を遡及的に処理するために使用する方がはるかに効率的です。

 

全体的な構図は以下のようとなります。

ソースはまだ未完成かと思いますが、こちらも解読中です。

ただ、ロジックは複雑ですが、セキュリティー問題が伴っているかと思います。監査(Audit)はどのようにしますか、興味深いですね。

今回はスマートコントラクトの詳しい解説を割愛させて頂きます。

 

submitRoot

Merkle TreeのRootをスマートコントラクトのストレージに保持します。同時にPlasmaチェーンのブロック番号を割振りします。

イーサリアムのブロックに無事に含めば、BlockSubmittedのようなイベントを発火します。

 

deposit

ETHあるいはERC-20を入金します。

入金金額により、Rangeを割振りします。源のステートを生成します。
CheckpointFinalizedのようなイベントを発火します。

finalizeExit

ExitのプロパティのクレームのステータスはTrueと判定できれば、イーサリアムのアカウントに出金します。
ExitFinalizedのようなイベントを発火します。

finalizeCheckpoint

CheckpointのプロパティのクレームのステータスはTrueと判定できれば、あるRangeの履歴を確定します。
CheckpointFinalizedのようなイベントを発火します。

challenge

不正クレームにチャレンジします。

claimProperty

あるプロパティ(例えば、Exit)をクレームします。
スマートコントラクトのストレージでこのクレームのステータスがUndecided、True、False三つがありますが、
この場合、ステータスをUndecidedに初期化されます。

decideClaimToTrue/decideClaimToFalse

スマートコントラクトのストレージでこのクレームのステータスをTrue、Falseに設定します。

decideTrue/decideFalse

ローカル情報をインプットとして、Predicateに送ります。Predicateは関係するプロパティはTrueかFalseか判定できます。
判定結果により、UniversalAdjudicationContractコントラクトのsetPredicateDecision方法を呼び出す。

setPredicateDecision

スマートコントラクトのストレージでこのクレームのステータスをTrueかFalseかに設定します。

isValidChallenge

チャレンジしようとするプロパティはルールに従うかチェックを行います。

verifyInclusion

あるステートはMerkle Interval Treeに含められるか検証します。
ローカル保持した存在証明でTreeを再構築し、TreeのRootはストレージで保持されたRootと一致かチェックを行います。
 

まとめ

Plasma Rustはまだ開発中に見えますが、Todoもまだ沢山が残っています。

読めない部分もあれば、こちらの理解間違い部分もありますが、皆さんのこのOSSへの理解に一助となれば幸いと思います。

これからはOVM contractの進展を見守って行き、心得ができれば、また共有します。

 

さいごに

次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。

皆さんのご応募をお待ちしています。