以太坊智能合约的两种数据分离模式(部署可升级式智能合约)

重要!

做数据分离推荐使用2018年后的的Geth版本,即v1.8以上。在genesis.json创世文件的配置config里需添加拜占庭Block,如下:

"config": {
    "chainId": 1,
    "homesteadBlock": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0
}

"byzantiumBlock": 0 为必添加项!

 

    以太坊智能合约的设计是部署后不允许修改的,所以智能合约修改后重新部署数据便会丢失,但是难免有业务需求的变更,这时候便出现了可升级式智能合约,其原理是将数据合约和业务合约进行分离,用业务合约去调用数据合约,这样当需求变更时,只需要重新部署业务合约即可,而在数据合约里存储的数据则不会丢失。目前智能合约的数据和业务分离有两种方式,第一种是通过合约的地址进行调用,另一种是通过一个Proxy代理合约进行代理调用。地址调用十分简单,代理调用相比稍复杂一些,国外使用代理调用稍多一些。个人没有对比过两种方式的差异,大家可以自己选择,两种方式都可以实现数据分离。

 

1 地址调用方式

数据合约如下

pragma solidity ^0.4.24;

contract MyCoinData {
    
    address private master;
    
    mapping (address=>uint256) private balances;
   
    constructor () public {
        master = msg.sender;
        balances[master] = 1000;
    }
    
    function getBalance(address who) view public returns (uint256 value) {
        return balances[who];
    }
    
    function setBalance(address who, uint256 value) public {
        balances[who] = value;
    }
    
}

数据合约不做逻辑相关的代码,只保留get和set方法。

 

业务合约如下

pragma solidity ^0.4.24;

import "./DataContract.sol";

contract MyCoinLogic {
    
    MyCoinData data;
   
    constructor (address dataAddr) public {
        data = MyCoinData(dataAddr);
    }
    
    function getBalance(address who) view public returns (uint256 value) {
        return data.getBalance(who);
    }
    
    function setBalance(address who, uint256 value) public {
        data.setBalance(who, value);
    }
    
    function transfer(address fr, address to, uint256 value) public returns (bool result){
        if (data.getBalance(fr) - value < 0) {
            return false;
        } else {
            data.setBalance(fr, data.getBalance(fr) - value);
            data.setBalance(to, data.getBalance(to) + value);
            return true;
        }
    }
    
    // 其他逻辑代码...
    
}

import "./DataContract.sol" 代表引用同级目录的DataContract.sol。然后在MyCoinLogic里面声明一个MyCoinData,在构造方法里面通过传递数据合约的部署地址去初始化data。后面即可用data的get和set方法去操作数据合约。

部署合约时,先部署数据合约,得到数据合约地址,再传递地址到业务合约的constructor参数里面部署业务合约(智能合约部署方式本文不再累述)。当业务合约有新需求进行改动后,数据合约不动,用相同的数据合约地址再一次部署业务合约即可,数据合约的数据不会丢失,达到了数据和业务分离的要求。

 

2 代理调用模式

首先是数据基础合约,此处命名为MyCoinStorage,后面的合约都需要继承此合约。

pragma solidity ^0.4.24;

contract MyCoinStorage {
    
    address private master;
    
    mapping (address=>uint256) public balances;
   
    constructor () public {
        master = msg.sender;
        balances[master] = 1000;
    }
    
}

可以注意到没有get和set方法,因为数据基础合约相当于提供一个数据存储结构,以供后面的合约继承此结构。

另外注意balances修饰符为public。

 

然后是业务逻辑合约,此处命名为MyCoinLogic。

pragma solidity ^0.4.24;

import "./MyCoinStorage.sol";

contract MyCoinLogic is MyCoinStorage {
    
    function getBalance(address who) view public returns (uint256 value) {
        return balances[who];
    }
    
    function setBalance(address who, uint256 value) public {
        balances[who] = value;
    }
    
    function transfer(address fr, address to, uint256 value) public returns (bool result){
        if (balances[fr] - value < 0) {
            return false;
        } else {
            balances[fr] -= value;
            balances[to] += value;
            return true;
        }
    }
    
    // 其他逻辑代码...
    
}

Logic合约继承了Storage合约的数据存储结构,可以直接对父类的balances进行操作。

 

最后是代理合约,此处命名为MyCoinProxy。

pragma solidity ^0.4.24;

import "./MyCoinStorage.sol";

contract MyCoinProxy is MyCoinStorage {
    
    address private myCoinLogicAddr;

    function setLogicContractAddr(address addr) public {
        myCoinLogicAddr = addr;
    }

    function () public {
        
        address target = myCoinLogicAddr;
        
        assembly {
            let ptr := mload(0x40)
            calldatacopy(ptr, 0, calldatasize)

            let result := delegatecall(sub(gas, 10000), target, ptr, calldatasize, 0, 0)

            let size := returndatasize
            returndatacopy(ptr, 0, size)

            switch result
            case 0 { revert(ptr, size) }
            case 1 { return(ptr, size) }
        }
    }
    
}

首先Proxy合约继承了Storage合约的数据存储结构。然后定义了一个业务合约Logic的地址变量,提供一个setLogicContractAddr()方法用于替换业务合约的地址。function () public 这个没有名字的方法是个callback,所有请求都由这个方法进行处理,assembly是solidity语言的汇编,里面的代码作用是用delegatecall方法和Logic合约地址去调用实例化的Logic合约。

 

部署方法

Storage合约只提供数据存储结构,所以不需要部署。首先部署Logic合约,记住Logic合约的部署地址。然后再部署Proxy合约,记住Proxy合约的部署地址。接着调用Proxy合约的setLogicContractAddr()方法存储刚才部署的Logic合约的地址,此时Proxy合约的变量myCoinLogicAddr即代表Logic合约的实例地址。最后是重点,需要我们使用Proxy合约的地址去再一次部署Logic合约(是的,你没听错)即可。

eth控制台的部署方式如下:先定义Logic合约的ABI(像这样,var mycoinlogicContract =web3.eth.contract([{"constant":true...,太长了我没写完,如有不清楚可以自行百度eth控制台部署智能合约的方法)。然后使用eth的contract.at()方法去实例化Logic合约(像这样,var mycoinlogic = mycoinlogicContract.at(这里填写Proxy合约的地址!))即可。

Truffle的部署方式则简单很多:deployer.deploy(MyCoinLogic, 这里填写Proxy合约的地址!);

可以看到,虽然外表是Logic合约,但实际上它代表的是Proxy合约的实例。注意现在我们是两个实例,三个合约壳子,不要搞混了!

用户的请求是发到我们最后一个部署的Logic合约上,然后会被Proxy合约代理到第一个部署的Logic合约上进行业务处理。

 

更新方法

Proxy合约不动。

先部署一次修改后的Logic合约,记下地址。然后调用Proxy合约的setLogicContractAddr()方法更新存储在Proxy合约里的Logic合约地址myCoinLogicAddr。最后故技重施,使用Proxy合约的地址第二次部署Logic合约即可。只要不动Proxy合约,数据就不会丢失!

 

 

 

以上两种智能合约的数据分离方式个人已经测试过了,但难免有纰漏的地方,如果有什么问题大家可以回复我!

本文为原创,欢迎转载,但转载请注明出处!

你可能感兴趣的:(以太坊智能合约的两种数据分离模式(部署可升级式智能合约))