対応方法 - バージョンアップ問題

バージョンアップの対応は簡単ではないので、順を追って対応方法を整理します。

CNSの導入

まず、下記のような ContractNameServcie という概念を導入します。

CNSは、DNS(DomainNameSystem)と同じような働きをします。

  • クラス図

    CNS

    このクラスは、コントラクト名と、コントラクトアドレスをマッピングして保持しており、コントラクト名に対して対象のContractのアドレスを返却します。

  • シーケンス図

    CNSSeq

    ある特定のコントラクトを呼び出す際に、必ずCNSにコントラクト名を元にアドレスを聞いてからユーザが対象のコントラクトを呼ぶように呼出側のアプリケーションを構築します。

    そうすることで、コントラクトにバグがあった場合も別のコントラクトに向き先を変えることが可能になります。

コードにすると次のようになります。

  • コード

    contract ContractNameService {
    
        address provider = msg.sender;
        mapping (bytes32 => address) contracts;
    
        modifier onlyByProvider() { if (msg.sender != provider) throw; _; }
        function setContract(bytes32 _contractName, address _contract) onlyByProvider {
             contracts[_contractName] = _contract;
        }
    
        function getContractAddress(bytes32 _contractName) constant returns(address) {
             return contracts[_contractName];
        }
    }
    

サービスプロバイダ限定でのバグ修正

ここでのポイントはContractNameService作成時に、msg.senderをproviderとしてセットしている点です。

address provider = msg.sender;

さらにこれを

modifier onlyByProvider() { if (msg.sender != provider) throw; _; }
function setContract(bytes32 _contractName, address _contract) onlyByProvider {
    contracts[_contractName] = _contract;
}

とすることで、このCNSにコントラクトを登録できるのは、CNSをデプロイしたプロバイダだけにすることができます。

つまり、CNSと、CNSに登録されているコントラクト群は、全てCNSのプロバイダが提供したものとなります。

過去のデータ問題

CNSの導入でバージョンアップの問題は解消したように見えますが、まだ課題が残っています。

過去のバージョンで管理していたデータの問題です。

問題を明確化するために、仮に「Someone」というnameフィールドを保持するコントラクトを作成し、バージョンアップしてageというフィールドを追加した状態を考えてみます。

GAS使用量問題に対応するためにフィールドはstructureとして分離しています。 )

  • クラス図

    SomeoneContract

    一見正常にバージョンアップしているように見えますが、オブジェクト図にしてみると、問題が顕在化します。

  • オブジェクト図

    CNS Object

    バージョンアップして Someone_v1 がCNSに登録された状態を見ると、過去の Someone_v1 時に登録していたデータ群(SomeoneStruct_v1)が新しいContractから切り離されて扱えなくなってしまいます。

過去データに対する解決策

これに対して考えられる解決策はいくつか存在します。

  1. Someone_v1#fieldsのアドレスをSomeone_v1に設定する。

    これは、一般的な開発言語なら可能ですが、分散型プログラムではmappingオブジェクトのアドレスの受渡しのようなことはできません。

  2. Someone_v1#fieldsに格納されているデータを全件 Someone_v1 に移行する。

    これは数件なら問題ないですが、件数が多くなると大量のデータ書込が発生します。

    書込み処理に対するGASの消費量は非常に多くなるため、現実的ではありません。

  3. Someone_v1にSomeone_v1をフィールドとして内包させる。

    この解決策であれば、1フィールドに対して旧バージョンのアドレスを書き込むだけで済むので、それほどGASを消費しなくて済みます。

Fieldコントラクトの追加

