このエントリーをはてなブックマークに追加

solidity勉強 (2)

前の続き

可視レベル

可視レベルとして、external, public, internal privateが指定できる。 ステート変数と関数でデフォルト値は違う。 ステート変数の場合internal(javaでいうprotected)がデフォルト値。 関数はpublicがデフォルト値。

詳細は以下を参照。

複数の値の返却

solidityはpythonのtupleのように複数の値を返却できる。

pragma solidity ^0.4.11;

contract test {
    function tuple() private returns (int, string, address) {
        return (1, "hello", msg.sender);
    }

    function vars() returns (int, string, address) {
        var (_i, _message, _sender) = tuple();
        return (_i + 1, _message, _sender);
    }

}

// vars
// Transaction cost: 22238 gas.
// Execution cost: 966 gas.
// Decoded:
//   int256: 2
//   string: hello
//   address: 0xca35b7d915458ef540ade6068dfe2f44e8fa733c

abstract contract

javaのabstractと同じ。 実装が欠けてるとデプロイできない。

pragma solidity ^0.4.11;

contract abstractContract {
    string name = "hoge";

    function getName() returns (string);
}


contract test is abstractContract {
    function getName() returns (string) {
        return name;
    }
}

// getName
// Transaction cost: 22736 gas.
// Execution cost: 1464 gas.
// Decoded:
//   string: hoge

interface

javaのインターフェイスと同じ。 インターフェイスを満たしていないとdeployができない。 abstrct contractとの違いは以下。

  • 他のコントラクトやインターフェイスを継承できない
  • コンストラクタを定義できない
  • 変数、struct、enumを定義できない
pragma solidity ^0.4.11;

contract testInterface {
    function getName() returns (string);
    function getMessage() returns (string);
}

contract test is testInterface {
    string name;
    string message;

    function test() {
        name = "alice";
        message = "bob";
    }

    function getName() returns (string) {
        return name;
    }

    function getMessage() returns (string) {
        return message;
    }

}

arrayの返却

arrayを返却することもできる。ただし、多次元配列やstructのarrayは返却できない。 stringは2次元配列扱いなのでできない。

pragma solidity ^0.4.11;

contract testInt {
    function returnIntArray() returns (int[]) {
        int[] memory a = new int[](3);
        a[0] = 1;
        a[1] = 10;
        a[2] = 100;
        return a;
    }
}

// returnIntArray
// Transaction cost: 22269 gas.
// Execution cost: 997 gas.
// Decoded:
//   int256[]: 1, 10, 100

contract testByte {
    function returnByteArray() returns (byte[]) {
        byte[] memory a = new byte[](3);
        a[0] = byte(1);
        a[1] = byte(10);
        a[2] = byte(100);
        return a;
    }
}

// returnByteArray
// Transaction cost: 22365 gas.
// Execution cost: 1093 gas.
// Decoded:
//   bytes1[]: 0x01, 0x0a, 0x64


// contract testString {
//     // compile error
//     // 2次元配列扱い
//     function returnStringArray() returns (string[]) {
//         string[] memory a = new string[](3);
//         a[0] = 1;
//         a[1] = 10;
//         a[2] = 100;
//         return a;
//     }
// }

// contract testStruct {
//     struct value {
//         int v;
//     }
//     // compile error
//     // structはだめ
//     function returnStructArray() returns (value[]) {
//         value[] memory a = new value[](3);
//         a[0].v = 1;
//         a[1].v = 10;
//         a[2].v = 100;
//         return a;
//     }
// }

constant

状態が変更しないこと明示的に指定する。変数や関数につけられる。 関数では主に状態変更をしないgetterにつけておく。 constantな関数はweb3.js等でmethods.myMethod.callを使うことで、ローカルノードで処理を完結できるためgasコスト無しで処理ができる。ただし、ローカルノードの状態に依存する。

現状、状態を変更する関数にconstantをつけてもコンパイラが怒ってくれないので注意。この状態で実行すると謎の挙動になる。

pragma solidity ^0.4.11;

contract test {
    string name;
    string message;

    function test() {
        name = "alice";
        message = "hello";
    }

    function getName() constant returns (string) {
        return name;
    }

    function getMessage() constant returns (string) {
        return message;
    }

    function getAndUpdateMessage() constant returns (string) {
        var m = message;
        name = "bob";
        message = "zzz";
        return m;
    }
}

// getName
// Transaction cost: 22758 gas. (caveat)
// Execution cost: 1486 gas.
// Decoded:
//   string: alice
// getMessage
// Transaction cost: 22780 gas. (caveat)
// Execution cost: 1508 gas.
// Decoded:
//   string: hello
// getAndUpdateMessage
// Transaction cost: 44069 gas. (caveat)
// Execution cost: 22797 gas.
// Decoded:
//   string: zzz

