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

hongji   |     |   1971 次阅读

由于区块链不可篡改的特性,智慧合约一但部署到区块链上,其执行的逻辑就无法再更改。长期来看,这个重要的特性反而限制了合约的弹性和发展。

接下来要介绍如何设计及部署合约才能让合约在需要时可以更新。但这里的更新意思不是修改已经部署的合约,而是部署新的合约、新的执行逻辑但同时能继续利用已经存在数据。

首先要知道的是 Ethereum Virtual Machine(EVM)如何知道要执行合约的哪个函式。合约最后都会被编译成bytecode,而你发起一个transaction要执行合约里的某个函式时,交易里的data字段同样也是bytecode而不是人看得懂的函式名称。 以一个简单的合约为例:

contract Multiply {

   function multiply(int x, int y) constant returns(int) {

       return x*y; 

    }

}

编译完的bytecode:

6060604052341561000c57fe5b5b60ae8061001b6000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633c4308a814603a575bfe5b3415604157fe5b605e60048080359060200190919080359060200190919050506074565b6040518082815260200191505060405180910390f35b600081830290505b929150505600a165627a7a72305820c40f61d36a3a1b7064b58c57c89d5c3d7c73b9116230f9948806b11836d2960c0029

如果你今天要执行multiply函式,算出8*7等于多少,你的transaction里的data字段会是

0x3c4308a800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000007

分成三个部分: 第一个是四个byte的3c4308a8,第二和第三个分别是32 byte长的参数,8和7。

3c4308a8是multiply函式的 signature,是取函式名称和参数型态丢进哈希后取前四个byte而得(不包含 0x ):

sha3("multiply(int256,int256)"));
//0x3c4308a8851ef99b4bfa5ffd64b68e5f2b4307725b25ad0d14040bdb81e3bafcsha3("multiply(int256,int256)")).substr(2,8);
//3c4308a8

EVM就是靠函式的signature来知道该执行哪个函式的。在合约编译完的bytecode里搜寻也能找到此signature。

接下来要介绍Solidity里的三种函数调用方式: callcallcodedelegatecall

  • call:一般的呼叫都是这种方式,执行背景跳到下一个函式的环境(这里的环境指 msg 的值和合约的 Storage )。如果被呼叫者是不同合约的函式则变成被呼叫者合约的环境,且msg.sender变成呼叫者。

  • callcode:和 call 相同,只是将被呼叫者的函式搬到呼叫者的环境里执行。

假设A合约的x函式用 callcode 方式呼叫B合约的y函式,则会在A合约里执行y函式,使用A的变量,所以如果y函式里修改某个变量的值且这个变量的名称刚好和A的某个变量名称一样,则A的该变量就会被修改。就把它想象成A多了一个y函式并执行。

  • delegatecall:和 callcode 相同,都是把被呼叫的函式搬到呼叫者的环境里执行,只是差在msg.sender的值。 用一个例子讲解会比较清楚:假设A合约用 delegatecall 的方式呼叫B合约的函式,B合约的函式接下用 callcode 或 call 的方式呼叫C合约的函式,则函式里看到的msg.sender会是B;但如果B改用 delegatecall 的方式呼叫C合约的函式,则函式里看到的msg.sender会是A。就把它想象成把msg相关的值保持不变传递下去。

接下来实际来看delegatecall的效果:

contract Plus {
    int z;
   function plus(int x, int y) {
       z = x+y;
    }
}
contract Multiply {
   int public z;
   function multiply(int x, int y) {
       z = x*y;
    }
   function delegateToPlus(address _plus, int x, int y) {
       _plus.delegatecall( bytes4(sha3("plus(int256,int256)")) ,x ,
       y);
    }
}

部署并按顺序执行Multiply的multiply和delegateToPlus并观察z值的变化:

1

可以看到执行delegatecall之后z的值变成是8+7。 所以如果要让我们未来可以改变执行逻辑的话要怎么写呢?

contract Plus {
   int z;
   function plus(int x, int y) { //sig:"0xccf65503"
       z = x+y;
    }
}
contract Multiply {
   int z;
   function multiply(int x, int y) { //sig:"0x3c4308a8"
       z = x*y;
    }
}
contract Main {
   int public z;
   function delegateCall(address _dest, bytes4 sig, int x, int y) {
       _dest.delegatecall(sig, x , y);
    }
}

我们将合约的地址和函式的 signature 当作参数传给 delegateCall 去执行,假设原本是用Plus合约的执行逻辑,现在我们更新成Multiply合约:

2

0x4429 是Plus合约的地址, 0xe905 是Multiply合约的地址。

我们以后只要给它改变后的函式的 signature 和合约地址就可以使用新的执行逻辑了!

但如果合约不是只给一个人使用的话,当要更新合约的时候所有参与的人都必须要更新新合约的位置。这时可以用一个合约来帮我们导到新的合约位置,就像路由器一样,我们统一发送(还是以 delegatecall 的形式)到路由合约,再由路由合约帮我们导到正确的位置,未来更新合约就只需要更新路由合约的数据。

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

    functioninitialize() {
       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);
    }

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

执行顺序:
1. 执行Main.deployDispatcher()部署路由合约
2. 部署upgrade合约并将其address当作Main.updateUpgrade()的参数传入来更新upgrade合约的地址信息。
3. 执行Main.delegateCall(),参数是plus(int256,int256)的signature和任意两个值。
4. 执行Main.get(),藉由delegatecall去呼叫upgrade合约的get函式,回传相加完的z值。因为是delegatecall,所以这个z值其实是Main合约自己的,upgrade合约的z值还是零。

如果 delegatecall 呼叫的函式有回传值的话,必须要用 assembly 来手动搬移回传值,因为 delegatecall 和 call 一样,只会回传 true of false 来代表执行是否成功。Dispatcher 在转传呼叫同样也是用 assembly code 。

但因为是用 assembly 手动搬移回传值,因此前提是回传值的长度必须是固定且已知的,所以当我们在步骤2更新 upgrade 合约时,Dispatcher 合约同时要去呼叫 upgrade 合约的 initialize() 函式,upgrade 合约在 initialize 函式里将它所有会有回传值的函式的回传值大小写入returnSizes中,之后如果呼叫具有回传值的函式,Dispatcher就知道该返还多少大小的回传值。

这里还有一个重点是变量宣告的顺序

因为合约执行要取用变量的值的时候,它会到对应的Storage位置去找。所以如果你的合约变量宣告像这样子

upgrade:
int x
int y

 — — — —

Dispathcer:
int x
int y

 — — — —

Main:
int x
int abc
int y

当upgrade合约的函式要用到x和y的值的时候,它会找不到y,因为Storage是Main的。

Reference:

  1. http://ethereum.stackexchange.com/questions/3667/difference-between-call-callcode-and-delegatecall
  2. https://gist.github.com/Arachnid/4ca9da48d51e23e5cfe0f0e14dd6318f

原文链接: 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-cbe015bdb339
作者: NIC Lin(林修平)

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


可更新智能合约系列:

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


你可能还会喜欢:

PPT分享 | 以太坊钱包分析与介绍
从智能合约到半智能审判法庭
三分钟入门区块链语言Solidity函数

 
1 人喜欢