読者です 読者をやめる 読者になる 読者になる

Develop with pleasure!

福岡でCloudとかBlockchainとか。

Ethereumのコントラクトのロジックの更新

Ethereum Solidity

Ethereumのコントラクトはブロックチェーンに記録されると基本的に変更ができない。

じゃあコントラクトにバグがあったり機能追加したい場合にどうするの?

という疑問がわく。

DELEGATECALL

Homesteadから導入されたEIP-7として定義されているDELEGATECALLというopcodeを使う方法がある。

EIPs/eip-7.md at master · ethereum/EIPs · GitHub

DELEGATECALLを使うと、コントラクトの実行時に別のアドレスから動的にコードをロードすることができる。その際コードのみが別のアドレスからロードされ、ストレージや現在のアドレス、残高は呼び出し元のデータが参照される。

このDELEGATECALLを使っているのがライブラリ。
このライブラリについてあまり知らないので↓のSolidityのドキュメントを見てみる。

Contracts — Solidity 0.4.3-develop documentation

Libraries

ライブラリはコントラクトに似ているが、ライブラリは特定のアドレスに一度だけデプロイされ、そのコードはEVMのDELEGATECALLの機能を使って再利用される。ライブラリの関数が呼び出された際、そのコードは呼び出し元のコントラクトのコンテキストで実行される。ここでポイントとなるのは、呼び出し元のコントラクトのコンテキストで実行されるということであり、呼び出し元のコントラクトのストレージにアクセスできるということである。