アドレスからコントラクトへの変換

<コントラクト名>(<コントラクトのアドレス>)でアドレスをコントラクトに変換することができる。 変換したコントラクトの関数を呼び出した場合、address.call同様にコンテキストや呼び出し元が変更されているので注意。

pragma solidity ^0.4.11;

contract A {
    int value = 100;

    function getValue() returns (int, address, address) {
        return (value, this, msg.sender);
    }
}

contract B {
    string message = "zzz";
    address contractA;

    function B(address _contractA) {
        contractA = _contractA;
    }

    function getValue() returns (int, string, address, address, address, address){
        message = "hello";
        var (_v, _this, _sender) = A(contractA).getValue();
        return (_v, message, this, msg.sender, _this, _sender);
    }
}

// 1. Aのコントラクトを作ってアドレスを取得する
//    output: 0x8609a0806279c94bcc5432e36b57281b3d524b9b
// 2. 1で取得したアドレスを指定してBのコントラクトを作る
//    input: "0x8609a0806279c94bcc5432e36b57281b3d524b9b"
//
// getValue
// Transaction cost: 35962 gas.
// Execution cost: 14690 gas.
// Decoded:
// int256: 100
// string: hello
// address: 0x9876e235a87f520c827317a8987c9e1fde804485
// address: 0xca35b7d915458ef540ade6068dfe2f44e8fa733c
// address: 0x8609a0806279c94bcc5432e36b57281b3d524b9b
// address: 0x9876e235a87f520c827317a8987c9e1fde804485

thisやsenderが変わっていることが確認できる。次に、呼び出した関数でrevert()を読んでみる。

pragma solidity ^0.4.11;

contract A {
    int value = 100;

    function getValue() returns (int, address, address) {
        revert();
        return (value, this, msg.sender);
    }
}

contract B {
    string message = "zzz";
    address contractA;

    function B(address _contractA) {
        contractA = _contractA;
    }

    function getValue() returns (int, string, address, address, address, address){
        message = "hello";
        var (_v, _this, _sender) = A(contractA).getValue();
        return (_v, message, this, msg.sender, _this, _sender);
    }
}

// getValue
// Exception during execution.

この場合は、例外になる。address.callのように自前で例外の処理をする必要はない。

contractの変更

一度deployしたcontractを変更することはできない。ベースがブロックチェーンなので、記録はできても消したり変更したりはできない。そこで、変更が必要になった時のことを考えてコントラクトを作る必要がある。

  • 変更可能なコントラクトを作るポイント
    • ストレージ用とロジック用のコントラクトを分ける
    • ストレージコントラクトを汎用的に作っておく
    • インターフェイス定義をしておいて、変更があってもインターフェイスは保障されるようにする

これまでのことを踏まえつつ、contractを作ると以下のような感じになる。

pragma solidity ^0.4.11;

contract Ownable {
  address public owner;

  function Ownable() {
    owner = msg.sender;
  }

  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }
}

contract storageContract is Ownable {
    address _this;
    address _sender;

    mapping (address => bool) allowContracts;

    function setAllowContracts(address _allowContract) onlyOwner {
        allowContracts[_allowContract] = true;
    }

    function checkAllowContract() private returns (bool) {
        return (allowContracts[msg.sender] == true);
    }

    mapping(bytes32 => uint) idMap;

    function getAndIncrementId(bytes32 key) returns (uint, bool) {
        if (!checkAllowContract()) {
            return (0, false);
        }
        var _v = idMap[key];
        idMap[key] = _v + 1;
        return (_v, true);
    }

    function getMaxId(bytes32 key) constant returns (uint) {
        return idMap[key];
    }

    function getIdArray(bytes32 key) constant returns (uint[]) {
        var _maxId = idMap[key];
        uint[] memory ids = new uint[](_maxId);
        for (uint i = 0; i < _maxId; i++) {
            ids[i] = i;
        }
        return ids;
    }

    mapping(bytes32 => string) stringMap;

    function setString(bytes32 key, string value) returns (bool) {
        if (!checkAllowContract()) {
            return false;
        }
        stringMap[key] = value;
        // thisはstorageContract
        _this = this;
        // msg.senderはmasterContract
        _sender = msg.sender;
        return true;
    }

    function getString(bytes32 key) constant returns (string) {
        return stringMap[key];
    }

    // this, sender確認用。不要。
    function getThisSender() returns (address, address) {
        return (_this, _sender);
    }
}

