在區塊鏈上建立可更新的智慧合約(一)

NIC Lin
11 min readMar 10, 2017

--

由於區塊鏈不可篡改的特性,智慧合約一但部署到區塊鏈上,其執行的邏輯就無法再更改。長期來看,這個重要的特性反而限制了合約的彈性和發展。

接下來要介紹如何設計及部署合約才能讓合約在需要時可以更新。但這裡的更新意思不是修改已經部署的合約,而是部署新的合約、新的執行邏輯但同時能繼續利用已經存在資料。

首先要知道的是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)"));
//0x3c4308a8851ef99b4bfa5ffd64b68e5f2b4307725b25ad0d14040bdb81e3bafc sha3("multiply(int256,int256)")).substr(2,8);
//3c4308a8

EVM就是靠函式的signature來知道該執行哪個函式的。在合約編譯完的bytecode裡搜尋也能找到此signature。

接下來要介紹Solidity裡的三種函式呼叫方式:call、callcode和delegatecall

  • 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值的變化:

可以看到執行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合約:

0x4429 是Plus合約的位址, 0xe905 是Multiply合約的位址。
我們以後只要給它改變後的函式的signature和合約地址就可以使用新的執行邏輯了!

但如果合約不是只給一個人使用的話,當要更新合約的時候所有參與的人都必須要更新新合約的位置。這時可以用一個合約來幫我們導到新的合約位置,就像路由器一樣,我們統一發送(還是以delegatecall的形式)到路由合約,再由路由合約幫我們導到正確的位置,未來更新合約就只需要更新路由合約的資料。

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. 執行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

--

--