ライブラリはそれを使うコントラクトの暗黙的なコントラクトとみなすことができる。明示的な継承関係があるようには見えないが、ライブラリ関数の呼び出しはまさに暗黙のベースコントラクトを呼び出しているように見える。またライブラリの内部関数は全てのコントラクト内で参照可能になる。当然ながら内部関数を呼び出す際は内部呼び出し規約に従う(=全ての内部型を渡すことができ、メモリ型は値のコピーではなく参照が渡される)。
EVMでこれを実現するために、internal宣言した内部ライブラリ関数(およびそこから呼び出される全ての関数)のコードは、呼び出し元のコード呼び出しの際に、`DELEGATECALLに代わってJUMPコールが使われる。

以下はライブラリの使い方を示している。

pragma solidity ^0.4.0;

library Set {
  // 呼び出し元のコントラクトにデータを保存するのに使うデータ型を定義
  struct Data { mapping(uint => bool) flags; }

  // 最初のパラメータはストレージ参照型で、渡ってくるのはストレージのアドレスであって、ストレージのコンテンツが渡ってくるわけではないので注意。
  // これはライブラリの特別な機能で、関数の最初のパラメータは必ずselfになる。
  function insert(Data storage self, uint value)
      returns (bool)
  {
      if (self.flags[value])
          return false; // already there
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      returns (bool)
  {
      if (!self.flags[value])
          return false; // not there
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      returns (bool)
  {
      return self.flags[value];
  }
}

contract C {
    Set.Data knownValues;

    function register(uint value) {
        // ライブラリのインスタンスを指定することなくライブラリ関数を呼び出しているが、この時のインスタンスは現在のコントラクトとなる。
        if (!Set.insert(knownValues, value))
            throw;
    }
    // このコントラクト内では、直接 knownValues.flagsにアクセスできる。
}

Set.containsSet.insertSet.removeの呼び出しは全て外部のコントラクト/ライブラリ呼び出し(DELEGATECALL)としてコンパイルされる。ライブラリを使う場合は、実際は外部関数呼び出しが行われていることに注意する。

以下の例では、外部関数呼び出しのオーバーヘッド無しにカスタム型を実装するために、ライブラリ内のメモリ型と内部関数を利用する方法を示している。

pragma solidity ^0.4.0;

library BigInt {
    struct bigint {
        uint[] limbs;
    }

    function fromUint(uint x) internal returns (bigint r) {
        r.limbs = new uint[](1);
        r.limbs[0] = x;
    }

    function add(bigint _a, bigint _b) internal returns (bigint r) {
        r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
        uint carry = 0;
        for (uint i = 0; i < r.limbs.length; ++i) {
            uint a = limb(_a, i);
            uint b = limb(_b, i);
            r.limbs[i] = a + b + carry;
            if (a + b < a || (a + b == uint(-1) && carry > 0))
                carry = 1;
            else
                carry = 0;
        }
        if (carry > 0) {
            // too bad, we have to add a limb
            uint[] memory newLimbs = new uint[](r.limbs.length + 1);
            for (i = 0; i < r.limbs.length; ++i)
                newLimbs[i] = r.limbs[i];
            newLimbs[i] = carry;
            r.limbs = newLimbs;
        }
    }

    function limb(bigint _a, uint _limb) internal returns (uint) {
        return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
    }

    function max(uint a, uint b) private returns (uint) {
        return a > b ? a : b;
    }
}


contract C {
    using BigInt for BigInt.bigint;

    function f() {
        var x = BigInt.fromUint(7);
        var y = BigInt.fromUint(uint(-1));
        var z = x.add(y);
    }
}

↑みたいにinternal宣言して内部関数とした場合、コントラクトから受け取るパラメータはEVM側でJUMP命令になり参照渡しになる。internal宣言しない場合はDELEGATECALL命令のままで値渡しになると。

ライブラリがデプロイされる場所をコンパイラが分からないように、これらのアドレスはリンカーによって最終的なバイトコードに入れられる。(リンクするのにコマンドラインコンパイラをどう使うかはUsing the Commandline Compilerを参照。)アドレスがコンパイラの引数に与えられてない場合は、コンパイルされたhexコードに__ライブラリの名前______ というフォーマットのプレースホルダが含まれている。このアドレスはライブラリコントラクトのアドレスをhexエンコードした値に手動で置き換えることができる。

コントラクトと比較した場合のライブラリの制約は↓

  • 状態変数が無い
  • 継承することも継承させることもできない
  • Etherを受け取れない

Using For

using A for B;という指定は、任意の型BにライブラリAのライブラリ関数をアタッチする際に使われる。これらの関数は最初のパラメータとして関数を呼び出したオブジェクトを受け取る(Pythonのselfみたいなイメージ)。

using A for *;と指定するとライブラリAの関数が任意の型にアタッチされる。

どちらの場合でも、最初のパラメータの型がオブジェクトの型と合わなくても全ての関数がアタッチされる。型は、関数が呼ばれ関数のオーバーロードの解決が実行された後にチェックされる。

using A for B;という指定は、現在のスコープでアクティブで、現在のコントラクトに限定されているが、後にグローバルスコープに上がる。ライブラリ関数を含むデータ型は、追加のコードなく利用可能である。

↑のライブラリは↓のように書き換えることができる。

pragma solidity ^0.4.0;

// このライブラリはコメントが無いだけで↑のコードと一緒
library Set {
  struct Data { mapping(uint => bool) flags; }

  function insert(Data storage self, uint value)
      returns (bool)
  {
      if (self.flags[value])
        return false; // already there
      self.flags[value] = true;
      return true;
  }

  function remove(Data storage self, uint value)
      returns (bool)
  {
      if (!self.flags[value])
          return false; // not there
      self.flags[value] = false;
      return true;
  }

  function contains(Data storage self, uint value)
      returns (bool)
  {
      return self.flags[value];
  }
}


contract C {
    using Set for Set.Data; // ←ここが変更点
    Set.Data knownValues;

    function register(uint value) {
        // ここではSet.Data型の全ての変数はメンバ関数に対応している。
        // ↓の関数呼び出しは、 Set.insert(knownValues, value)と同じである
        if (!knownValues.insert(value))
            throw;
    }
}

↓のように基本型を拡張することも可能。

pragma solidity ^0.4.0;

library Search {
    function indexOf(uint[] storage self, uint value) returns (uint) {
        for (uint i = 0; i < self.length; i++)
            if (self[i] == value) return i;
        return uint(-1);
    }
}


contract C {
    using Search for uint[];
    uint[] data;

    function append(uint value) {
        data.push(value);
    }

    function replace(uint _old, uint _new) {
        // ↓はライブラリ関数を呼び出している
        uint index = data.indexOf(_old);
        if (index == uint(-1))
            data.push(_new);
        else
            data[index] = _new;
    }
}

全てのライブラリ呼び出しは、実際はEVMの関数呼び出しであることに注意する必要がある。そのためメモリ型or値型を渡す場合、コピーが実行される。唯一ストレージ型を使った場合だけコピーが発生しない。

コントラクトを置き換える方法

↓のような仕組みを作ればコントラクトのロジックの更新が可能になりそう。

  • ライブラリの呼び出し元のコントラクトのコンテキストで実行されるという特性を利用して、ロジックをライブラリとして実装し、コントラクトからそのライブラリ関数を実行するようにする。
  • コントラクト内で最新のライブラリのアドレスを管理できるようにする。
  • 置き換えるロジックはライブラリに記述しているので、ロジックを更新する際は、ライブラリを書き換えデプロイし、更新済みの新しいアドレスを入手する。
  • コントラクト内のライブラリのアドレスを新しいアドレスに更新する。