library logicLibrary {
    int constant NG = 0;
    int constant OK = 1;

    function checkName(string _name) returns (bool) {
        if (bytes(_name).length == 0) {
            return false;
        }
        return true;
    }

    function setUser(address _storage, string _name, string _email) internal returns (uint, int) {
        if (!checkName(_name)) {
            return (0, NG);
        }
        var (_userId, _ok) = storageContract(_storage).getAndIncrementId(sha3("userId"));
        if (!_ok) {
            return (0, NG);
        }
        _ok = storageContract(_storage).setString(sha3("user.name", _userId), _name);
        if (!_ok) {
            return (0, NG);
        }
        _ok = storageContract(_storage).setString(sha3("user.email", _userId), _email);
        if (!_ok) {
            return (0, NG);
        }
        return (_userId, OK);
    }
}

contract masterContractInterface {
    function setUser(string _name, string _email) returns (uint, int);
}

contract mainContract is masterContractInterface {
    using logicLibrary for address;
    address externalStorage;
    address _this;
    address _sender;

    function mainContract(address _externalStorage) {
        require(_externalStorage != address(0));
        externalStorage = _externalStorage;
    }

    function setUser(string _name, string _email) returns (uint, int) {
        // thisはmasterContract
        _this = this;
        // msg.senderはこの関数を呼び出したコントラクトのアドレス
        _sender = msg.sender;
        return externalStorage.setUser(_name, _email);
    }

    // sha3の値が知りたいだけ、クライアント側で計算すれば不要
    function getIdKey(string _key) returns (bytes32) {
        return sha3(_key);
    }
    function getValueKey(string _key, uint _userId) returns (bytes32) {
        return sha3(_key, _userId);
    }

    // this, sender確認用。不要。
    function getThisSender() returns (address, address) {
        return (_this, _sender);
    }
}

// 1. storageContractをdeployしてアドレスを取得
//      output: "0x11f0c46dc617619d51aa60e02daca7608dc714e7"
// 2. mainContractをdeplayしてアドレスを取得
//      input:  "0x11f0c46dc617619d51aa60e02daca7608dc714e7"
//      output: "0x91e3e0cd9afa1a0ced2456214a2e544a4f5a1db2"
// 3. storageContractのsetAllowContractでmainContractのアドレスを設定
//      input:  "0x91e3e0cd9afa1a0ced2456214a2e544a4f5a1db2"
// 4. mainContractのsetUserで一人目のユーザーを登録
//      input:  "user1", "email1"
//      output  0, 1
// 5. mainContractのsetUserで二人目のユーザーを登録
//      input:  "user2", "email2"
//      output  1, 1
// 6. mainContractのgetIdKeyで"userId"のsha3の値を取得
//      input:  "userId"
//      output: 0x27c962f3b25d487c0ce42b202affbfd86a5cc45d430047bd3644438ed354eaaa
// 7. storageContractのgetIdArrayでuserIdリストを確認
//      input:  "0x27c962f3b25d487c0ce42b202affbfd86a5cc45d430047bd3644438ed354eaaa"
//      output: 0, 1
// 8. mainContractのgetValueKeyで"user.name", 0のsha3の値を取得
//      input: "user.name", 0
//      output: 0x68582ccb44bf56e47bc7080f480a1889bc73a0c06b5be9bcca501755eb67fc91
// 9. mainContractのgetValueKeyで"user.email", 1のsha3の値を取得
//      input: "user.email", 1
//      output: 0xf8c4454c0d4b8625ee2090e89210f762544f806f2ca8d8c9c5b43b12acf64b50
// 10. storageContractのgetStringでuserId 0のユーザ名を取得
//      input: "0x68582ccb44bf56e47bc7080f480a1889bc73a0c06b5be9bcca501755eb67fc91"
//      output: user1
// 11. storageContractのgetStringでuserId 1のメールアドレスを取得
//      input: "0xf8c4454c0d4b8625ee2090e89210f762544f806f2ca8d8c9c5b43b12acf64b50"
//      output: email2

ここで問題なのは、ライブラリのビジネスロジックだけを更新したい場合どうすればいいのかという問題が出てくるのだけど、どうしたらいいのだろう。。。 基本的に、DB操作とビジネスロジックは近くにあって欲しいし、delegatecallとか使い倒すとそれはそれでgasコストのオーバヘッドが出てくる。

単純に思いつく方法としては、機能単位にコントラクトとライブラリのセットを細分化しておけば、変更が必要になった場合の影響が小さくなることが考えられるぐらい。

以下のサイトでもライブラリのアップグレードをどうするか考えているみたい。

name:
email:
comment: