教程 | 在区块链上建立可更新的智慧合约(二)

hongji   |     |   1138 次阅读

这篇介绍用library的方式来建立可更新的合约逻辑。

Library

Library 是另外一种形式的 contract ,宣告方式也几乎一样: Library libA{} 。Library 会被部署在链上,有一个专属的 address ,任何人都可以呼叫它,但是Library

  1. 不能持有ether
  2. 没办法储存任何数据,Library 里面只有函式(动作)。合约呼叫 Library 是用 delegatecall 的形式,所以变成合约用 Library 里的函式(动作)来对合约自己的变量做操作。

如果合约会用到 Library,则合约在编译完后会在 bytecode 中留下一段空白,这个空白就是要用来填 Library 的位置的。可以手动填,solc 和 truffle 等工具都有提供 link 到 Library 的功能。

Library还有另外一个使用技巧 — using lib for type;
用来将指定的Library函式依附(attach)到指定的型别上。例如

library Action {
  struct Data { mapping(address => uint) amount; }

  function insert(Data storage self, uint value)
      returns (bool)
  {
      if (self.amount[msg.sender] >= 0)
        return false;
      self.amount[msg.sender] = value;
      return true;
  }
}


contract Bet{
    using Action for Action.Data;
    Action.Data playerMapping;

    function register(uint value) {
        if (!playerMapping.insert(value))
            throw;
    }
}

合约里的 using Action for Action.Data 表示将 Action library 里的函式都依附到 Action.Data 这个资料型别(一个 struct )上,型别为 Action.Data 的 playerMapping 就可以直接使用 Action library 的 insert 函式。如果是使用这种方式呼叫函式的话,则被依附的变量会被当作函式的第一个参数(在这个例子就是 playeMapping 被当作 insert 的 self 参数)。

或是套用在其他数据型别上,这个官方范例将 Search library 的函式依附到正整数数组上:

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) {
        // This performs the library function call
        uint index = data.indexOf(_old);
        if (index == uint(-1))
            data.push(_new);
        else
            data[index] = _new;
    }
}

如果type是星号(*),表示将函式依附到所有资料型别上:

using lib for *;

不使用using for并不会影响Library的使用,就差在某些情况使用using for会让函式执行比较易懂,例如:

playerMapping.insert(value) v.s Action.insert(playerMapping, value)

Upgradable Library

如果要用 Library 来建立可更新的合约逻辑,那表示我们也会需要更新 Library 。但 Library 不是在部署前就需要将地址写死在 bytecode 里吗?
这里我们一样利用一个Dispathcer来解决。

我们要做的是将 Dispatcher 的 address 写死在主合约,让主合约把 Dispatcher 当作是 Library 用 delegatecall 的方式呼叫 Dispatcher ,Dispatcher 再一次用 delegatecall 传给Library。主合约并不会知道 Dispatcher 是不是真的是一个 Library (它认为它是),只要 Dispatcher 收到这个呼叫能顺利执行且成功返回结果即可。

这是我们原本用合约的方式建立可更新合约逻辑:

contract Upgrade {
    mapping(bytes4=>uint32) returnSizes;
    int z;

    function initialize() {
        returnSizes[bytes4(sha3("get()"))] = 32;
    }

    function plus(int _x, int _y) {
        z = _x + _y;
    }
    function get() returns(int) {
        return z;
    }
}
contract Dispatcher {
    mapping(bytes4=>uint32) returnSizes;
    int z;
    address upgradeContract;
    address public dispatcherContract;
function replace(address newUpgradeContract) {
        upgradeContract = newUpgradeContract;
        upgradeContract.delegatecall(bytes4(sha3("initialize()")));
    }
function() {
        bytes4 sig;
        assembly { sig := calldataload(0) }
        var len = returnSizes[sig];
        var target = upgradeContract;

        assembly {
            calldatacopy(mload(0x40), 0x0, calldatasize)
            delegatecall(sub(gas, 10000), target, mload(0x40),
                         calldatasize, mload(0x40), len)
            return(mload(0x40), len)
        }
    }
}
contract Main {
    mapping(bytes4=>uint32) public returnSizes;
    int public z;
    address public upgradeContract;
    address public dispatcherContract;

    function deployDispatcher() {
        dispatcherContract = new Dispatcher();
    }

    function updateUpgrade(address newUpgradeContract) {
        dispatcherContract.delegatecall(
            bytes4( sha3("replace(address)")), newUpgradeContract
        );
    }

    function delegateCall(bytes4 _sig, int _x, int _y) {
        dispatcherContract.delegatecall(_sig, _x, _y);
    }

    function get() constant returns(int output){
        dispatcherContract.delegatecall(bytes4( sha3("get()")));
        assembly {
            output := mload(0x60)
        }
    }
}