今回 Someone_v1 のアドレスを Someone_v1 のフィールドとして抱えることで、旧バージョン(Someone_v1)のデータを引き続き使えるようになりましたが、Someone_v1で新規に追加した関数(Someone_v1#someLogic2)から扱い易い形式で、Someone_v1#name へのアクセッサが用意されているとも限りません。

将来必要になるであろう関数を事前に用意するのは至難の技なので、基本的にはフィールドは専用のフィールドコントラクトを生成して、あらゆるフィールドに対するアクセッサ(getter / setter)を付けておきます。

  • クラス図

    フィールドの分離

    これでバージョンアップ後も旧フィールドにアクセスできないという事態はなくなります。

    ただ、フィールドへの書込み(setXxx)が誰でもできてしまっては、書き込まれたデータの信憑性がなくなってしまいます。

    とはいえ、バージョンアップの度に SomeoneField_v1 で呼び出し元の許可アドレス(Someone_v1やSomeone_v1)を管理するのも非常に困難です。

    そのため、Fieldクラスでは、『CNSに"Someone"として登録されているコントラクトからの呼出は全て許可する』という形にしておくことで、バージョンアップしてもセキュリティレベルが崩されずに対応できます。

これらの権限管理については、本サービスの関連ソースをOpenSourceとして提供しますので、こちらを利用して簡単に実装できます。

VersionUpで追加したフィールド

バージョンアップ時に追加したフィールド(今回の例では"age"フィールド)について、過去の"age"フィールドがないデータ群をどう扱うかも問題になります。

RDBのように、『全過去データに一括してデータ投入する』というようなことをすれば、莫大なGAS
が消費されます。

そのため、バージョンアップ後も必要がなければデータ記録しないで済むようにFieldコントラクト内で工夫をします。

  • getter

    前述の例の SomeoneField_v1#getAge の場合、fieldsのキー自体が存在しない場合(Someone_v1としての登録がない場合)は、過去(SomeoneField_v1)のフィールドも探します。

    過去の履歴を見ても完全新規の場合は throw し、 新バージョンのデータだけがない場合はデフォルト値を返す実装にしておきます。

  • setter

    同様に SomeoneField_v1#setAge の場合、fieldsのキー自体が存在しない場合(Someone_v1 としての登録がない場合)は、過去(SomeoneField_v1)のフィールドも探します。

    過去の履歴を見ても完全新規の場合は、こちらも throw し、 新バージョンのデータだけがない場合はageフィールドに値を設定し、他のフィールドにはデフォルト値を記録しておきます。

これらの処理も複雑なため、今回これらの処理を軽減する親コントラクトを作成し、OpenSourceで公開しているので、ぜひご利用ください。

継承によるロジック肥大化の問題

ここまで対応してもまだバージョンアップには考慮しないといけない問題が存在します。

継承コントラクトの肥大化問題です。

  • クラス図

    フィールドの分離

    この図では Someone_v1 は Someone_v1 を継承していますが、これはコンパイルすると、両方のコントラクトの合算したコントラクトサイズになってしまいます。

    Ethereumにはブロックに入れられる最大容量が決められているため、Contract単体でサイズが大きくなってしまうと、デプロイもできない事態に陥ります。

    そこで下記の対応を取ります。

    1. Logicとインタフェースを分離する。
    2. インタフェースはロジックを持たずにLogicコントラクトに転送だけする。
    3. 継承関係はインタフェース部分だけ残す。

その対応結果は次のような構造になります。

  • クラス図

    CNSFullField

コントラクトが保持するEtherの問題

これまでの対応で、バージョンアップに対する問題はほとんど解消しましたが、最後に一点だけ問題が残っています。

例えば、Depositコントラクトのようなものを作って、ユーザのEtherを管理するスマートコントラクトアプリケーションを作りたいケースを考えます。

  • クラス図

    CNSFullField

ユーザがDepositコントラクトにEtherを渡した場合、渡したEtherは最初に呼び出されたコントラクトが受け取ります。

つまり、Version0のうちは Deposit_v1 のアドレスに Ether が溜まることになり、バージョンアップすると、Deposit_v1 のアドレスに Ether が溜まります。

エンドユーザからすると、v0 も v1 も同じ Deposit コントラクトなので、バージョンを跨いで総額で管理する必要があります。

Deposit_v1 Deposit_v1はインタフェースなので、実際の入出金処理はDepositLogicに記載されていて、そこで他のアドレスへの出金の必要があった場合はDeposit内でEtherを集めて送付先に送る必要があります。

これらの処理は複雑なので、インタフェースの親クラスを用意してそこで実装をしました。
こちらもOpenSourceで公開していますので、そちらをご利用ください。

バージョンアップ可能なコントラクトの構造について

前述の問題を全て解消したコントラクト構成が次の構造になります。

  • クラス図

    全体コントラクト構成

上記空色のコントラクト群はフレームワークとして本サービスからOpenSourceとして提供します。

下記参考ページ内の Version... コントラクト群と、AddressGroup... コントラクト群のソースを見ていただければ、具体的にどのように問題群に対応しているかが把握いただけると思います。

results matching ""

    No results matching ""