这边我们要把

  1. Upgrade 改成 Library
  2. 将 Upgrade 的变数移除,Upgrade 和 main 的 z 值改放进 struct 里
  3. main 里用 using for 的方式修改z的值
library Upgrade {
    struct Data{
        int z;
    }

    function plus(Data storage self, int _x, int _y) {
        self.z = _x + _y;
    }
    function get(Data storage self) returns(int) {
        return self.z;
    }
}
contract DispatcherStorage {
    address public addrUpgrade;
    mapping(bytes4 => uint32) public sizes;

    function DispatcherStorage(address newUpgrade) {
        sizes[bytes4(sha3("get(Upgrade.Data storage)"))] = 32;
        replace(newUpgrade);
    }

    function replace(address newUpgrade) {
        addrUpgrade = newUpgrade;
    }
}
contract Dispatcher {
function() {
        DispatcherStorage dispatcherStorage = DispatcherStorage(0xc8e2211a1241dc1906bc1eee85b1807fd4c820e4);
        uint32 len = dispatcherStorage.sizes(msg.sig);
        address target = dispatcherStorage.addrUpgrade();

        assembly {
            calldatacopy(mload(0x40), 0x0, calldatasize)
            delegatecall(sub(gas, 10000), target, mload(0x40),
                         calldatasize, mload(0x40), len)
            return(mload(0x40), len)
        }
    }
}
contract Main {
    using Upgrade for Upgrade.Data;
    Upgrade.Data data;

    function plus(int _x, int _y) {
        data.plus(_x, _y);
    }

    function get() constant returns(int output){
        data.get();
        assembly {
            output := mload(0x60)
        }
    }
}

还有一个新的改变是新增了 DispatcherStorage 合约,将 Dispatcher 的变数放进 DispatcherStorage 里。这是因为 main 现在将 Dispatcher 视为 Upgrade ,只能用 Upgrade 的函数调用,所以如果 Dispatcher 里面有变量也没办法操作。另外 main 也没办法用 deployDispatcher() 函式来动态部署新的 Dispatcher 合约,Dispatcher 的address必须在部署前塞进 main 的 bytecode 里。

所以新增了一个 DispatcherStorage 来储存 Dispatcher 需要用的信息,每次 Dispatcher 收到呼叫,就会透过 DispatcherStorage (这边用 call 而不是 delegatecall )来取回相关信息。取回信息后再送出到指定的 Library address 。

如果要更新 Library,部署完后透过 DispatcherStorage.replace() 来更新。

img

部署的顺序:

  1. Upgrade
  2. DispatcherStorage
  3. 将 DispatcherStorage 的 address 填入 Dispatcher 的 code 里编译后再部署 Dispatcher
  4. 将 Dispatcher 的 address 填入 Main 的 bytecode 里再部署 Main

使用合约或 Library 的方式都可以建立出可更新的合约逻辑。
用 Library 的方式可以省下较多的储存成本(当然如果你不在意储存成本的话就没有差)但限制是 Library 只能对他知道的 struct 里的变量做操作。

Reference:

[1]http://solidity.readthedocs.io/en/develop/contracts.html#libraries

[2]https://blog.aragon.one/library-driven-development-in-solidity-2bebcaf88736

[3]https://medium.com/zeppelin-blog/proxy-libraries-in-solidity-79fbe4b970fd


原文链接: https://medium.com/@twedusuck/%E5%9C%A8%E5%8D%80%E5%A1%8A%E9%8F%88%E4%B8%8A%E5%BB%BA%E7%AB%8B%E5%8F%AF%E6%9B%B4%E6%96%B0%E7%9A%84%E6%99%BA%E6%85%A7%E5%90%88%E7%B4%84-%E4%BA%8C-24f07206d033
作者: NIC Lin(林修平)

EthFans经作者授权后转载。本文原文为繁体中文,为大陆读者阅读需要,转成简体中文,专有名词则原样沿用。


可更新智能合约系列:

教程 | 在区块链上建立可更新的智慧合约(一)
教程 | 在区块链上建立可更新的智慧合约(二)


你可能还会喜欢:

用智能合约来实现保险柜
区块链技术(九):以太坊非公开拍卖智能合约案例
以太坊连载(12):创建安全多签名钱包及高级设置

 
0 人喜欢